package pipeline

import (
	"context"
	"errors"
	"fmt"

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

// producerAttemptKind records why a producer invocation followed the prior
// one. It is deliberately independent of any artifact family.
type producerAttemptKind string

const (
	producerAttemptInitial          producerAttemptKind = "initial"
	producerAttemptOperationalRetry producerAttemptKind = "operational_error_retry"
	producerAttemptStructuralRetry  producerAttemptKind = "structural_retry"
	producerAttemptModuleRetry      producerAttemptKind = "module_requested_retry"
	producerAttemptSemanticRetry    producerAttemptKind = "semantic_correction"
)

type producerAttemptOutcome string

const (
	producerAttemptAccepted           producerAttemptOutcome = "accepted"
	producerAttemptRejected           producerAttemptOutcome = "rejected"
	producerAttemptIncompleteAccepted producerAttemptOutcome = "incomplete_accepted"
	producerAttemptRetried            producerAttemptOutcome = "retried"
	producerAttemptFailed             producerAttemptOutcome = "failed"
)

type producerTerminalAction string

const (
	producerTerminalAccepted           producerTerminalAction = "accepted"
	producerTerminalRejected           producerTerminalAction = "reject_output"
	producerTerminalIncompleteAccepted producerTerminalAction = "warn_continue"
	producerTerminalFailed             producerTerminalAction = "fail_run"
)

// producerAttemptRequest contains only attempt-local control material. The
// producer reconstructs its ordinary request from its own durable inputs.
type producerAttemptRequest struct {
	Number     int
	Kind       producerAttemptKind
	Correction *contracts.SemanticCorrection
}

// producerRetryDirective asks for another producer invocation while retaining
// the current value as a safe fallback if its shared budget is exhausted.
// Artifact-specific adapters are responsible for validating and populating it.
type producerRetryDirective struct {
	CorrectionGuidance  string
	FallbackDiagnostics []contracts.ProducerDiagnostic
}

func (directive *producerRetryDirective) clone() *producerRetryDirective {
	if directive == nil {
		return nil
	}
	return &producerRetryDirective{
		CorrectionGuidance:  directive.CorrectionGuidance,
		FallbackDiagnostics: contracts.CloneProducerDiagnostics(directive.FallbackDiagnostics),
	}
}

// producerAttemptOutput is intentionally artifact-neutral. Value remains
// opaque to the state machine; Candidate is the attempt-local response that
// may support semantic correction.
type producerAttemptOutput struct {
	Value       any
	Candidate   *contracts.ModelCandidate
	Diagnostics []contracts.ProducerDiagnostic
	Retry       *producerRetryDirective
}

func (output producerAttemptOutput) clone() (producerAttemptOutput, error) {
	candidate, err := contracts.CloneModelCandidate(output.Candidate)
	if err != nil {
		return producerAttemptOutput{}, fmt.Errorf("clone model candidate: %w", err)
	}
	return producerAttemptOutput{
		Value:       output.Value,
		Candidate:   candidate,
		Diagnostics: contracts.CloneProducerDiagnostics(output.Diagnostics),
		Retry:       output.Retry.clone(),
	}, nil
}

type producerAttemptProducer func(context.Context, producerAttemptRequest) (producerAttemptOutput, error)
type producerAttemptValidator func(context.Context, producerAttemptOutput) (validationReport, error)

type producerAttemptConfig struct {
	Retries              int
	Policy               ValidationPolicy
	AllowStructuralRetry bool
}

// producerAttemptProvenance is ordered by producer invocation. It retains the
// settled validation report for later debug and durable-provenance adapters.
type producerAttemptProvenance struct {
	Number     int
	Kind       producerAttemptKind
	Outcome    producerAttemptOutcome
	Validation validationReport
}

func (provenance producerAttemptProvenance) clone() producerAttemptProvenance {
	provenance.Validation = cloneValidationReport(provenance.Validation)
	return provenance
}

// producerAttemptTerminal describes the state-machine decision without
// materializing an artifact or persisting stage-specific diagnostics.
type producerAttemptTerminal struct {
	Action               producerTerminalAction
	Value                any
	Diagnostics          []contracts.ProducerDiagnostic
	Rejection            *contracts.RejectedOutput
	Validation           validationReport
	ValidationIncomplete bool
	Provenance           []producerAttemptProvenance
}

func (terminal producerAttemptTerminal) clone() producerAttemptTerminal {
	terminal.Diagnostics = contracts.CloneProducerDiagnostics(terminal.Diagnostics)
	if terminal.Rejection != nil {
		rejection := *terminal.Rejection
		rejection.Validation = cloneValidationSummaryPtr(rejection.Validation)
		terminal.Rejection = &rejection
	}
	terminal.Validation = cloneValidationReport(terminal.Validation)
	terminal.Provenance = cloneProducerAttemptProvenance(terminal.Provenance)
	return terminal
}

func cloneProducerAttemptProvenance(provenance []producerAttemptProvenance) []producerAttemptProvenance {
	if len(provenance) == 0 {
		return nil
	}
	cloned := make([]producerAttemptProvenance, len(provenance))
	for index, item := range provenance {
		cloned[index] = item.clone()
	}
	return cloned
}

func cloneValidationReport(report validationReport) validationReport {
	return validationReport{records: report.Records()}
}

func runProducerAttempts(ctx context.Context, config producerAttemptConfig, produce producerAttemptProducer, validate producerAttemptValidator) (producerAttemptTerminal, error) {
	if ctx == nil {
		return producerAttemptTerminal{}, errors.New("producer attempt context must not be nil")
	}
	if config.Retries < 0 {
		return producerAttemptTerminal{}, errors.New("producer retries must not be negative")
	}
	if produce == nil {
		return producerAttemptTerminal{}, errors.New("producer attempt closure must not be nil")
	}
	if validate == nil {
		return producerAttemptTerminal{}, errors.New("producer validation closure must not be nil")
	}

	attemptLimit := config.Retries + 1
	provenance := make([]producerAttemptProvenance, 0, attemptLimit)
	kind := producerAttemptInitial
	var correction *contracts.SemanticCorrection

	for number := 1; number <= attemptLimit; number++ {
		if err := ctx.Err(); err != nil {
			return failedProducerAttempt(provenance), err
		}
		requestCorrection, err := contracts.CloneSemanticCorrection(correction)
		if err != nil {
			return failedProducerAttempt(provenance), fmt.Errorf("clone semantic correction: %w", err)
		}
		output, err := produce(ctx, producerAttemptRequest{Number: number, Kind: kind, Correction: requestCorrection})
		if err != nil {
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed})
			if isImmediateProducerFailure(err) {
				return failedProducerAttempt(provenance), err
			}
			if errors.Is(err, contracts.ErrInvalidStructuredOutput) && config.AllowStructuralRetry {
				if number < attemptLimit {
					kind, correction = producerAttemptStructuralRetry, nil
					continue
				}
				return applyStructuralTerminalPolicy(config.Policy, provenance, number, err)
			}
			if number < attemptLimit {
				kind, correction = producerAttemptOperationalRetry, nil
				continue
			}
			return failedProducerAttempt(provenance), fmt.Errorf("producer failed after %d attempt(s): %w", number, err)
		}
		if err := validateProducerAttemptDiagnostics(output); err != nil {
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed})
			return failedProducerAttempt(provenance), err
		}

		output, err = output.clone()
		if err != nil {
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed})
			return failedProducerAttempt(provenance), err
		}
		if output.Retry != nil && number < attemptLimit {
			correction, err = moduleRetryCorrection(output)
			if err != nil {
				provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed})
				return failedProducerAttempt(provenance), err
			}
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptRetried})
			kind = producerAttemptModuleRetry
			continue
		}
		if output.Retry != nil && output.Retry.CorrectionGuidance != "" {
			if _, err := moduleRetryCorrection(output); err != nil {
				provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed})
				return failedProducerAttempt(provenance), err
			}
		}
		if output.Retry != nil {
			output.Diagnostics = append(output.Diagnostics, contracts.CloneProducerDiagnostics(output.Retry.FallbackDiagnostics)...)
		}
		correctionCandidate, err := contracts.CloneModelCandidate(output.Candidate)
		if err != nil {
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed})
			return failedProducerAttempt(provenance), fmt.Errorf("clone correction candidate: %w", err)
		}

		report, err := validate(ctx, output)
		if err != nil {
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed})
			return failedProducerAttempt(provenance), err
		}
		report = cloneValidationReport(report)
		if err := ctx.Err(); err != nil {
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed, Validation: report})
			return failedProducerAttempt(provenance), err
		}

		if rejection := report.FirstRejection(); rejection != nil {
			if correctionCandidate != nil {
				if err := correctionCandidate.Validate(); err != nil {
					provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed, Validation: report})
					return failedProducerAttempt(provenance), fmt.Errorf("validate producer model candidate: %w", err)
				}
			}
			if number < attemptLimit && correctionCandidate != nil && correctionCandidate.Protocol == contracts.CorrectionProtocolSingleResponseV1 {
				correctionRequest, guidanceErr := report.CorrectionRequest()
				if guidanceErr != nil {
					provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed, Validation: report})
					return failedProducerAttempt(provenance), fmt.Errorf("construct semantic correction request: %w", guidanceErr)
				}
				correction, err = contracts.NewSemanticCorrection(correctionCandidate.Response, correctionRequest)
				if err != nil {
					provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptFailed, Validation: report})
					return failedProducerAttempt(provenance), fmt.Errorf("construct semantic correction: %w", err)
				}
				provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptRetried, Validation: report})
				kind = producerAttemptSemanticRetry
				continue
			}
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptRejected, Validation: report})
			return applySemanticTerminalPolicy(config.Policy, provenance, number, output, report, *rejection)
		}

		if incomplete := firstIncompleteValidation(report); incomplete != nil {
			provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptIncompleteAccepted, Validation: report})
			if config.Policy.ValidatorFailure == ValidatorFailureWarnContinue {
				return producerAttemptTerminal{Action: producerTerminalIncompleteAccepted, Value: output.Value, Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, ValidationIncomplete: true, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
			}
			return failedProducerAttempt(provenance), validatorFailureError(*incomplete)
		}

		provenance = append(provenance, producerAttemptProvenance{Number: number, Kind: kind, Outcome: producerAttemptAccepted, Validation: report})
		return producerAttemptTerminal{Action: producerTerminalAccepted, Value: output.Value, Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Validation: report, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
	}

	return failedProducerAttempt(provenance), errors.New("producer attempt budget was not exhausted deterministically")
}

