package semanticreconcile

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
)

// PromptSpec identifies the exact prompt selected by a reconciliation owner.
type PromptSpec struct {
	ID      string
	Version string
	SHA256  string
}

// Validate rejects incomplete prompt identity and non-canonical digests.
func (spec PromptSpec) Validate() error {
	if strings.TrimSpace(spec.ID) == "" {
		return fmt.Errorf("semantic reconciliation prompt ID must not be empty")
	}
	if strings.TrimSpace(spec.Version) == "" {
		return fmt.Errorf("semantic reconciliation prompt version must not be empty")
	}
	if err := validateSHA256(spec.SHA256); err != nil {
		return fmt.Errorf("semantic reconciliation prompt digest: %w", err)
	}
	return nil
}

// DefaultPromptSpec returns the identity of the core-owned generic prompt.
func DefaultPromptSpec() (PromptSpec, error) {
	digest, err := PromptHash()
	if err != nil {
		return PromptSpec{}, err
	}
	return PromptSpec{ID: PromptID, Version: PromptVersion, SHA256: digest}, nil
}

// Request contains one typed owner's source-backed reconciliation input.
type Request struct {
	StageName                      string
	Source                         *source.SourceDocument
	Candidates                     []Candidate
	ProfileID                      string
	StructuredOutputRepairAttempts *int
	SessionID                      string
	Correction                     *contracts.SemanticCorrection
}

// ResultDisposition classifies a provider-neutral reconciliation outcome.
type ResultDisposition uint8

const (
	Complete ResultDisposition = iota + 1
	RetryableInvalidStructuredOutput
	RetryableDiscardedProposalGroups
	SkippedInsufficientCandidates
	SkippedLimitExceeded
)

// Result owns the safe plan and neutral diagnostics from one call.
type Result struct {
	disposition          ResultDisposition
	plan                 Plan
	issues               []Issue
	discardedGroupCount  int
	candidateNumberRange CandidateNumberRange
	candidateMappings    []CandidateMapping
	modelCandidate       *contracts.ModelCandidate
}

// Disposition returns the classified outcome.
func (result Result) Disposition() ResultDisposition { return result.disposition }

// Plan returns an independently owned safe plan.
func (result Result) Plan() Plan { return result.planCopy() }

// Issues returns an owned copy of stable proposal issues.
func (result Result) Issues() []Issue { return append([]Issue(nil), result.issues...) }

// DiscardedGroupCount returns the number of excluded proposal groups.
func (result Result) DiscardedGroupCount() int { return result.discardedGroupCount }

// CandidateNumberRange returns the exact request-local range used for this call.
func (result Result) CandidateNumberRange() CandidateNumberRange {
	return result.candidateNumberRange
}

// CandidateMappings returns the request-local number mapping used for this call.
func (result Result) CandidateMappings() []CandidateMapping {
	return append([]CandidateMapping(nil), result.candidateMappings...)
}

// CorrectionDetails returns bounded model-facing descriptions of the result's
// proposal issues using the exact candidate-number range for this call.
func (result Result) CorrectionDetails() ([]string, error) {
	return CorrectionDetails(result.Issues(), result.candidateNumberRange)
}

// CorrectionGuidance returns one bounded replacement request for this result.
func (result Result) CorrectionGuidance(additionalDetails ...string) (string, error) {
	return CorrectionGuidance(result.Issues(), result.candidateNumberRange, additionalDetails...)
}

// ModelCandidate returns an owned copy of the proposal response when a model
// completion produced this result.
func (result Result) ModelCandidate() *contracts.ModelCandidate {
	candidate, err := contracts.CloneModelCandidate(result.modelCandidate)
	if err != nil {
		return nil
	}
	return candidate
}

func (result Result) planCopy() Plan {
	return Plan{groups: result.plan.Groups()}
}

// Engine prepares bounded material, performs one structured completion, and
// classifies the deterministic assessment without applying it to typed values.
type Engine struct {
	client contracts.StructuredLLMClient
	prompt PromptSpec
	schema llm.ResponseSchema
	limits Limits
}

// NewEngine constructs a reconciliation engine using the core response schema.
func NewEngine(client contracts.StructuredLLMClient, prompt PromptSpec, limits Limits) (*Engine, error) {
	if client == nil {
		return nil, fmt.Errorf("construct semantic reconciliation engine: LLM client must not be nil")
	}
	if err := prompt.Validate(); err != nil {
		return nil, fmt.Errorf("construct semantic reconciliation engine: %w", err)
	}
	if err := limits.Validate(); err != nil {
		return nil, fmt.Errorf("construct semantic reconciliation engine: %w", err)
	}
	schema, err := LoadResponseSchema()
	if err != nil {
		return nil, fmt.Errorf("construct semantic reconciliation engine: load response schema: %w", err)
	}
	return newEngine(client, prompt, schema, limits), nil
}

