package lkgateway

import (
	"context"
	"crypto/sha256"
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// caCertsDir — куда складываются скачанные сертификаты УЦ.
const caCertsDir = "/var/lib/bj/ca-certs"

// defaultNSDCAURLs — список URL для авто-загрузки сертификатов УЦ НРД.
// Эти URL пользователь может скорректировать в /admin/setup → «Сертификаты
// УЦ» (раздел появляется после первого сохранения настроек). На сайте НРД
// (www.nsd.ru/workflow/system/cryptography/) сертификаты выложены в виде
// .cer файлов — нужно скопировать их прямые URL сюда.
//
// По умолчанию список пустой, потому что прямые URL у НРД меняются от
// релиза к релизу и должны быть проверены оператором перед использованием.
var defaultNSDCAURLs = []string{
	// https://www.nsd.ru/workflow/system/cryptography/ — раскомментируйте
	// нужные ссылки в UI после того, как уточните URL у НРД.
}

// FetchCACertificates скачивает все URL из настроек, парсит .cer и
// сохраняет файл в /var/lib/bj/ca-certs/. Если передан rc — на каждое
// фактическое изменение сертификата (новый или изменился SHA-256)
// публикуется новость в ленту через rc.AddNews. На сертификаты,
// истекающие в ближайшие 14 дней — отдельная новость-предупреждение.
func FetchCACertificates(ctx context.Context, s CACertsSettings, rc *RuntimeConfig) (CACertsSettings, string) {
	if len(s.URLs) == 0 {
		return s, "Список URL пуст. Добавьте ссылки на .cer-файлы УЦ НРД в /admin/setup → «Сертификаты УЦ»."
	}
	var logBuf strings.Builder
	now := time.Now()
	newFetched := make([]FetchedCACert, 0, len(s.URLs))

	for _, u := range s.URLs {
		u = strings.TrimSpace(u)
		if u == "" {
			continue
		}
		fc := FetchedCACert{URL: u, FetchedAt: now}
		der, err := downloadAndParseCert(ctx, u)
		if err != nil {
			fc.Error = err.Error()
			newFetched = append(newFetched, fc)
			fmt.Fprintf(&logBuf, "%s — ОШИБКА: %s\n", u, err)
			continue
		}
		cert, perr := x509.ParseCertificate(der)
		if perr != nil {
			fc.Error = "не удалось распарсить X.509: " + perr.Error()
			newFetched = append(newFetched, fc)
			fmt.Fprintf(&logBuf, "%s — не X.509: %s\n", u, perr)
			continue
		}
		fc.SubjectCN = cert.Subject.CommonName
		fc.IssuerCN = cert.Issuer.CommonName
		fc.NotAfter = cert.NotAfter
		fc.SHA256 = hex.EncodeToString(sha256Bytes(der))
		// Корневые (Issuer == Subject) и промежуточные складываем рядом,
		// в общую папку /var/lib/bj/ca-certs/.
		kind := "intermediate"
		if cert.Subject.CommonName == cert.Issuer.CommonName {
			kind = "root"
		}
		fc.Store = kind

		// Дедуп: если sha256 совпадает с уже импортированным — пропускаем
		// сам импорт (но фиксируем что проверили).
		alreadyImported := false
		for _, old := range s.FetchedCerts {
			if old.URL == u && old.SHA256 == fc.SHA256 && old.Error == "" {
				alreadyImported = true
				break
			}
		}
		if alreadyImported {
			fmt.Fprintf(&logBuf, "%s — не изменился (sha256=%s...)\n", u, fc.SHA256[:12])
			newFetched = append(newFetched, fc)
			continue
		}

		// Сохраняем DER на диск в /var/lib/bj/ca-certs/<sha>.cer.
		isNew := true
		for _, old := range s.FetchedCerts {
			if old.URL == u && old.Error == "" {
				isNew = false
				break
			}
		}
		if err := saveCertToDir(der, fc.SHA256); err != nil {
			fc.Error = "save: " + err.Error()
			fmt.Fprintf(&logBuf, "%s — сохранить не удалось: %s\n", u, err)
			if rc != nil {
				_ = rc.AddNews(NewsItem{
					ID:    "ca-error-" + fc.SHA256[:12],
					At:    now,
					Kind:  "system",
					Title: "Не удалось сохранить сертификат УЦ",
					Body:  "URL: " + u + "\nCN: " + fc.SubjectCN + "\nОшибка: " + err.Error(),
					URL:   u,
				})
			}
		} else {
			fmt.Fprintf(&logBuf, "%s — сохранён (%s, CN=%s, sha256=%s...)\n",
				u, kind, fc.SubjectCN, fc.SHA256[:12])
			if rc != nil {
				kindTitle := "Обновлён сертификат УЦ"
				if isNew {
					kindTitle = "Установлен новый сертификат УЦ"
				}
				_ = rc.AddNews(NewsItem{
					ID:    "ca-update-" + fc.SHA256[:12],
					At:    now,
					Kind:  "feature",
					Title: kindTitle + ": " + fc.SubjectCN,
					Body: fmt.Sprintf("Тип: %s\nИздатель: %s\nДействителен до: %s\nSHA-256: %s…\nURL источника: %s",
						kind, fc.IssuerCN, fc.NotAfter.Format("02.01.2006"), fc.SHA256[:16], u),
					URL:     u,
					ValidTo: fc.NotAfter,
				})
				// Предупреждение если истекает в ближайшие 14 дней.
				if !fc.NotAfter.IsZero() && time.Until(fc.NotAfter) < 14*24*time.Hour {
					_ = rc.AddNews(NewsItem{
						ID:    "ca-expiring-" + fc.SHA256[:12],
						At:    now,
						Kind:  "system",
						Title: "⚠ Сертификат УЦ скоро истечёт: " + fc.SubjectCN,
						Body: fmt.Sprintf("Срок действия — %s (через %d дней). Получите новую версию у УЦ и обновите URL в /admin/setup → «Сертификаты УЦ».",
							fc.NotAfter.Format("02.01.2006"),
							int(time.Until(fc.NotAfter)/(24*time.Hour))),
						URL:     u,
						ValidTo: fc.NotAfter,
					})
				}
			}
		}
		newFetched = append(newFetched, fc)
	}

	s.LastFetch = now
	s.LastFetchLog = logBuf.String()
	s.FetchedCerts = newFetched
	return s, logBuf.String()
}

func sha256Bytes(b []byte) []byte {
	h := sha256.Sum256(b)
	return h[:]
}

// downloadAndParseCert качает URL и возвращает DER-байты сертификата.
// Поддерживает PEM (-----BEGIN CERTIFICATE-----) и сырой DER.
func downloadAndParseCert(ctx context.Context, rawURL string) ([]byte, error) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return nil, fmt.Errorf("не URL: %w", err)
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return nil, fmt.Errorf("поддерживаются только http/https, получено %q", u.Scheme)
	}
	reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()
	req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, rawURL, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Safari/537.36")
	// noProxyClient определён в news.go — игнорирует HTTPS_PROXY (zetit).
	resp, err := noProxyClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("сеть: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	data, err := io.ReadAll(io.LimitReader(resp.Body, 5<<20))
	if err != nil {
		return nil, err
	}
	// Пробуем PEM.
	if block, _ := pem.Decode(data); block != nil && block.Type == "CERTIFICATE" {
		return block.Bytes, nil
	}
	// Иначе считаем что DER.
	return data, nil
}

