package projectmemory

import (
	"context"
	"log/slog"

	"github.com/google/uuid"

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

// EmbedderSelector resolves the configured platform embedder, or a typed error
// when embeddings are disabled. Implemented in app.go (shared with codeindex).
type EmbedderSelector interface {
	Embedder(ctx context.Context) (emb llm.Embedder, endpointID uuid.UUID, model string, dims int, err error)
}

// Service is the projectmemory application service. Scope enforcement (project
// access) is performed by callers (MCP tools / supervisor) before invoking it.
type Service interface {
	Write(ctx context.Context, in WriteInput) (*Entry, error)
	Search(ctx context.Context, q Query) ([]Entry, error)
	List(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error)
	Delete(ctx context.Context, id uuid.UUID) error
	RenderAgentsMD(ctx context.Context, projectID uuid.UUID, wsID *uuid.UUID) (string, error)
}

type service struct {
	repo     Repository
	embedSel EmbedderSelector
	log      *slog.Logger
}

// NewService constructs the projectmemory Service. embedSel may be nil (no
// embeddings → keyword fallback only); log may be nil.
func NewService(repo Repository, embedSel EmbedderSelector, log *slog.Logger) Service {
	if log == nil {
		log = slog.Default()
	}
	return &service{repo: repo, embedSel: embedSel, log: log}
}

func (s *service) Write(ctx context.Context, in WriteInput) (*Entry, error) {
	if !IsValidKind(in.Kind) {
		return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
	}
	if in.Title == "" || in.Body == "" {
		return nil, errs.New(errs.CodeInvalidInput, "title and body required")
	}
	// Best-effort embed of title+body; null embedding when disabled/unavailable.
	var model *string
	var vec []float32
	if emb, _, m, _, err := s.embedder(ctx); err == nil {
		vecs, eerr := emb.Embed(ctx, m, []string{in.Title + "\n\n" + in.Body})
		if eerr != nil {
			s.log.Warn("projectmemory.embed_failed", "err", eerr.Error())
		} else if len(vecs) == 1 {
			vec = vecs[0]
			model = &m
		}
	}
	entry, err := s.repo.Insert(ctx, in, model)
	if err != nil {
		return nil, err
	}
	if vec != nil && model != nil {
		if err := s.repo.SetEmbedding(ctx, entry.ID, vec, *model); err != nil {
			// Non-fatal: the entry exists and is keyword-searchable.
			s.log.Warn("projectmemory.set_embedding_failed", "id", entry.ID, "err", err.Error())
		} else {
			entry.EmbeddingModel = model
		}
	}
	return entry, nil
}

func (s *service) Search(ctx context.Context, q Query) ([]Entry, error) {
	if q.Text == "" {
		return nil, errs.New(errs.CodeInvalidInput, "query text required")
	}
	if q.Kind != "" && !IsValidKind(q.Kind) {
		return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
	}
	// Vector path when an embedder is configured; ILIKE/tags fallback otherwise.
	if emb, _, model, _, err := s.embedder(ctx); err == nil {
		vecs, eerr := emb.Embed(ctx, model, []string{q.Text})
		if eerr == nil && len(vecs) == 1 {
			res, kerr := s.repo.SearchKNN(ctx, q.ProjectID, vecs[0], q.Kind, q.TopK)
			if kerr != nil {
				return nil, kerr
			}
			// If the project has no embedded entries yet, fall back to keyword.
			if len(res) > 0 {
				return res, nil
			}
		} else if eerr != nil {
			s.log.Warn("projectmemory.search_embed_failed", "err", eerr.Error())
		}
	}
	return s.repo.SearchKeyword(ctx, q.ProjectID, q.Kind, q.Text, q.TopK)
}

func (s *service) List(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error) {
	if kind != "" && !IsValidKind(kind) {
		return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
	}
	return s.repo.ListByProject(ctx, projectID, kind)
}

func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
	return s.repo.Delete(ctx, id)
}

func (s *service) RenderAgentsMD(ctx context.Context, projectID uuid.UUID, wsID *uuid.UUID) (string, error) {
	entries, err := s.repo.ListByProject(ctx, projectID, "")
	if err != nil {
		return "", err
	}
	return renderAgentsMD(entries, wsID), nil
}

// embedder returns the selector result, or an error when no selector is set.
func (s *service) embedder(ctx context.Context) (llm.Embedder, uuid.UUID, string, int, error) {
	if s.embedSel == nil {
		return nil, uuid.Nil, "", 0, llm.ErrEmbeddingsUnsupported
	}
	return s.embedSel.Embedder(ctx)
}
