package lkgateway

import (
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sync"
	"time"
)

// RuntimeConfig — конфигурация подсистем, редактируемая через admin UI
// без перезапуска. Сохраняется в JSON-файл (BJ_SETUP_PATH или
// ~/.bj/setup.json), грузится при старте.
type RuntimeConfig struct {
	mu   sync.RWMutex
	path string
	data Settings
}

// Settings — сериализуемое представление настроек.
type Settings struct {
	Postgres  PostgresSettings `json:"postgres"`
	Crypto    CryptoSettings   `json:"crypto"`
	NSD       NSDSettings      `json:"nsd"`
	LK        LKSettings       `json:"lk"`
	CACerts   CACertsSettings  `json:"ca_certs"`
	News      NewsSettings     `json:"news"`
	Update    UpdateSettings   `json:"update"`
	License   LicenseSettings  `json:"license"`
	LastTest  *TestRunResult   `json:"last_test,omitempty"`
	UpdatedAt time.Time        `json:"updated_at"`
}

// LicenseSettings — лицензионный ключ (подписанный токен).
type LicenseSettings struct {
	Key       string `json:"key"`        // компактный токен payload.signature.keyid
	PublicKey string `json:"public_key"` // base64 (если не зашит в бинарь)
}

// UpdateSettings — авто-обновления из артефактории (#18/#20).
type UpdateSettings struct {
	BaseURL    string    `json:"base_url"`          // https://updates.example.com
	Channel    string    `json:"channel"`           // "stable" | "beta"
	PublicKey  string    `json:"public_key"`        // base64 Ed25519 (если не зашит в бинарь)
	AutoCheck  bool      `json:"auto_check"`        // проверять автоматически
	LastCheck  time.Time `json:"last_check"`        // когда последний раз проверяли
	LastResult string    `json:"last_result"`       // текст результата проверки
	Available  string    `json:"available_version"` // доступная версия (если новее)
	Notes      string    `json:"notes,omitempty"`   // заметки доступного релиза
}

// NewsSettings — лента новостей (события системы, окна техработ, обновления
// документации НРД). События добавляются вручную через UI или автоматически
// doc-watcher'ом и cron-задачами. Каждое событие может быть скрыто (Dismissed)
// оператором, но не удалено — лента служит «журналом» для аудита.
type NewsSettings struct {
	Items          []NewsItem    `json:"items"`
	DocSources     []DocSource   `json:"doc_sources"`     // URL'ы для авто-проверки (NSD pages)
	LastDocCheck   time.Time     `json:"last_doc_check"`
	DocCheckResult string        `json:"doc_check_result"`
}

// NewsItem — одно событие в ленте.
type NewsItem struct {
	ID        string    `json:"id"` // уникальный идентификатор для dismiss
	At        time.Time `json:"at"`
	Kind      string    `json:"kind"` // "maintenance" | "feature" | "doc-update" | "manual" | "system"
	Title     string    `json:"title"`
	Body      string    `json:"body"`
	URL       string    `json:"url,omitempty"`        // ссылка на источник
	ValidFrom time.Time `json:"valid_from,omitempty"` // для maintenance окон
	ValidTo   time.Time `json:"valid_to,omitempty"`
	Dismissed bool      `json:"dismissed"`
}

// DocSource — страница НРД, которую doc-watcher периодически проверяет.
type DocSource struct {
	URL          string            `json:"url"`
	Name         string            `json:"name"`           // человекочитаемое имя
	LastChecked  time.Time         `json:"last_checked"`
	KnownPDFs    map[string]string `json:"known_pdfs"`     // url → sha256
}

// CACertsSettings — URL'ы для авто-загрузки сертификатов УЦ НРД и нашего
// УЦ. Список редактируется пользователем; раз в сутки фоновая горутина
// перекачивает каждый URL и сохраняет сертификат, если он поменялся.
type CACertsSettings struct {
	URLs          []string         `json:"urls"`
	AutoUpdate    bool             `json:"auto_update"`
	LastFetch     time.Time        `json:"last_fetch"`
	LastFetchLog  string           `json:"last_fetch_log"`
	FetchedCerts  []FetchedCACert  `json:"fetched_certs"`
}

