package lkgateway

import (
	"context"
	"errors"
	"log"
	"net/http"
	"time"

	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter"
	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/mock"
)

// igwClientAdapter адаптирует igw.Client под узкий nsdadapter.IGWClient:
// разворачивает (channel, since, type) в igw.ListFilter.
type igwClientAdapter struct{ c *igw.Client }

func (a igwClientAdapter) SendPackage(ctx context.Context, channel, packageType string, body []byte) (string, error) {
	return a.c.SendPackage(ctx, channel, packageType, body)
}

func (a igwClientAdapter) ListIncoming(ctx context.Context, channel string, since time.Time, packageType string) ([]igw.Package, error) {
	return a.c.ListIncoming(ctx, igw.ListFilter{Channel: channel, Date: since, Type: packageType})
}

// ServerConfig — конфигурация HTTP-сервера lk-gateway.
type ServerConfig struct {
	Addr              string
	DefaultSender     m2m.DeponentCode
	DefaultReceiver   m2m.DeponentCode
	CheckOptions      func() CheckOptions
	MockDecisionDelay time.Duration // 0 = дефолт 3 секунды
	SetupPath         string        // путь к JSON-файлу runtime-конфига (пусто = ~/.bj/setup.json)
}

// Server — обвязка HTTP + сервис + workers.
type Server struct {
	cfg    ServerConfig
	svc    *Service
	mock   *mock.Sender
	store  *SeedStore
	rc     *RuntimeConfig
	mux    *http.ServeMux
	server *http.Server

	// igwClient/igwChannel заполнены только в реальном режиме (ИШ настроен).
	// На них работает поллер входящих pollIncoming — забирает ответы НРД
	// (M2MTransferDecision/Response) и применяет через svc.ApplyDecision.
	igwClient  *igw.Client
	igwChannel string
}

