// Package cryptocli — Go-клиент к СКЗИ через PKCS#11 (КриптоПро CSP,
// Рутокен ЭЦП 2.0, ViPNet, Валидата). Загружает указанный .so модуль,
// открывает сессию, перечисляет токены, читает сертификаты и
// предоставляет операции Sign/Verify.
//
// На ВМ без установленного СКЗИ модуль не загрузится — клиент
// возвращает понятную ошибку и помечает себя как «провайдер
// недоступен». В этом случае lk-gateway переходит в режим stub:
// XMLDSig-подписи проходят без реальной проверки (только для
// дев-стендов и демо).
package cryptocli

import (
	"context"
	"crypto/sha256"
	"crypto/x509"
	"encoding/asn1"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"sync"
	"time"

	"github.com/miekg/pkcs11"

	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
)

// Provider — тип СКЗИ-провайдера.
type Provider string

// Известные провайдеры.
const (
	ProviderStub      Provider = "stub"
	ProviderCryptoPro Provider = "cryptopro"
	ProviderRutoken   Provider = "rutoken"
	ProviderValidata  Provider = "validata"
	ProviderVipNet    Provider = "vipnet"
)

// DefaultModulePath возвращает дефолтный путь до PKCS#11 .so модуля
// для указанного провайдера. Используется в /admin/setup как placeholder.
func DefaultModulePath(p Provider) string {
	switch p {
	case ProviderCryptoPro:
		return "/opt/cprocsp/lib/amd64/libcppkcs11.so"
	case ProviderRutoken:
		return "/usr/lib64/librtpkcs11ecp.so"
	case ProviderValidata:
		return "/opt/validata/lib/libvalidata-pkcs11.so"
	case ProviderVipNet:
		return "/opt/itcs/lib/libvipnet-pkcs11.so"
	}
	return ""
}

// Config — конфигурация клиента.
type Config struct {
	Provider   Provider
	ModulePath string // путь до PKCS#11 .so модуля (libcppkcs11.so и т.п.)
	PIN        string // PIN для сессии (логин на токен)
	SlotID     uint   // 0 = первый доступный
	Timeout    time.Duration
}

// Client — PKCS#11-клиент к СКЗИ.
type Client struct {
	cfg    Config
	mu     sync.Mutex
	ctx    *pkcs11.Ctx
	opened bool
}

// New создаёт клиент. Сам Initialize() здесь не вызывается — это
// делает Connect или явный Ping (Health-check на admin-странице).
func New(cfg Config) *Client {
	if cfg.Timeout == 0 {
		cfg.Timeout = 5 * time.Second
	}
	return &Client{cfg: cfg}
}

// Health — лёгкая проверка готовности. Шаги:
//  1. Сам файл .so существует?
//  2. Initialize модуля?
//  3. Есть ли хотя бы один доступный слот с токеном?
//  4. Информация о токене (label, manufacturer, serial).
func (c *Client) Health(_ context.Context) (HealthInfo, error) {
	if c.cfg.Provider == "" || c.cfg.Provider == ProviderStub {
		return HealthInfo{Provider: string(ProviderStub),
			Message: "Провайдер stub — реальная криптография не подключена."}, nil
	}
	if c.cfg.ModulePath == "" {
		return HealthInfo{}, errors.New("cryptocli: ModulePath не задан")
	}
	if _, err := os.Stat(c.cfg.ModulePath); err != nil {
		return HealthInfo{}, fmt.Errorf("cryptocli: модуль %s не найден: %w", c.cfg.ModulePath, err)
	}

	c.mu.Lock()
	defer c.mu.Unlock()
	if err := c.ensureInitLocked(); err != nil {
		return HealthInfo{}, err
	}

	info, err := c.ctx.GetInfo()
	if err != nil {
		return HealthInfo{}, fmt.Errorf("cryptocli: GetInfo: %w", err)
	}

	slots, err := c.ctx.GetSlotList(true) // только токены
	if err != nil {
		return HealthInfo{}, fmt.Errorf("cryptocli: GetSlotList: %w", err)
	}
	h := HealthInfo{
		Provider:        string(c.cfg.Provider),
		ModulePath:      c.cfg.ModulePath,
		CryptokiVersion: fmt.Sprintf("%d.%d", info.CryptokiVersion.Major, info.CryptokiVersion.Minor),
		ManufacturerID:  info.ManufacturerID,
		LibraryVersion:  fmt.Sprintf("%d.%d", info.LibraryVersion.Major, info.LibraryVersion.Minor),
	}
	for _, slot := range slots {
		tok, err := c.ctx.GetTokenInfo(slot)
		if err != nil {
			h.Tokens = append(h.Tokens, TokenInfo{SlotID: slot, Error: err.Error()})
			continue
		}
		h.Tokens = append(h.Tokens, TokenInfo{
			SlotID:       slot,
			Label:        tok.Label,
			Manufacturer: tok.ManufacturerID,
			Model:        tok.Model,
			SerialNumber: tok.SerialNumber,
		})
	}
	if len(h.Tokens) == 0 {
		h.Message = "Модуль PKCS#11 загружен, но активных токенов не найдено. Подключите Рутокен или установите ключевой контейнер."
	} else {
		h.Message = fmt.Sprintf("Доступно токенов: %d. Криптография готова к работе.", len(h.Tokens))
	}
	return h, nil
}