// saveCertToDir сохраняет DER-байты в /var/lib/bj/ca-certs/<sha>.cer.
func saveCertToDir(der []byte, sha256hex string) error {
	if err := os.MkdirAll(caCertsDir, 0o755); err != nil {
		return err
	}
	dst := filepath.Join(caCertsDir, sha256hex+".cer")
	return os.WriteFile(dst, der, 0o644)
}

// StartCACertsAutoUpdater запускает горутину, которая раз в сутки
// перекачивает сертификаты УЦ и переустанавливает изменённые. Возвращает
// функцию остановки. Если AutoUpdate=false — фон не запускается.
func StartCACertsAutoUpdater(rc *RuntimeConfig) func() {
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		// При старте — небольшой запас, чтобы не лезть в сеть в ту же
		// секунду запуска bj-server.
		select {
		case <-ctx.Done():
			return
		case <-time.After(30 * time.Second):
		}
		ticker := time.NewTicker(24 * time.Hour)
		defer ticker.Stop()
		for {
			s := rc.Snapshot().CACerts
			if s.AutoUpdate && len(s.URLs) > 0 {
				updated, _ := FetchCACertificates(ctx, s, rc)
				if err := rc.UpdateCACerts(updated); err != nil {
					log.Printf("ca-certs auto-update: save failed: %v", err)
				} else {
					log.Printf("ca-certs auto-update: %d url'ов проверено", len(s.URLs))
				}
			}
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
			}
		}
	}()
	return cancel
}

// saveCACerts — POST /admin/setup/cacerts.
// Принимает форму с textarea (одна URL на строку) и чекбоксом auto_update.
func (h *setupHandlers) saveCACerts(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method", http.StatusMethodNotAllowed)
		return
	}
	raw := r.FormValue("urls")
	auto := r.FormValue("auto_update") == "on"
	urls := []string{}
	for _, line := range strings.Split(raw, "\n") {
		line = strings.TrimSpace(line)
		if line != "" && !strings.HasPrefix(line, "#") {
			urls = append(urls, line)
		}
	}
	cur := h.rc.Snapshot().CACerts
	cur.URLs = urls
	cur.AutoUpdate = auto
	if err := h.rc.UpdateCACerts(cur); err != nil {
		setupFlash(w, r, "Сертификаты УЦ: не получилось сохранить: "+err.Error())
		return
	}
	setupFlash(w, r, fmt.Sprintf("Сертификаты УЦ: сохранено %d URL'ов, авто-обновление: %v", len(urls), auto))
}

// fetchCACertsNow — POST /admin/setup/cacerts/fetch.
// Ручной триггер «скачать сейчас», вызывает FetchCACertificates сразу.
func (h *setupHandlers) fetchCACertsNow(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method", http.StatusMethodNotAllowed)
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
	defer cancel()
	cur := h.rc.Snapshot().CACerts
	updated, summary := FetchCACertificates(ctx, cur, h.rc)
	if err := h.rc.UpdateCACerts(updated); err != nil {
		setupFlash(w, r, "Сертификаты УЦ: ошибка сохранения: "+err.Error())
		return
	}
	if summary == "" {
		summary = "готово"
	}
	// Обрезаем длинный лог в flash-сообщении.
	if len(summary) > 800 {
		summary = summary[:800] + "…"
	}
	setupFlash(w, r, "Сертификаты УЦ обновлены: "+strings.TrimSpace(summary))
}

// caCertsTemplateString — компактный URL для отображения в UI.
func caCertsTemplateString(s CACertsSettings) string {
	return strings.Join(s.URLs, "\n")
}