// Package cryptocli — gRPC-клиент к crypto-service по Unix Domain
// Socket. Сам Go-процесс не выполняет криптографию — всё делает
// Java-сайдкар (services/crypto-service) поверх АПК «Валидата
// Клиент L».
//
// На дев-стендах без поднятого сайдкара (стандартный путь
// /run/bj/crypto.sock не существует) клиент возвращает понятную
// ошибку «провайдер недоступен» и lk-gateway работает в stub-режиме:
// XMLDSig-подписи проходят без проверки (только для демо).
package cryptocli

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

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

// Provider — тип СКЗИ-провайдера (информативный — реальный выбор
// делает crypto-service через переменную BJ_CRYPTO_PROVIDER).
type Provider string

const (
	ProviderStub     Provider = "stub"
	ProviderValidata Provider = "validata"
)

// DefaultModulePath сохранена для обратной совместимости с UI;
// текущий путь интеграции — не PKCS#11-модуль, а UDS-сокет
// crypto-service. Возвращаемое значение информативное.
func DefaultModulePath(p Provider) string {
	if p == ProviderValidata {
		return "/opt/Validata/VDCSP/lib/amd64 (через сайдкар, не PKCS#11)"
	}
	return ""
}

// Config — конфигурация клиента.
type Config struct {
	// SocketPath — путь к UDS-сокету crypto-service.
	// Пустое значение = /run/bj/crypto.sock.
	SocketPath string
	// Provider — желаемый провайдер; информативно (см. выше).
	Provider Provider
	// ModulePath — сохраняется для UI; в gRPC-режиме не используется.
	ModulePath string
	// Timeout — таймаут одной gRPC-операции.
	Timeout time.Duration
}

// Client — gRPC-клиент к crypto-service.
type Client struct {
	cfg  Config
	mu   sync.Mutex
	conn *grpc.ClientConn
	api  cryptopb.CryptoServiceClient
}

// New создаёт клиент. Само соединение поднимается лениво при первом
// вызове.
func New(cfg Config) *Client {
	if cfg.Timeout == 0 {
		cfg.Timeout = 5 * time.Second
	}
	if cfg.SocketPath == "" {
		cfg.SocketPath = "/run/bj/crypto.sock"
	}
	return &Client{cfg: cfg}
}

// Close закрывает gRPC-соединение.
func (c *Client) Close() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.conn != nil {
		err := c.conn.Close()
		c.conn = nil
		c.api = nil
		return err
	}
	return nil
}

// ensureConn устанавливает gRPC-канал к UDS-сокету при первом
// использовании. Используем встроенный в grpc-go резолвер unix:.
func (c *Client) ensureConn() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.api != nil {
		return nil
	}
	target := "unix:" + c.cfg.SocketPath
	conn, err := grpc.NewClient(target,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		return fmt.Errorf("cryptocli: dial %s: %w", c.cfg.SocketPath, err)
	}
	c.conn = conn
	c.api = cryptopb.NewCryptoServiceClient(conn)
	return nil
}

// Health — gRPC Health-вызов. Если сокет недоступен (сайдкар не
// поднят) — вернёт «провайдер недоступен» с явной ошибкой.
func (c *Client) Health(ctx context.Context) (HealthInfo, error) {
	if c.cfg.Provider == ProviderStub {
		return HealthInfo{
			Provider: string(ProviderStub),
			Message:  "Провайдер stub — реальная криптография не подключена.",
		}, nil
	}
	if err := c.ensureConn(); err != nil {
		return HealthInfo{}, err
	}
	cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
	defer cancel()
	resp, err := c.api.Health(cctx, &cryptopb.HealthRequest{})
	if err != nil {
		return HealthInfo{}, fmt.Errorf("cryptocli: Health: %w", err)
	}
	return HealthInfo{
		Provider:   resp.GetProvider(),
		Message:    resp.GetVersion(),
		ModulePath: c.cfg.SocketPath,
	}, nil
}

// Certificate — упрощённое описание сертификата (для совместимости с
// прежним UI). В gRPC-режиме crypto-service возвращает информацию о
// подписанте через VerifyResponse; полный список сертификатов
// (FindCertificates) пока не реализован — для UI возвращаем пустой
// список.
type Certificate struct {
	SlotID        uint
	TokenLabel    string
	Label         string
	SubjectCN     string
	IssuerCN      string
	Serial        string
	NotBefore     time.Time
	NotAfter      time.Time
	INN           string
	DER           []byte
	HasPrivateKey bool
}

// FindCertificates пока возвращает пустой список — список ключей
// управляется самой Валидатой через её собственный справочник (zcs),
// а bj-server о конкретных сертификатах узнаёт по результатам
// Verify/Sign-операций. Эту функцию переопределим позже отдельным
// gRPC-методом ListCertificates если потребуется.
func (c *Client) FindCertificates(_ context.Context) ([]Certificate, error) {
	return nil, nil
}