func moduleRetryCorrection(output producerAttemptOutput) (*contracts.SemanticCorrection, error) {
	if output.Retry == nil || output.Retry.CorrectionGuidance == "" {
		return nil, nil
	}
	if output.Candidate == nil {
		return nil, errors.New("module retry correction guidance requires a model candidate")
	}
	if output.Candidate.Protocol != contracts.CorrectionProtocolSingleResponseV1 {
		return nil, fmt.Errorf("module retry correction guidance requires protocol %q", contracts.CorrectionProtocolSingleResponseV1)
	}
	correction, err := contracts.NewSemanticCorrection(output.Candidate.Response, output.Retry.CorrectionGuidance)
	if err != nil {
		return nil, fmt.Errorf("construct module retry semantic correction: %w", err)
	}
	return correction, nil
}

func validateProducerAttemptDiagnostics(output producerAttemptOutput) error {
	if err := contracts.ValidateProducerDiagnostics(output.Diagnostics); err != nil {
		return fmt.Errorf("producer returned invalid diagnostics: %w", err)
	}
	if output.Retry != nil {
		if err := contracts.ValidateProducerDiagnostics(output.Retry.FallbackDiagnostics); err != nil {
			return fmt.Errorf("producer returned invalid retry fallback diagnostics: %w", err)
		}
	}
	return nil
}

