package codeindex

import (
	"context"
	"encoding/json"
	"errors"
	"time"

	"github.com/google/uuid"

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

// ErrEmbeddingDisabled is returned by Search/EnqueueBuild when no embedding
// endpoint is configured. Callers degrade to keyword discovery (fs/grep) and
// expose this as a typed, non-fatal "feature off" condition.
var ErrEmbeddingDisabled = errors.New("codeindex: no embedding endpoint configured")

// WorkspaceInfo is the minimal workspace data the indexer needs. Provided by a
// locator implemented in app.go over the workspace repository (no import cycle).
type WorkspaceInfo struct {
	ID            uuid.UUID
	MainPath      string
	DefaultBranch string
}

// WorkspaceLocator resolves a workspace's on-disk path and default branch.
type WorkspaceLocator interface {
	LocateWorkspace(ctx context.Context, wsID uuid.UUID) (WorkspaceInfo, error)
	// LocateWorktree resolves a worktree's on-disk path + branch (for branch builds).
	LocateWorktree(ctx context.Context, worktreeID uuid.UUID) (path, branch string, wsID uuid.UUID, err error)
}

// EmbedderSelector resolves the configured platform embedder, or nil + a typed
// error when embeddings are disabled (no endpoint configured / unsupported).
type EmbedderSelector interface {
	// Embedder returns the embedder, the embedding endpoint id, the model id and
	// the platform dims. Returns ErrEmbeddingDisabled when not configured.
	Embedder(ctx context.Context) (emb llm.Embedder, endpointID uuid.UUID, model string, dims int, err error)
}

// Enqueuer is the subset of jobs.Repository the service needs.
type Enqueuer interface {
	Enqueue(ctx context.Context, typ jobs.JobType, payload json.RawMessage, scheduledAt time.Time, maxAttempts int32) (*jobs.Job, error)
}

// Service is the codeindex application service.
type Service interface {
	// EnqueueBuild resolves the build commit (HEAD of the worktree/main) and
	// enqueues a codeindex.build job. Idempotent on (workspace, commit, model):
	// a re-enqueue at the same commit returns the existing run without rebuild.
	EnqueueBuild(ctx context.Context, wsID uuid.UUID, worktreeID *uuid.UUID, commitSHA string) (*Run, error)
	Search(ctx context.Context, wsID uuid.UUID, q SearchQuery) ([]Snippet, error)
	MarkStale(ctx context.Context, wsID uuid.UUID) error
}

type service struct {
	repo     Repository
	locator  WorkspaceLocator
	gitr     git.Runner
	embedSel EmbedderSelector
	jobs     Enqueuer
	maxTopK  int
}

// NewService constructs the codeindex Service. maxTopK caps Search top_k.
func NewService(repo Repository, locator WorkspaceLocator, gitr git.Runner, embedSel EmbedderSelector, jobsRepo Enqueuer, maxTopK int) Service {
	if maxTopK <= 0 {
		maxTopK = 50
	}
	return &service{repo: repo, locator: locator, gitr: gitr, embedSel: embedSel, jobs: jobsRepo, maxTopK: maxTopK}
}

func (s *service) EnqueueBuild(ctx context.Context, wsID uuid.UUID, worktreeID *uuid.UUID, commitSHA string) (*Run, error) {
	emb, endpointID, model, dims, err := s.embedSel.Embedder(ctx)
	if err != nil {
		return nil, err // ErrEmbeddingDisabled or config error
	}
	_ = emb

	branch := ""
	dir := ""
	if worktreeID != nil {
		path, b, locWS, lerr := s.locator.LocateWorktree(ctx, *worktreeID)
		if lerr != nil {
			return nil, lerr
		}
		dir, branch, wsID = path, b, locWS
	} else {
		info, lerr := s.locator.LocateWorkspace(ctx, wsID)
		if lerr != nil {
			return nil, lerr
		}
		dir, branch = info.MainPath, info.DefaultBranch
	}

	// Resolve HEAD commit when not supplied.
	if commitSHA == "" {
		commits, lerr := s.gitr.Log(ctx, dir, 1)
		if lerr != nil {
			return nil, errs.Wrap(lerr, errs.CodeGitCmdFailed, "resolve HEAD")
		}
		if len(commits) == 0 {
			return nil, errs.New(errs.CodeInvalidInput, "empty repository: no commit to index")
		}
		commitSHA = commits[0].Hash
	}

	// Idempotency: an existing run at this (ws, commit, model) is reused unless it
	// previously failed/stale (in which case we re-enqueue against the same row).
	existing, lerr := s.repo.GetRunByCommit(ctx, wsID, commitSHA, model)
	if lerr != nil {
		return nil, lerr
	}
	if existing != nil {
		switch existing.Status {
		case StatusPending, StatusRunning, StatusCompleted:
			return existing, nil
		}
		// failed/stale: fall through and create a fresh run is blocked by UNIQUE;
		// re-enqueue the existing run id instead.
		if err := s.enqueueJob(ctx, existing.ID); err != nil {
			return nil, err
		}
		return existing, nil
	}

	run := &Run{
		ID:                  uuid.New(),
		WorkspaceID:         wsID,
		WorktreeID:          worktreeID,
		Branch:              branch,
		CommitSHA:           commitSHA,
		Status:              StatusPending,
		EmbeddingEndpointID: &endpointID,
		EmbeddingModel:      model,
		Dims:                dims,
	}
	saved, err := s.repo.InsertRun(ctx, run)
	if err != nil {
		return nil, err
	}
	if err := s.enqueueJob(ctx, saved.ID); err != nil {
		return nil, err
	}
	return saved, nil
}

func (s *service) enqueueJob(ctx context.Context, runID uuid.UUID) error {
	payload, _ := json.Marshal(buildPayload{RunID: runID})
	if _, err := s.jobs.Enqueue(ctx, JobTypeBuild, payload, time.Now(), 3); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "enqueue codeindex.build")
	}
	return nil
}

func (s *service) Search(ctx context.Context, wsID uuid.UUID, q SearchQuery) ([]Snippet, error) {
	emb, _, model, _, err := s.embedSel.Embedder(ctx)
	if err != nil {
		return nil, err
	}
	if q.Text == "" {
		return nil, errs.New(errs.CodeInvalidInput, "query text required")
	}
	topK := q.TopK
	if topK <= 0 {
		topK = 10
	}
	if topK > s.maxTopK {
		topK = s.maxTopK
	}
	// Ensure there is a completed run before paying for an embedding call.
	latest, err := s.repo.GetLatestCompletedRun(ctx, wsID)
	if err != nil {
		return nil, err
	}
	if latest == nil {
		return nil, errs.New(errs.CodeNotFound, "no completed code index for workspace; reindex first")
	}
	vecs, err := emb.Embed(ctx, model, []string{q.Text})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "embed query")
	}
	if len(vecs) != 1 {
		return nil, errs.New(errs.CodeInternal, "embed query: unexpected vector count")
	}
	return s.repo.SearchKNN(ctx, wsID, vecs[0], topK, q.PathPrefix, q.Lang)
}

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