// Certificate — DER-сертификат с распарсенными атрибутами для UI.
type Certificate struct {
	SlotID        uint
	TokenLabel    string
	Label         string // CKA_LABEL (объект на токене)
	SubjectCN     string
	IssuerCN      string
	Serial        string
	NotBefore     time.Time
	NotAfter      time.Time
	INN           string // если есть в OID 1.2.643.3.131.1.1
	DER           []byte
	HasPrivateKey bool // найден ли парный приватный ключ на токене
}

// FindCertificates перечисляет сертификаты на всех подключенных
// токенах. Не требует Login для публичных сертификатов; для контейнеров
// CryptoPro/Rutoken достаточно открыть сессию (CKU_USER не выполняется).
func (c *Client) FindCertificates(_ context.Context) ([]Certificate, error) {
	if c.cfg.Provider == "" || c.cfg.Provider == ProviderStub {
		return nil, errors.New("cryptocli: провайдер stub — нет реальных сертификатов")
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if err := c.ensureInitLocked(); err != nil {
		return nil, err
	}

	slots, err := c.ctx.GetSlotList(true)
	if err != nil {
		return nil, fmt.Errorf("cryptocli: GetSlotList: %w", err)
	}

	var out []Certificate
	for _, slot := range slots {
		tokInfo, _ := c.ctx.GetTokenInfo(slot)
		certs, err := c.listSlotCertificates(slot, tokInfo.Label)
		if err != nil {
			// продолжаем — возможно один слот занят, другие доступны
			continue
		}
		out = append(out, certs...)
	}
	return out, nil
}

// listSlotCertificates открывает сессию на слоте, ищет CKO_CERTIFICATE,
// читает DER и парсит x509.
func (c *Client) listSlotCertificates(slot uint, tokenLabel string) ([]Certificate, error) {
	sess, err := c.ctx.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION)
	if err != nil {
		return nil, fmt.Errorf("OpenSession: %w", err)
	}
	defer func() { _ = c.ctx.CloseSession(sess) }()

	template := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
	}
	if err := c.ctx.FindObjectsInit(sess, template); err != nil {
		return nil, fmt.Errorf("FindObjectsInit: %w", err)
	}
	handles, _, err := c.ctx.FindObjects(sess, 32)
	_ = c.ctx.FindObjectsFinal(sess)
	if err != nil {
		return nil, fmt.Errorf("FindObjects: %w", err)
	}

	out := make([]Certificate, 0, len(handles))
	for _, h := range handles {
		attrs, err := c.ctx.GetAttributeValue(sess, h, []*pkcs11.Attribute{
			pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil),
			pkcs11.NewAttribute(pkcs11.CKA_LABEL, nil),
			pkcs11.NewAttribute(pkcs11.CKA_ID, nil),
		})
		if err != nil {
			continue
		}
		cert := Certificate{
			SlotID:     slot,
			TokenLabel: tokenLabel,
		}
		var idAttr []byte
		for _, a := range attrs {
			switch a.Type {
			case pkcs11.CKA_VALUE:
				cert.DER = a.Value
			case pkcs11.CKA_LABEL:
				cert.Label = string(a.Value)
			case pkcs11.CKA_ID:
				idAttr = a.Value
			}
		}
		// Парсим X.509 (ГОСТ-сертификаты тоже парсятся через crypto/x509
		// — Subject/Issuer/Serial/Validity не зависят от алгоритма подписи).
		parsed, err := x509.ParseCertificate(cert.DER)
		if err == nil {
			cert.SubjectCN = parsed.Subject.CommonName
			cert.IssuerCN = parsed.Issuer.CommonName
			cert.Serial = parsed.SerialNumber.Text(16)
			cert.NotBefore = parsed.NotBefore
			cert.NotAfter = parsed.NotAfter
			// ИНН в OID 1.2.643.3.131.1.1 — извлекаем из Subject.
			cert.INN = extractINN(parsed)
		}
		// Проверим есть ли парный приватный ключ.
		if len(idAttr) > 0 {
			cert.HasPrivateKey = c.hasPrivateKey(sess, idAttr)
		}
		out = append(out, cert)
	}
	return out, nil
}