func isImmediateProducerFailure(err error) bool {
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return true
	}
	var debugErr *attemptDebugPersistenceError
	return errors.As(err, &debugErr)
}

func applyStructuralTerminalPolicy(policy ValidationPolicy, provenance []producerAttemptProvenance, number int, err error) (producerAttemptTerminal, error) {
	switch policy.ProducerStructuralFailure {
	case ProducerStructuralFailureRejectOutput:
		return producerAttemptTerminal{Action: producerTerminalRejected, Rejection: &contracts.RejectedOutput{ReasonCode: "invalid_structured_output", Message: "producer returned invalid structured output", AttemptCount: number}, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
	case ProducerStructuralFailureFailRun:
		return failedProducerAttempt(provenance), fmt.Errorf("producer returned invalid structured output after %d attempt(s): %w", number, err)
	default:
		return failedProducerAttempt(provenance), fmt.Errorf("unknown producer structural-failure action %q", policy.ProducerStructuralFailure)
	}
}

func applySemanticTerminalPolicy(policy ValidationPolicy, provenance []producerAttemptProvenance, number int, output producerAttemptOutput, report validationReport, rejection validationRecord) (producerAttemptTerminal, error) {
	rejected := contracts.RejectedOutput{ValidatorName: rejection.validatorName, ReasonCode: rejection.reasonCode, Message: rejection.message, AttemptCount: number, DiagnosticArtifactPath: rejection.diagnosticPath}
	switch policy.SemanticRejection {
	case SemanticRejectionRejectOutput:
		return producerAttemptTerminal{Action: producerTerminalRejected, Diagnostics: cloneProducerDiagnostics(output.Diagnostics), Rejection: &rejected, Validation: report, ValidationIncomplete: firstIncompleteValidation(report) != nil, Provenance: cloneProducerAttemptProvenance(provenance)}, nil
	case SemanticRejectionFailRun:
		return failedProducerAttempt(provenance), fmt.Errorf("producer candidate rejected after %d attempt(s): %s", number, rejection.message)
	default:
		return failedProducerAttempt(provenance), fmt.Errorf("unknown semantic-rejection action %q", policy.SemanticRejection)
	}
}

func firstIncompleteValidation(report validationReport) *validationRecord {
	for _, record := range report.records {
		if record.outcome == validationFailed || record.outcome == validationSkipped {
			clone := record.clone()
			return &clone
		}
	}
	return nil
}

func cloneProducerDiagnostics(diagnostics []contracts.ProducerDiagnostic) []contracts.ProducerDiagnostic {
	return contracts.CloneProducerDiagnostics(diagnostics)
}

// incompleteValidationDiagnostics reports every applicable validator that
// could not complete under warn_continue. It uses fixed text so provider
// errors and arbitrary validator prose cannot cross this boundary.
func incompleteValidationDiagnostics(report validationReport) []validationDiagnosticRecord {
	diagnostics := make([]validationDiagnosticRecord, 0)
	for _, record := range report.records {
		if record.outcome != validationFailed && record.outcome != validationSkipped {
			continue
		}
		diagnostics = append(diagnostics, validationDiagnosticRecord{validatorName: record.validatorName, diagnostic: contracts.ProducerDiagnostic{
			Disposition:     contracts.DiagnosticDispositionWarning,
			Category:        contracts.DiagnosticCategoryValidationIncomplete,
			ReasonCode:      "validator_execution_incomplete",
			OccurrenceCount: 1,
			Samples: []contracts.DiagnosticSample{{
				Scope:   record.validatorName,
				Message: "Validator execution did not complete within its configured budget.",
			}},
		}})
	}
	return diagnostics
}

// validationSummary projects a terminal state-machine result into the durable
// bounded representation used by manifests, rejections, and receipts.
func validationSummary(terminal producerAttemptTerminal, stage ModuleStage, stepID, laneID, moduleKey, chunkID string, chunkIndex int) artifacts.ValidationSummary {
	summary := artifacts.ValidationSummary{
		Stage:                string(stage),
		StepID:               stepID,
		LaneID:               laneID,
		ModuleKey:            moduleKey,
		ChunkID:              chunkID,
		ChunkIndex:           chunkIndex,
		ProducerAttemptCount: len(terminal.Provenance),
		TerminalAction:       string(terminal.Action),
	}
	switch {
	case terminal.Action == producerTerminalRejected:
		summary.Status = "rejected"
	case terminal.Action == producerTerminalFailed:
		summary.Status = "incomplete"
	case terminal.ValidationIncomplete:
		summary.Status = "incomplete"
	default:
		summary.Status = "complete"
	}
	seenValidators := make(map[string]struct{})
	seenReasons := make(map[string]struct{})
	seenIncomplete := make(map[string]struct{})
	for _, record := range terminal.Validation.records {
		if record.reasonCode != "" {
			if _, exists := seenReasons[record.reasonCode]; !exists {
				seenReasons[record.reasonCode] = struct{}{}
				summary.ReasonCodes = append(summary.ReasonCodes, record.reasonCode)
			}
		}
		if record.outcome == validationRejected {
			if _, exists := seenValidators[record.validatorName]; !exists {
				seenValidators[record.validatorName] = struct{}{}
				summary.RejectingValidators = append(summary.RejectingValidators, record.validatorName)
			}
		}
		if record.outcome == validationFailed || record.outcome == validationSkipped {
			if _, exists := seenIncomplete[record.validatorName]; !exists {
				seenIncomplete[record.validatorName] = struct{}{}
				summary.IncompleteValidators = append(summary.IncompleteValidators, record.validatorName)
			}
		}
	}
	if terminal.Rejection != nil && terminal.Rejection.ReasonCode != "" {
		if _, exists := seenReasons[terminal.Rejection.ReasonCode]; !exists {
			summary.ReasonCodes = append(summary.ReasonCodes, terminal.Rejection.ReasonCode)
		}
	}
	return summary
}

func failedProducerAttempt(provenance []producerAttemptProvenance) producerAttemptTerminal {
	return producerAttemptTerminal{Action: producerTerminalFailed, Provenance: cloneProducerAttemptProvenance(provenance)}
}