// FetchedCACert — информация о последнем удачно скачанном сертификате.
type FetchedCACert struct {
	URL       string    `json:"url"`
	SHA256    string    `json:"sha256"`
	SubjectCN string    `json:"subject_cn"`
	IssuerCN  string    `json:"issuer_cn"`
	NotAfter  time.Time `json:"not_after"`
	Store     string    `json:"store"`
	FetchedAt time.Time `json:"fetched_at"`
	Error     string    `json:"error,omitempty"`
}

// PostgresSettings — DSN для подключения к БД (M2-шаг-3).
type PostgresSettings struct {
	DSN string `json:"dsn"`
}

// CryptoSettings — путь к PKCS#11 модулю и тип провайдера.
type CryptoSettings struct {
	Provider   string `json:"provider"`    // "stub" | "validata"
	SocketPath string `json:"socket_path"` // UDS crypto-service
	ModulePath string `json:"module_path"` // путь до .so модуля PKCS#11
	Profile    string `json:"profile"`     // активный профиль Валидаты (имя из pki1.conf)
}

// NSDSettings — профиль и подключение к ИШ НРД.
type NSDSettings struct {
	Profile      string `json:"profile"`       // "guest-gost", "test3-gost", ...
	IGWBaseURL   string `json:"igw_base_url"`  // http://host:port
	KeyContainer string `json:"key_container"` // имя контейнера (на стороне ИШ)

	// Депозитарные реквизиты клиента — откуда списываются бумаги
	// (SettlementLocation в M2MTransferRequest). Из договора/письма НРД.
	DeponentCode string `json:"deponent_code"` // депкод, напр. MC0413600000
	AccountID    string `json:"account_id"`    // депозитарный счёт
	SectionID    string `json:"section_id"`    // раздел депозитарного счёта
}

// LKSettings — настройки callback в ЛК клиента.
type LKSettings struct {
	CallbackURL string `json:"callback_url"`
}

// TestRunResult — результат последнего тестового прогона.
type TestRunResult struct {
	StartedAt   time.Time `json:"started_at"`
	FinishedAt  time.Time `json:"finished_at"`
	ClaimID     string    `json:"claim_id"`
	FinalStatus string    `json:"final_status"`
	OK          bool      `json:"ok"`
	Message     string    `json:"message"`
}

// NewRuntimeConfig создаёт runtime-конфиг, читая JSON из path (или дефолт).
func NewRuntimeConfig(path string) (*RuntimeConfig, error) {
	if path == "" {
		home, _ := os.UserHomeDir()
		if home == "" {
			home = "."
		}
		path = filepath.Join(home, ".bj", "setup.json")
	}
	rc := &RuntimeConfig{path: path}
	if err := rc.load(); err != nil && !errors.Is(err, os.ErrNotExist) {
		return nil, err
	}
	// Гарантируем разумные дефолты.
	if rc.data.Crypto.Provider == "" {
		rc.data.Crypto.Provider = "stub"
	}
	if rc.data.Crypto.SocketPath == "" {
		rc.data.Crypto.SocketPath = "/run/bj/crypto.sock"
	}
	return rc, nil
}

// Snapshot возвращает копию текущих настроек.
func (r *RuntimeConfig) Snapshot() Settings {
	r.mu.RLock()
	defer r.mu.RUnlock()
	out := r.data
	if r.data.LastTest != nil {
		t := *r.data.LastTest
		out.LastTest = &t
	}
	return out
}

// UpdatePostgres сохраняет postgres-настройки.
func (r *RuntimeConfig) UpdatePostgres(s PostgresSettings) error {
	r.mu.Lock()
	r.data.Postgres = s
	r.data.UpdatedAt = time.Now().UTC()
	r.mu.Unlock()
	return r.save()
}