// hasPrivateKey ищет CKO_PRIVATE_KEY с тем же CKA_ID что и сертификат.
func (c *Client) hasPrivateKey(sess pkcs11.SessionHandle, id []byte) bool {
	tmpl := []*pkcs11.Attribute{
		pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
		pkcs11.NewAttribute(pkcs11.CKA_ID, id),
	}
	if err := c.ctx.FindObjectsInit(sess, tmpl); err != nil {
		return false
	}
	defer func() { _ = c.ctx.FindObjectsFinal(sess) }()
	handles, _, err := c.ctx.FindObjects(sess, 1)
	return err == nil && len(handles) > 0
}

// extractINN ищет ИНН в Subject сертификата по OID НРД 1.2.643.3.131.1.1.
func extractINN(c *x509.Certificate) string {
	innOID := asn1.ObjectIdentifier{1, 2, 643, 3, 131, 1, 1}
	for _, name := range c.Subject.Names {
		if name.Type.Equal(innOID) {
			if s, ok := name.Value.(string); ok {
				return s
			}
		}
	}
	return ""
}

// VerifyXMLDSig — заглушка для интерфейса m2mcore.CryptoVerifier.
// Реальная проверка XMLDSig потребует канонизации XML и parsing
// сертификатов; пока возвращает CertInfo с подписанной полезной
// нагрузкой как хеш SHA-256 и заглушку CN. На M3-M4 заменим на
// полноценный verify через PKCS#11 + Apache Santuario-like канонизатор.
func (c *Client) VerifyXMLDSig(ctx context.Context, payload []byte) (m2mcore.CertInfo, error) {
	if _, err := c.Health(ctx); err != nil {
		return m2mcore.CertInfo{}, err
	}
	sum := sha256.Sum256(payload)
	return m2mcore.CertInfo{
		SignerCN:  "stub-verifier",
		SignerINN: "",
		Serial:    hex.EncodeToString(sum[:8]),
		NotBefore: time.Now().Add(-365 * 24 * time.Hour),
		NotAfter:  time.Now().Add(365 * 24 * time.Hour),
	}, nil
}

// Close завершает работу PKCS#11 модуля.
func (c *Client) Close() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.ctx == nil {
		return nil
	}
	_ = c.ctx.Finalize()
	c.ctx.Destroy()
	c.ctx = nil
	c.opened = false
	return nil
}

// ensureInitLocked инициализирует PKCS#11 модуль если ещё не.
// Должен вызываться под c.mu.Lock.
func (c *Client) ensureInitLocked() error {
	if c.opened {
		return nil
	}
	c.ctx = pkcs11.New(c.cfg.ModulePath)
	if c.ctx == nil {
		return fmt.Errorf("cryptocli: не получилось загрузить модуль %s", c.cfg.ModulePath)
	}
	if err := c.ctx.Initialize(); err != nil {
		c.ctx.Destroy()
		c.ctx = nil
		return fmt.Errorf("cryptocli: Initialize: %w", err)
	}
	c.opened = true
	return nil
}

// HealthInfo — что показывает /admin/setup и /admin/status.
type HealthInfo struct {
	Provider        string
	ModulePath      string
	CryptokiVersion string
	ManufacturerID  string
	LibraryVersion  string
	Tokens          []TokenInfo
	Message         string
}

// TokenInfo — описание подключённого токена/контейнера.
type TokenInfo struct {
	SlotID       uint
	Label        string
	Manufacturer string
	Model        string
	SerialNumber string
	Error        string
}

// Ensure Client реализует m2mcore.CryptoVerifier.
var _ m2mcore.CryptoVerifier = (*Client)(nil)
