package chat

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
)

// compile-time assertions
var _ EndpointService = (*endpointService)(nil)
var _ llm.EndpointLoader = (*endpointLoader)(nil)

// ===== endpointService =====

type endpointService struct {
	repo     Repository
	crypto   *crypto.Encryptor
	registry *llm.Registry
	auditor  audit.Recorder
	log      *slog.Logger
}

// NewEndpointService constructs an EndpointService.
func NewEndpointService(repo Repository, c *crypto.Encryptor, reg *llm.Registry, a audit.Recorder, log *slog.Logger) EndpointService {
	return &endpointService{repo: repo, crypto: c, registry: reg, auditor: a, log: log}
}

// ListEndpoints returns all LLM endpoints.
func (s *endpointService) ListEndpoints(ctx context.Context) ([]LLMEndpoint, error) {
	return s.repo.ListLLMEndpoints(ctx)
}

// CreateEndpoint encrypts the API key, persists the endpoint, and emits an audit event.
func (s *endpointService) CreateEndpoint(ctx context.Context, userID uuid.UUID, in CreateEndpointInput) (*LLMEndpoint, error) {
	enc, err := s.crypto.Encrypt([]byte(in.APIKey))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "encrypt api key")
	}

	id, err := uuid.NewV7()
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "generate endpoint id")
	}

	ep, err := s.repo.InsertLLMEndpoint(ctx, InsertEndpointParams{
		ID:              id,
		Provider:        in.Provider,
		DisplayName:     in.DisplayName,
		BaseURL:         in.BaseURL,
		APIKeyEncrypted: enc,
		CreatedBy:       userID,
	})
	if err != nil {
		return nil, err
	}

	s.tryAudit(ctx, audit.Entry{
		UserID:     &userID,
		Action:     "llm_endpoint.created",
		TargetType: "llm_endpoint",
		TargetID:   ep.ID.String(),
	})
	return ep, nil
}

// UpdateEndpoint updates mutable fields and invalidates the registry cache.
func (s *endpointService) UpdateEndpoint(ctx context.Context, id uuid.UUID, in UpdateEndpointInput) error {
	// If a new API key is provided, encrypt it first; the repo layer converts
	// in.APIKey (*string) back to []byte, so we pass the encrypted bytes as a
	// string (binary-safe round-trip in Go).
	if in.APIKey != nil {
		enc, err := s.crypto.Encrypt([]byte(*in.APIKey))
		if err != nil {
			return errs.Wrap(err, errs.CodeInternal, "encrypt api key")
		}
		encStr := string(enc)
		in.APIKey = &encStr
	}

	if err := s.repo.UpdateLLMEndpoint(ctx, id, in); err != nil {
		return err
	}

	s.registry.Invalidate(id)

	s.tryAudit(ctx, audit.Entry{
		Action:     "llm_endpoint.updated",
		TargetType: "llm_endpoint",
		TargetID:   id.String(),
	})
	return nil
}

// DeleteEndpoint removes an endpoint and invalidates the registry cache.
func (s *endpointService) DeleteEndpoint(ctx context.Context, id uuid.UUID) error {
	if err := s.repo.DeleteLLMEndpoint(ctx, id); err != nil {
		return err
	}

	s.registry.Invalidate(id)

	s.tryAudit(ctx, audit.Entry{
		Action:     "llm_endpoint.deleted",
		TargetType: "llm_endpoint",
		TargetID:   id.String(),
	})
	return nil
}

// TestEndpoint pings the upstream by streaming a minimal request.
func (s *endpointService) TestEndpoint(ctx context.Context, id uuid.UUID) error {
	client, err := s.registry.GetClient(ctx, id)
	if err != nil {
		return err
	}

	// Find at least one enabled model for this endpoint.
	models, err := s.repo.ListLLMModels(ctx, true)
	if err != nil {
		return err
	}
	var modelID string
	for _, m := range models {
		if m.EndpointID == id {
			modelID = m.ModelID
			break
		}
	}
	if modelID == "" {
		return errs.New(errs.CodeInvalidInput, "no enabled model under this endpoint")
	}

	ch, err := client.Stream(ctx, modelID, llm.Request{
		Messages: []llm.Message{
			{Role: llm.RoleUser, Blocks: []llm.ContentBlock{{Type: "text", Text: "ping"}}},
		},
		MaxOutputTokens: 4,
	})
	if err != nil {
		return err
	}

	for ev := range ch {
		switch ev.Type {
		case llm.EventError:
			return ev.Err
		case llm.EventDone:
			return nil
		}
	}
	return nil
}