// SaveLicense сохраняет лицензионные настройки.
func (r *RuntimeConfig) SaveLicense(s LicenseSettings) error {
	r.mu.Lock()
	r.data.License = s
	r.data.UpdatedAt = time.Now().UTC()
	r.mu.Unlock()
	return r.save()
}

// SaveUpdateSettings сохраняет настройки авто-обновлений.
func (r *RuntimeConfig) SaveUpdateSettings(s UpdateSettings) error {
	r.mu.Lock()
	r.data.Update = s
	r.data.UpdatedAt = time.Now().UTC()
	r.mu.Unlock()
	return r.save()
}

// UpdateCrypto сохраняет crypto-настройки.
func (r *RuntimeConfig) UpdateCrypto(s CryptoSettings) error {
	r.mu.Lock()
	r.data.Crypto = s
	r.data.UpdatedAt = time.Now().UTC()
	r.mu.Unlock()
	return r.save()
}

// UpdateNSD сохраняет NSD-настройки.
func (r *RuntimeConfig) UpdateNSD(s NSDSettings) error {
	r.mu.Lock()
	r.data.NSD = s
	r.data.UpdatedAt = time.Now().UTC()
	r.mu.Unlock()
	return r.save()
}

// UpdateLK сохраняет LK callback URL.
// UpdateCACerts сохраняет настройки авто-загрузки сертификатов УЦ.
func (r *RuntimeConfig) UpdateCACerts(s CACertsSettings) error {
	r.mu.Lock()
	r.data.CACerts = s
	r.data.UpdatedAt = time.Now()
	r.mu.Unlock()
	return r.save()
}

// UpdateNews заменяет всю ленту новостей.
func (r *RuntimeConfig) UpdateNews(s NewsSettings) error {
	r.mu.Lock()
	r.data.News = s
	r.data.UpdatedAt = time.Now()
	r.mu.Unlock()
	return r.save()
}

// AddNews добавляет новость в начало ленты (newest first). Если в ленте уже
// есть новость с таким же ID — она обновляется (вместо дубликата).
func (r *RuntimeConfig) AddNews(item NewsItem) error {
	r.mu.Lock()
	if item.ID == "" {
		item.ID = item.At.Format("20060102-150405") + "-" + item.Kind
	}
	if item.At.IsZero() {
		item.At = time.Now()
	}
	// Дедуп по ID.
	replaced := false
	for i, ex := range r.data.News.Items {
		if ex.ID == item.ID {
			r.data.News.Items[i] = item
			replaced = true
			break
		}
	}
	if !replaced {
		r.data.News.Items = append([]NewsItem{item}, r.data.News.Items...)
	}
	r.data.UpdatedAt = time.Now()
	r.mu.Unlock()
	return r.save()
}

// DismissNews помечает новость скрытой по ID (не удаляет — для аудита).
func (r *RuntimeConfig) DismissNews(id string) error {
	r.mu.Lock()
	for i := range r.data.News.Items {
		if r.data.News.Items[i].ID == id {
			r.data.News.Items[i].Dismissed = true
		}
	}
	r.data.UpdatedAt = time.Now()
	r.mu.Unlock()
	return r.save()
}

func (r *RuntimeConfig) UpdateLK(s LKSettings) error {
	r.mu.Lock()
	r.data.LK = s
	r.data.UpdatedAt = time.Now().UTC()
	r.mu.Unlock()
	return r.save()
}

// RecordTestRun сохраняет результат теста.
func (r *RuntimeConfig) RecordTestRun(res TestRunResult) error {
	r.mu.Lock()
	r.data.LastTest = &res
	r.data.UpdatedAt = time.Now().UTC()
	r.mu.Unlock()
	return r.save()
}