// Shutdown — отправляет команду «выйти с exit-code 2» сайдкару.
// systemd с Restart=on-failure поднимет его обратно. Возвращает
// ошибку если соединение разорвалось (что нормально и означает что
// сайдкар уже завершается).
func (c *Client) Shutdown(ctx context.Context) error {
	if c.cfg.Provider == ProviderStub {
		return errors.New("provider=stub: некуда отправлять Shutdown")
	}
	if err := c.ensureConn(); err != nil {
		return err
	}
	cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
	defer cancel()
	_, err := c.api.Shutdown(cctx, &cryptopb.ShutdownRequest{})
	// Закрываем соединение, чтобы не держать ссылку на падающий процесс.
	_ = c.Close()
	return err
}

// ActivateResult — результат переключения профиля Валидаты.
type ActivateResult struct {
	OK       bool
	Provider string
	Profile  string
	Message  string
}

// Activate переключает crypto-service на указанный профиль pki1.conf.
// Пустая строка = minimal mode (без профиля).
func (c *Client) Activate(ctx context.Context, profile string) (ActivateResult, error) {
	if c.cfg.Provider == ProviderStub {
		return ActivateResult{
			OK:       false,
			Provider: string(ProviderStub),
			Message:  "Провайдер stub — переключение профиля недоступно.",
		}, nil
	}
	if err := c.ensureConn(); err != nil {
		return ActivateResult{}, err
	}
	cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
	defer cancel()
	resp, err := c.api.Activate(cctx, &cryptopb.ActivateRequest{Profile: profile})
	if err != nil {
		return ActivateResult{}, fmt.Errorf("cryptocli: Activate: %w", err)
	}
	return ActivateResult{
		OK:       resp.GetOk(),
		Provider: resp.GetProvider(),
		Profile:  resp.GetProfile(),
		Message:  resp.GetMessage(),
	}, nil
}

// VerifyXMLDSig — проксирует в crypto-service.VerifyXMLDSig.
// Реализует m2mcore.CryptoVerifier — поэтому возвращает CertInfo,
// заполненный из gRPC-ответа.
func (c *Client) VerifyXMLDSig(ctx context.Context, payload []byte) (m2mcore.CertInfo, error) {
	if c.cfg.Provider == ProviderStub {
		return m2mcore.CertInfo{
			SignerCN: "stub-verifier",
		}, nil
	}
	if err := c.ensureConn(); err != nil {
		return m2mcore.CertInfo{}, err
	}
	cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
	defer cancel()
	resp, err := c.api.VerifyXMLDSig(cctx, &cryptopb.VerifyRequest{
		Payload: payload,
	})
	if err != nil {
		return m2mcore.CertInfo{}, fmt.Errorf("cryptocli: VerifyXMLDSig: %w", err)
	}
	if !resp.GetValid() {
		var msg string
		if errs := resp.GetErrors(); len(errs) > 0 {
			msg = errs[0]
		} else {
			msg = "подпись недействительна"
		}
		return m2mcore.CertInfo{}, errors.New("cryptocli: " + msg)
	}
	return m2mcore.CertInfo{
		SignerCN:  resp.GetSignerCn(),
		SignerINN: resp.GetSignerInn(),
		Serial:    resp.GetSerial(),
		NotBefore: time.Unix(resp.GetNotBefore(), 0),
		NotAfter:  time.Unix(resp.GetNotAfter(), 0),
	}, nil
}

// SignXMLDSig — проксирует в crypto-service.SignXMLDSig. Возвращает
// DER-байты CMS detached signature (готовы к включению в XMLDSig-обёртку
// или к самостоятельной отправке как .p7s).
//
// keyAlias — alias ключа из ПСП Валидаты (пустой = ключ по умолчанию
// активного профиля). profile — имя профиля в pki1.conf, пустой = тот
// что инициализирован.
func (c *Client) SignXMLDSig(ctx context.Context, payload []byte, keyAlias, profile string) ([]byte, error) {
	if c.cfg.Provider == ProviderStub {
		return nil, errors.New("provider=stub: подпись недоступна")
	}
	if err := c.ensureConn(); err != nil {
		return nil, err
	}
	cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
	defer cancel()
	resp, err := c.api.SignXMLDSig(cctx, &cryptopb.SignRequest{
		Payload:  payload,
		KeyAlias: keyAlias,
		Profile:  profile,
	})
	if err != nil {
		return nil, fmt.Errorf("cryptocli: SignXMLDSig: %w", err)
	}
	return resp.GetSignedXml(), nil
}

// HealthInfo — что показывает /admin/setup → СКЗИ.
type HealthInfo struct {
	Provider        string
	ModulePath      string // в gRPC-режиме — UDS-сокет
	CryptokiVersion string // не используется
	ManufacturerID  string // не используется
	LibraryVersion  string // не используется
	Tokens          []TokenInfo
	Message         string
}

// TokenInfo — для совместимости с UI; в gRPC-режиме пустой.
type TokenInfo struct {
	SlotID       uint
	Label        string
	Manufacturer string
	Model        string
	SerialNumber string
	Error        string
}

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