// ListModels returns LLM models, optionally filtered to enabled-only.
func (s *endpointService) ListModels(ctx context.Context, onlyEnabled bool) ([]LLMModel, error) {
	return s.repo.ListLLMModels(ctx, onlyEnabled)
}

// CreateModel persists a new model and emits an audit event.
func (s *endpointService) CreateModel(ctx context.Context, in CreateModelInput) (*LLMModel, error) {
	id, err := uuid.NewV7()
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "generate model id")
	}

	m, err := s.repo.InsertLLMModel(ctx, InsertModelParams{
		ID:               id,
		EndpointID:       in.EndpointID,
		ModelID:          in.ModelID,
		DisplayName:      in.DisplayName,
		Capabilities:     in.Capabilities,
		ContextWindow:    in.ContextWindow,
		MaxOutputTokens:  in.MaxOutputTokens,
		PromptPrice:      in.PromptPrice,
		CompletionPrice:  in.CompletionPrice,
		ThinkingPrice:    in.ThinkingPrice,
		IsTitleGenerator: in.IsTitleGenerator,
		Enabled:          in.Enabled,
		SortOrder:        in.SortOrder,
	})
	if err != nil {
		return nil, err
	}

	s.tryAudit(ctx, audit.Entry{
		Action:     "llm_model.created",
		TargetType: "llm_model",
		TargetID:   m.ID.String(),
	})
	return m, nil
}

// UpdateModel updates mutable model fields.
func (s *endpointService) UpdateModel(ctx context.Context, id uuid.UUID, in UpdateModelInput) error {
	if err := s.repo.UpdateLLMModel(ctx, id, in); err != nil {
		return err
	}

	s.tryAudit(ctx, audit.Entry{
		Action:     "llm_model.updated",
		TargetType: "llm_model",
		TargetID:   id.String(),
	})
	return nil
}

// DeleteModel removes a model.
func (s *endpointService) DeleteModel(ctx context.Context, id uuid.UUID) error {
	if err := s.repo.DeleteLLMModel(ctx, id); err != nil {
		return err
	}

	s.tryAudit(ctx, audit.Entry{
		Action:     "llm_model.deleted",
		TargetType: "llm_model",
		TargetID:   id.String(),
	})
	return nil
}

// tryAudit logs a warning on failure but never fails the calling operation.
func (s *endpointService) tryAudit(ctx context.Context, e audit.Entry) {
	if err := s.auditor.Record(ctx, e); err != nil {
		s.log.Warn("audit record failed", "action", e.Action, "err", err)
	}
}

// ===== endpointLoader =====

// endpointLoader implements llm.EndpointLoader. Used by app.go to wire the
// llm.Registry: it pulls a chat.LLMEndpoint via Repository, decrypts the
// API key, and returns an llm.Endpoint suitable for buildClient.
type endpointLoader struct {
	repo   Repository
	crypto *crypto.Encryptor
}

// NewEndpointLoader constructs an llm.EndpointLoader bound to repo + crypto.
func NewEndpointLoader(repo Repository, c *crypto.Encryptor) llm.EndpointLoader {
	return &endpointLoader{repo: repo, crypto: c}
}

func (l *endpointLoader) Load(ctx context.Context, id uuid.UUID) (llm.Endpoint, error) {
	ep, err := l.repo.GetLLMEndpoint(ctx, id)
	if err != nil {
		return llm.Endpoint{}, err
	}
	plain, err := l.crypto.Decrypt(ep.APIKeyEncrypted)
	if err != nil {
		return llm.Endpoint{}, fmt.Errorf("decrypt api key: %w", err)
	}
	return llm.Endpoint{
		ID:       ep.ID,
		Provider: string(ep.Provider),
		BaseURL:  ep.BaseURL,
		APIKey:   string(plain),
	}, nil
}