// NewServer собирает Server с репозиторием, mock NSDSender, SeedStore
// и REST + Admin маршрутами. Выбор Repository:
//   - если в runtime-конфиге (или ENV-fallback в cfg) задан PostgresDSN
//     и pgx-Pool успешно создаётся — используется PostgresRepository;
//   - иначе fallback на MemoryRepository (M2-демо).
func NewServer(cfg ServerConfig) (*Server, error) {
	store := NewSeedStore()
	mockCfg := mock.DefaultConfig()
	mockCfg.NSDSenderCode = "MC0010300000"
	if cfg.MockDecisionDelay > 0 {
		mockCfg.DecisionDelay = cfg.MockDecisionDelay
	}

	rc, err := NewRuntimeConfig(cfg.SetupPath)
	if err != nil {
		return nil, err
	}

	// Выбор NSD-сендера: если в runtime-конфиге задан профиль ИШ и URL —
	// используем реальный nsdadapter поверх REST ИШ; иначе mock-эмуляция.
	// mockSender остаётся не-nil только в mock-режиме — на нём висит
	// consumeDecisions (реальные Decision приходят поллером входящих ИШ).
	var sender m2mcore.NSDSender
	var mockSender *mock.Sender
	var igwClient *igw.Client
	var igwChannel string
	{
		s := rc.Snapshot()
		if s.NSD.IGWBaseURL != "" && s.NSD.Profile != "" {
			prof, perr := nsdadapter.LookupProfile(s.NSD.Profile)
			if perr != nil {
				log.Printf("lk-gateway: профиль ИШ %q неизвестен (%v) — fallback mock", s.NSD.Profile, perr)
			} else {
				prof.IGWBaseURL = s.NSD.IGWBaseURL // override URL из setup.json
				cl := igw.NewClient(s.NSD.IGWBaseURL)
				sender = nsdadapter.NewSender(prof, igwClientAdapter{c: cl})
				igwClient = cl
				// Канал ИШ резолвится по составному коду <канал>+<депонент>.
				igwChannel = prof.Channel + string(cfg.DefaultSender)
				log.Printf("lk-gateway: реальный ИШ-адаптер — профиль %s, канал %s, ИШ %s",
					prof.Name, igwChannel, s.NSD.IGWBaseURL)
			}
		}
		if sender == nil {
			mockSender = mock.NewSender(mockCfg)
			sender = mockSender
			log.Printf("lk-gateway: NSD mock-режим (Decision через эмуляцию)")
		}
	}

	// Repository: pgx если DSN указан, иначе in-memory.
	var repo m2mcore.Repository = m2mcore.NewMemoryRepository()
	if dsn := rc.Snapshot().Postgres.DSN; dsn != "" {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		pgRepo, pgErr := m2mcore.NewPostgresRepository(ctx, dsn)
		cancel()
		if pgErr != nil {
			log.Printf("lk-gateway: PostgresRepository отказал, fallback MemoryRepository: %v", pgErr)
		} else {
			repo = pgRepo
			log.Printf("lk-gateway: PostgresRepository подключён (m2m_core.deals)")
		}
	}

	svc := NewService(Config{
		Repository:      repo,
		Sender:          sender,
		Store:           store,
		Recorder:        m2mcore.NewMemoryRecorder(),
		DefaultSender:   cfg.DefaultSender,
		DefaultReceiver: cfg.DefaultReceiver,
	})

	// Если runtime-конфиг уже содержит callback URL — применяем его.
	if s := rc.Snapshot(); s.LK.CallbackURL != "" {
		svc.callbackURL = s.LK.CallbackURL
	}

	mux := http.NewServeMux()
	RegisterAPI(mux, svc)

	// CheckOptions берётся из runtime-конфига при каждом запросе на дашборд.
	checkOpts := func() CheckOptions {
		s := rc.Snapshot()
		profile := "demo (mock NSD)"
		if s.NSD.Profile != "" {
			profile = s.NSD.Profile
		}
		return CheckOptions{
			PostgresDSN:    s.Postgres.DSN,
			CryptoSocket:   s.Crypto.SocketPath,
			NSDAdapterURL:  s.NSD.IGWBaseURL,
			LKCallbackURL:  s.LK.CallbackURL,
			Profile:        profile,
			CryptoProvider: nonEmpty(s.Crypto.Provider, "stub"),
			Timeout:        2 * time.Second,
		}
	}
	if cfg.CheckOptions != nil {
		// Опциональный override (например, из cmd/lk-gateway для override ENV-перетягивания).
		checkOpts = cfg.CheckOptions
	}

	adminTpl, err := RegisterAdmin(mux, svc, rc, checkOpts)
	if err != nil {
		return nil, err
	}
	registerSetup(mux, adminTpl, rc, svc)
	registerHealth(mux)
	registerSetCallback(mux, svc, rc)
	registerSeedListing(mux, store)

	return &Server{
		cfg:        cfg,
		svc:        svc,
		mock:       mockSender,
		store:      store,
		rc:         rc,
		mux:        mux,
		igwClient:  igwClient,
		igwChannel: igwChannel,
		server: &http.Server{
			Addr:              cfg.Addr,
			Handler:           mux,
			ReadHeaderTimeout: 5 * time.Second,
		},
	}, nil
}

// RuntimeConfig возвращает текущий runtime-конфиг (для тестов).
func (s *Server) RuntimeConfig() *RuntimeConfig { return s.rc }

func nonEmpty(s, def string) string {
	if s == "" {
		return def
	}
	return s
}

// SetCallbackURL обновляет адрес, куда отправлять PATCH callback'и в ЛК.
func (s *Server) SetCallbackURL(url string) { s.svc.callbackURL = url }

// Service возвращает Service для тестов.
func (s *Server) Service() *Service { return s.svc }

// Mock возвращает mock-сендер.
func (s *Server) Mock() *mock.Sender { return s.mock }

// Store возвращает SeedStore.
func (s *Server) Store() *SeedStore { return s.store }

// Mux возвращает обработчик (для httptest).
func (s *Server) Mux() http.Handler { return s.mux }

