package chat

import (
	"context"
	"log/slog"
	"strings"
	"time"

	"github.com/google/uuid"

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

// compile-time assertion
var _ ConversationService = (*conversationService)(nil)

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

// NewConversationService constructs a ConversationService.
func NewConversationService(repo Repository, reg *llm.Registry, a audit.Recorder, log *slog.Logger) ConversationService {
	return &conversationService{repo: repo, registry: reg, auditor: a, log: log}
}

// Create validates the model and optional template, then inserts a new conversation.
func (s *conversationService) Create(ctx context.Context, userID uuid.UUID, in CreateConversationInput) (*Conversation, error) {
	// Validate model exists and is enabled.
	model, err := s.repo.GetLLMModel(ctx, in.ModelID)
	if err != nil {
		return nil, err
	}
	if !model.Enabled {
		return nil, errs.New(errs.CodeChatModelNotFound, "model is disabled")
	}

	// Resolve system prompt: custom override > template content > empty string.
	var systemPrompt string
	if in.SystemPrompt != nil {
		systemPrompt = *in.SystemPrompt
	} else if in.TemplateID != nil {
		tpl, err := s.repo.GetPromptTemplate(ctx, *in.TemplateID)
		if err != nil {
			return nil, err
		}
		// Check user-scoped template ownership.
		if tpl.Scope == TemplateScopeUser && (tpl.OwnerID == nil || *tpl.OwnerID != userID) {
			return nil, errs.New(errs.CodeForbidden, "template not accessible")
		}
		systemPrompt = tpl.Content
	}

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

	c, err := s.repo.InsertConversation(ctx, InsertConversationParams{
		ID:            id,
		UserID:        userID,
		ProjectID:     in.ProjectID,
		WorkspaceID:   in.WorkspaceID,
		IssueID:       in.IssueID,
		RequirementID: in.RequirementID,
		ModelID:       in.ModelID,
		SystemPrompt:  systemPrompt,
		Title:         nil,
	})
	if err != nil {
		return nil, err
	}

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

// Get returns a conversation and its most recent messages, enforcing ownership.
// limit caps the number of returned messages (defaults to 50 when non-positive).
func (s *conversationService) Get(ctx context.Context, userID, id uuid.UUID, limit int) (*Conversation, []Message, error) {
	c, err := s.repo.GetConversation(ctx, id)
	if err != nil {
		return nil, nil, err
	}
	if c.UserID != userID {
		return nil, nil, errs.New(errs.CodeForbidden, "not conversation owner")
	}
	if limit <= 0 {
		limit = 50
	}
	msgs, err := s.repo.ListMessagesByConversation(ctx, id, nil, limit)
	if err != nil {
		return nil, nil, err
	}
	// Populate each message's attachments for the user-facing read path.
	for i := range msgs {
		atts, err := s.repo.ListAttachmentsForMessage(ctx, msgs[i].ID)
		if err != nil {
			return nil, nil, err
		}
		msgs[i].Attachments = atts
	}
	return c, msgs, nil
}

// List returns conversations for the given user, defaulting limit to 50.
func (s *conversationService) List(ctx context.Context, userID uuid.UUID, f ConversationFilter) ([]Conversation, error) {
	if f.Limit == 0 {
		f.Limit = 50
	}
	return s.repo.ListConversationsByUser(ctx, userID, f)
}

// UpdateTitle manually sets a conversation title, enforcing ownership.
func (s *conversationService) UpdateTitle(ctx context.Context, userID, id uuid.UUID, title string) error {
	c, err := s.repo.GetConversation(ctx, id)
	if err != nil {
		return err
	}
	if c.UserID != userID {
		return errs.New(errs.CodeForbidden, "not conversation owner")
	}
	return s.repo.SetConversationTitleManual(ctx, id, &title)
}

// SoftDelete marks a conversation as deleted, enforcing ownership.
func (s *conversationService) SoftDelete(ctx context.Context, userID, id uuid.UUID) error {
	c, err := s.repo.GetConversation(ctx, id)
	if err != nil {
		return err
	}
	if c.UserID != userID {
		return errs.New(errs.CodeForbidden, "not conversation owner")
	}
	if err := s.repo.SoftDeleteConversation(ctx, id); err != nil {
		return err
	}
	s.tryAudit(ctx, audit.Entry{
		UserID:     &userID,
		Action:     "chat.conversation.deleted",
		TargetType: "conversation",
		TargetID:   id.String(),
	})
	return nil
}

// GenerateTitle is NOT on the ConversationService interface.
// It is called by the streamer (T14) via type assertion on the concrete type.
// It runs in a fire-and-forget goroutine; errors are silently swallowed.
func (s *conversationService) GenerateTitle(ctx context.Context, conversationID uuid.UUID) {
	tCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	titleModel, err := s.repo.GetTitleGeneratorModel(tCtx)
	if err != nil {
		s.log.Warn("GenerateTitle: no title model", "err", err)
		return
	}

	first, err := s.repo.GetFirstUserMessage(tCtx, conversationID)
	if err != nil {
		s.log.Warn("GenerateTitle: no first user message", "conversation_id", conversationID, "err", err)
		return
	}

	client, err := s.registry.GetClient(tCtx, titleModel.EndpointID)
	if err != nil {
		s.log.Warn("GenerateTitle: get client failed", "endpoint_id", titleModel.EndpointID, "err", err)
		return
	}

	req := llm.Request{
		SystemPrompt: "用 ≤20 字概括下面对话主题，仅输出标题：",
		Messages: []llm.Message{{
			Role:   llm.RoleUser,
			Blocks: []llm.ContentBlock{{Type: "text", Text: first.Content}},
		}},
		MaxOutputTokens: 64,
	}

	ch, err := client.Stream(tCtx, titleModel.ModelID, req)
	if err != nil {
		s.log.Warn("GenerateTitle: stream failed", "err", err)
		return
	}

	var title string
	for ev := range ch {
		if ev.Type == llm.EventTextDelta {
			title += ev.Text
		}
	}
	title = strings.TrimSpace(title)
	if title == "" {
		return
	}

	if err := s.repo.UpdateConversationTitle(tCtx, conversationID, &title); err != nil {
		s.log.Warn("GenerateTitle: update title failed", "conversation_id", conversationID, "err", err)
	}
}

func (s *conversationService) 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)
	}
}