// load читает JSON в r.data.
func (r *RuntimeConfig) load() error {
	raw, err := os.ReadFile(r.path)
	if err != nil {
		return err
	}
	return json.Unmarshal(raw, &r.data)
}

// save пишет JSON в r.path атомарно через tmp + rename.
func (r *RuntimeConfig) save() error {
	r.mu.RLock()
	raw, err := json.MarshalIndent(r.data, "", "  ")
	r.mu.RUnlock()
	if err != nil {
		return err
	}
	dir := filepath.Dir(r.path)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	tmp := r.path + ".tmp"
	if err := os.WriteFile(tmp, raw, 0o600); err != nil {
		return err
	}
	return os.Rename(tmp, r.path)
}

// Readiness — сводная готовность подсистемы.
type Readiness struct {
	Name       string `json:"name"`
	Ready      bool   `json:"ready"`      // полностью настроена и проверена
	Configured bool   `json:"configured"` // есть пользовательский конфиг (не stub)
	Message    string `json:"message"`
}

// ReadinessSummary возвращает компактную сводку для UI/admin.
func (r *RuntimeConfig) ReadinessSummary() []Readiness {
	s := r.Snapshot()
	out := []Readiness{
		{
			Name:       "postgres",
			Configured: s.Postgres.DSN != "",
			Ready:      false, // настоящий ping будет в checks.go
			Message:    posMsg(s.Postgres.DSN),
		},
		{
			Name:       "crypto-service",
			Configured: s.Crypto.Provider != "" && s.Crypto.Provider != "stub",
			Ready:      false,
			Message:    cryptoMsg(s.Crypto),
		},
		{
			Name:       "nsd-adapter",
			Configured: s.NSD.IGWBaseURL != "" && s.NSD.Profile != "",
			Ready:      false,
			Message:    nsdMsg(s.NSD),
		},
		{
			Name:       "lk-callback",
			Configured: s.LK.CallbackURL != "",
			Ready:      false,
			Message:    lkMsg(s.LK),
		},
	}
	return out
}

func posMsg(dsn string) string {
	if dsn == "" {
		return "DSN не задан — система в режиме in-memory (M2-демо)"
	}
	return "DSN настроен: " + maskDSN(dsn)
}

func cryptoMsg(c CryptoSettings) string {
	if c.Provider == "" || c.Provider == "stub" {
		return "Криптография не настроена (provider=stub) — реальная подпись недоступна."
	}
	if c.ModulePath == "" {
		return "Провайдер " + c.Provider + ", путь к PKCS#11 модулю не задан."
	}
	return "Провайдер " + c.Provider + ", PKCS#11 модуль: " + c.ModulePath
}

func nsdMsg(n NSDSettings) string {
	if n.IGWBaseURL == "" {
		return "ИШ НРД не настроен — используется mock-режим (Decision через 3 сек)"
	}
	if n.Profile == "" {
		return "URL ИШ задан, но профиль не выбран"
	}
	return "Профиль " + n.Profile + ", ИШ " + n.IGWBaseURL
}

func lkMsg(l LKSettings) string {
	if l.CallbackURL == "" {
		return "Callback URL не настроен — используется встроенный lk-emulator"
	}
	return "Callback URL: " + l.CallbackURL
}

// maskDSN скрывает пароль в DSN для отображения в UI.
func maskDSN(dsn string) string {
	// простая маскировка: ищем :///user:pass@host
	const sep = "@"
	if idx := indexAt(dsn, sep); idx > 0 {
		if colon := lastColonBefore(dsn, idx); colon > 0 && colon < idx {
			return dsn[:colon+1] + "***" + dsn[idx:]
		}
	}
	return dsn
}

func indexAt(s, sub string) int {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return i
		}
	}
	return -1
}

func lastColonBefore(s string, idx int) int {
	for i := idx - 1; i >= 0; i-- {
		if s[i] == ':' {
			return i
		}
	}
	return -1
}