// Run поднимает HTTP-сервер и фоновый Decisions-consumer.
// Блокируется до ctx.Done().
func (s *Server) Run(ctx context.Context) error {
	go s.consumeDecisions(ctx)

	// Поллер входящих от НРД (только в реальном режиме ИШ): забирает
	// ответы робота/контрагента и применяет их через ApplyDecision.
	if s.igwClient != nil && s.igwChannel != "" {
		go s.pollIncoming(ctx)
	}

	// Фоновая авто-проверка обновлений из артефактории (если включена).
	go NewUpdater(s.rc).updateLoop(ctx)

	// Авто-обновление сертификатов УЦ раз в сутки (если оператор включил).
	stopCACerts := StartCACertsAutoUpdater(s.rc)
	defer stopCACerts()

	// Doc-watcher: раз в сутки проверяет сайт НРД на новые PDF и
	// эмитирует новости в ленту. Дефолтные источники + дефолтные
	// новости (окно техработ TEST3, появление робота) сеются один раз.
	EnsureDocSources(s.rc)
	SeedDefaultNews(s.rc)
	stopDocWatcher := StartDocWatcher(s.rc)
	defer stopDocWatcher()

	errCh := make(chan error, 1)
	go func() {
		log.Printf("lk-gateway: listen %s", s.cfg.Addr)
		errCh <- s.server.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = s.server.Shutdown(shutdownCtx)
		return nil
	case err := <-errCh:
		if errors.Is(err, http.ErrServerClosed) {
			return nil
		}
		return err
	}
}

// consumeDecisions слушает Decisions от mock и обновляет соответствующие сделки.
func (s *Server) consumeDecisions(ctx context.Context) {
	if s.mock == nil {
		// Реальный ИШ-режим: Decision приходят не из mock-канала, а через
		// поллер входящих пакетов ИШ (отдельный механизм). Здесь нечего слушать.
		<-ctx.Done()
		return
	}
	for {
		select {
		case <-ctx.Done():
			return
		case d := <-s.mock.Decisions():
			if d == nil {
				continue
			}
			if err := s.svc.ApplyDecision(ctx, d); err != nil {
				log.Printf("lk-gateway: ApplyDecision GUID=%s: %v", d.Header.GUID, err)
			} else {
				log.Printf("lk-gateway: Decision применён GUID=%s, callback в %s", d.Header.GUID, s.svc.callbackURL)
			}
		}
	}
}

func registerHealth(mux *http.ServeMux) {
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok\n"))
	})
}

// registerSetCallback — служебный POST /admin/api/callback-url для
// эмулятора ЛК, чтобы сообщить gateway свой URL. Если URL уже сохранён
// в runtime-конфиге (пользователь явно настроил его через UI), запрос
// эмулятора игнорируется — приоритет у явно настроенного.
func registerSetCallback(mux *http.ServeMux, svc *Service, rc *RuntimeConfig) {
	mux.HandleFunc("/admin/api/callback-url", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "method", http.StatusMethodNotAllowed)
			return
		}
		url := r.URL.Query().Get("url")
		if url == "" {
			http.Error(w, "url required", http.StatusBadRequest)
			return
		}
		if rc != nil {
			if s := rc.Snapshot(); s.LK.CallbackURL != "" {
				w.WriteHeader(http.StatusOK)
				_, _ = w.Write([]byte("kept-user-configured"))
				return
			}
		}
		svc.callbackURL = url
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})
}

func registerSeedListing(mux *http.ServeMux, store *SeedStore) {
	mux.HandleFunc("/admin/api/clients", func(w http.ResponseWriter, _ *http.Request) {
		type c struct {
			ID, LastName, FirstName, MiddleName string
		}
		out := make([]c, 0)
		for _, cl := range store.Clients() {
			out = append(out, c{ID: cl.ID, LastName: cl.LastName, FirstName: cl.FirstName, MiddleName: cl.MiddleName})
		}
		writeJSON(w, http.StatusOK, out)
	})
}