func newEngine(client contracts.StructuredLLMClient, prompt PromptSpec, schema llm.ResponseSchema, limits Limits) *Engine {
	return &Engine{client: client, prompt: prompt, schema: schema, limits: limits}
}

// Reconcile prepares and assesses one request. Retryable semantic outcomes are
// returned as results; provider and transport failures remain errors.
func (engine *Engine) Reconcile(ctx context.Context, request Request) (Result, error) {
	if err := engine.validate(); err != nil {
		return Result{}, err
	}
	if ctx == nil {
		return Result{}, fmt.Errorf("semantic reconciliation %q: context must not be nil", request.StageName)
	}
	if strings.TrimSpace(request.StageName) == "" {
		return Result{}, fmt.Errorf("semantic reconciliation stage name must not be empty")
	}
	if request.Source == nil {
		return Result{}, fmt.Errorf("semantic reconciliation %q: source document must not be nil", request.StageName)
	}
	if err := ctx.Err(); err != nil {
		return Result{}, fmt.Errorf("semantic reconciliation %q: context error before preparation: %w", request.StageName, err)
	}

	preparation, err := Prepare(request.Source, request.Candidates, engine.limits)
	if err != nil {
		return Result{}, fmt.Errorf("semantic reconciliation %q: prepare materials: %w", request.StageName, err)
	}
	result := Result{
		candidateNumberRange: preparation.CandidateNumberRange(),
		candidateMappings:    preparation.CandidateMappings(),
	}
	switch preparation.Disposition() {
	case InsufficientCandidates:
		result.disposition = SkippedInsufficientCandidates
		return result, nil
	case LimitExceeded:
		result.disposition = SkippedLimitExceeded
		return result, nil
	case Ready:
	default:
		return Result{}, fmt.Errorf("semantic reconciliation %q: unknown preparation disposition %d", request.StageName, preparation.Disposition())
	}

	if err := ctx.Err(); err != nil {
		return Result{}, fmt.Errorf("semantic reconciliation %q: context error before completion: %w", request.StageName, err)
	}
	correction, err := contracts.CloneSemanticCorrection(request.Correction)
	if err != nil {
		return Result{}, fmt.Errorf("semantic reconciliation %q: clone correction: %w", request.StageName, err)
	}
	var response ProposalResponse
	completion, err := engine.client.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
		StageName:                      request.StageName,
		PromptID:                       engine.prompt.ID,
		PromptVersion:                  engine.prompt.Version,
		ProfileID:                      request.ProfileID,
		SessionID:                      request.SessionID,
		StructuredOutputRepairAttempts: request.StructuredOutputRepairAttempts,
		Inputs:                         preparation.Materials(),
		Correction:                     correction,
	}, &response)
	if err != nil {
		if errors.Is(err, contracts.ErrInvalidStructuredOutput) {
			result.disposition = RetryableInvalidStructuredOutput
			return result, nil
		}
		return Result{}, fmt.Errorf("semantic reconciliation %q: complete structured output: %w", request.StageName, err)
	}
	candidate, err := contracts.NewModelCandidate(completion.Content, contracts.CorrectionProtocolSingleResponseV1)
	if err != nil {
		return Result{}, fmt.Errorf("semantic reconciliation %q: own model candidate: %w", request.StageName, err)
	}
	result.modelCandidate = candidate

	assessment := preparation.Assess(response)
	result.plan = assessment.Plan()
	result.issues = assessment.Issues()
	result.discardedGroupCount = assessment.DiscardedGroupCount()
	if result.discardedGroupCount > 0 {
		result.disposition = RetryableDiscardedProposalGroups
	} else {
		result.disposition = Complete
	}
	return result, nil
}

func (engine *Engine) validate() error {
	if engine == nil {
		return fmt.Errorf("semantic reconciliation engine must not be nil")
	}
	if engine.client == nil {
		return fmt.Errorf("semantic reconciliation engine: LLM client must not be nil")
	}
	if err := engine.prompt.Validate(); err != nil {
		return fmt.Errorf("semantic reconciliation engine: invalid construction state: %w", err)
	}
	if err := engine.limits.Validate(); err != nil {
		return fmt.Errorf("semantic reconciliation engine: invalid construction state: %w", err)
	}
	if err := validateResponseSchemaIdentity(engine.schema); err != nil {
		return fmt.Errorf("semantic reconciliation engine: invalid construction state: %w", err)
	}
	return nil
}
