package acceptance

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/devrites/devrites/internal/markdowntext"
	"github.com/devrites/devrites/internal/state"
)

// MaxLedgerBytes bounds one ledger read; the artifact budget keeps real
// ledgers far below it.
const MaxLedgerBytes = 8 << 20

// ErrOracleMoved marks a discarded result: the gate's bound oracle changed
// between execution and writeback, so the run must not certify it.
var ErrOracleMoved = errors.New("gate oracle changed during the run")

// GateResult is one executed gate's outcome line for the report.
type GateResult struct {
	ID        string
	Ran       bool
	Passed    bool
	Detail    string // exit/match/timeout/approval diagnostic
	Discarded bool   // result dropped: bound oracle or inputs changed during the run
	WriteErr  error  // evidence writeback failed for a reason other than drift
	Reused    bool   // met gate kept under the selected reuse policy
	Inputs    string // INPUTS content digest at run time, when declared
	Origin    string // complete executed transcript retained when proof is reused
	// Outcome, Started and Ended describe the execution this result judged;
	// they are zero when the gate did not execute.
	Outcome        RunOutcome
	Started, Ended time.Time
}

var (
	uncheckedRE = regexp.MustCompile(`^(\s*)- \[ \] `)
	checkedRE   = regexp.MustCompile(`^(\s*)- \[[xX]\] `)
)

// ReadLedgerFile reads a ledger within the size bound. Caller-resolved path.
func ReadLedgerFile(ledgerPath string) (string, error) {
	info, err := os.Lstat(ledgerPath)
	if err != nil {
		return "", err
	}
	if !info.Mode().IsRegular() {
		return "", fmt.Errorf("gate ledger is not a regular file: %s", ledgerPath)
	}
	if info.Size() > MaxLedgerBytes {
		return "", fmt.Errorf("gate ledger exceeds %d bytes: %s", MaxLedgerBytes, ledgerPath)
	}
	// #nosec G304 -- ledger path resolved inside the operator workspace
	raw, err := os.ReadFile(ledgerPath)
	if err != nil {
		return "", err
	}
	return string(raw), nil
}

// RunOptions controls evidence reuse and short ledger writeback locking.
// TranscriptWork requires a usable executed origin before a met gate can skip.
// The caller holds the gates execution lock through transcript publication.
type RunOptions struct {
	WriteLock      func(func() error) error
	TranscriptWork string
}

// RunGates executes the runnable gates of ledgerPath. reverify also runs met
// gates without INPUTS; met gates with unchanged INPUTS reuse their proof in
// either mode, subject to options.TranscriptWork's executed-origin requirement.
// approved is the vetted command set from test-plan.md; a
// CHECK outside it never executes, and neither does one outside a surface that
// could not be determined — an absent preflight is a refusal, not permission.
// Each pass writes definition-bound evidence; each failure clears the box and
// restores EVIDENCE: pending.
//
// Gates sharing one CHECK and CWD execute once per call; each matches its own
// EXPECT against that shared output. options.WriteLock, when non-nil, wraps every
// ledger writeback so the caller need not hold the feature lock while checks
// execute; writeResult re-verifies the gate's oracle before it writes.
func RunGates(ctx context.Context, root, ledgerPath string, doc *Document, approved []ApprovedCommand, reverify bool, timeout time.Duration, options *RunOptions) []GateResult {
	var writeLock func(func() error) error
	transcriptWork := ""
	if options != nil {
		writeLock, transcriptWork = options.WriteLock, options.TranscriptWork
	}
	var results []GateResult
	executions := map[string]struct {
		ran     bool
		outcome RunOutcome
		span    [2]time.Time
		inputs  map[string]string
	}{}
	// Collect every dependent INPUTS spec before any shared command can run.
	for _, gate := range doc.Gates {
		if gate.Check == "" || gate.Inputs == "" {
			continue
		}
		key := gate.Check + "\x00" + filepath.Join(root, filepath.FromSlash(gate.Cwd))
		execution := executions[key]
		if execution.inputs == nil {
			execution.inputs = map[string]string{}
		}
		execution.inputs[gate.Inputs] = ""
		executions[key] = execution
	}
	for _, gate := range doc.Gates {
		if _, abandoned := doc.Abandoned[gate.ID]; abandoned {
			results = append(results, GateResult{ID: gate.ID, Detail: "abandoned; handoff required"})
			continue
		}
		if gate.Check == "" {
			results = append(results, GateResult{ID: gate.ID, Detail: "manual gate; evidence is judged, not executed"})
			continue
		}
		isApproved := Approved(gate.Check, gate.Cwd, approved)
		if transcriptWork != "" && !isApproved {
			results = append(results, GateResult{ID: gate.ID, Detail: "CHECK/CWD is not an approved test-plan.md preflight row; not executed"})
			continue
		}
		inputs := ""
		if GateState(gate, doc) == StateMet {
			canReuse := gate.Inputs == "" && !reverify
			if gate.Inputs != "" {
				// An unreadable or unmatched spec never matches recorded evidence.
				inputs, _ = InputsDigest(root, gate.Inputs)
				canReuse = inputs != "" && inputs == recordedInputs(gate)
			}
			if canReuse {
				origin := ""
				if transcriptWork != "" {
					origin = passingOrigin(transcriptWork, gate)
				}
				if transcriptWork == "" || origin != "" {
					detail := "already met"
					if gate.Inputs != "" {
						detail = "reused: inputs unchanged"
					}
					results = append(results, GateResult{ID: gate.ID, Passed: true, Reused: gate.Inputs != "" || origin != "", Inputs: inputs, Origin: origin, Detail: detail})
					continue
				}
			}
		}
		if !isApproved {
			results = append(results, GateResult{ID: gate.ID, Detail: "CHECK/CWD is not an approved test-plan.md preflight row; not executed"})
			continue
		}
		expectation, err := CompileExpect(gate.Expect)
		if err != nil {
			results = append(results, GateResult{ID: gate.ID, Detail: err.Error()})
			continue
		}
		cwd := filepath.Join(root, filepath.FromSlash(gate.Cwd))
		key := gate.Check + "\x00" + cwd
		execution := executions[key]
		if !execution.ran {
			for spec := range execution.inputs {
				execution.inputs[spec], _ = InputsDigest(root, spec)
			}
			started := time.Now().UTC()
			execution.outcome = Execute(ctx, gate.Check, cwd, timeout)
			execution.span = [2]time.Time{started, time.Now().UTC()}
			for spec, before := range execution.inputs {
				after, _ := InputsDigest(root, spec)
				if after != before {
					execution.inputs[spec] = ""
				}
			}
			execution.ran = true
			executions[key] = execution
		}
		outcome := execution.outcome
		inputs = execution.inputs[gate.Inputs]
		passed := outcome.Err == nil && outcome.ExitCode == 0 && expectation.Matches(outcome.Output)
		detail := "pass"
		if !passed {
			detail = failDetail(outcome, expectation)
		}
		write := func() error {
			if passed && gate.Inputs != "" {
				// Earlier writebacks may have moved files since execution.
				current, _ := InputsDigest(root, gate.Inputs)
				if inputs == "" || current != inputs {
					passed = false
					detail = "INPUTS changed or could not be read during the run"
				}
			}
			evidence := "pending"
			if passed {
				evidence = withInputs(FormatEvidence(gate, OutputDigest(outcome.Output), outcome.OutputBytes, time.Now().UTC().Format(time.RFC3339)), inputs)
			}
			return writeResult(ledgerPath, gate, evidence, passed)
		}
		var writeErr error
		if writeLock == nil {
			writeErr = write()
		} else {
			writeErr = writeLock(write)
		}
		results = append(results, GateResult{
			ID: gate.ID, Ran: true, Passed: passed,
			Discarded: errors.Is(writeErr, ErrOracleMoved), WriteErr: writeErr,
			Detail: resultDetail(writeErr, detail),
			Inputs: inputs, Outcome: outcome, Started: execution.span[0], Ended: execution.span[1],
		})
	}
	// Later checks may invalidate an earlier execution or reused pass.
	for i, gate := range doc.Gates {
		result := &results[i]
		if !result.Passed || result.Discarded || result.WriteErr != nil || gate.Inputs == "" {
			continue
		}
		validate := func() error {
			current, _ := InputsDigest(root, gate.Inputs)
			if current != "" && current == result.Inputs {
				return nil
			}
			result.Passed, result.Reused, result.Discarded = false, false, true
			result.Detail = "INPUTS changed or could not be read after a later check"
			return writeResult(ledgerPath, gate, "pending", false)
		}
		var err error
		if writeLock == nil {
			err = validate()
		} else {
			err = writeLock(validate)
		}
		if err != nil {
			result.WriteErr = err
			result.Detail = resultDetail(err, result.Detail)
		}
	}
	return results
}

func resultDetail(writeErr error, ok string) string {
	if writeErr == nil {
		return ok
	}
	if errors.Is(writeErr, ErrOracleMoved) {
		return ok + "; evidence discarded: " + writeErr.Error()
	}
	return ok + "; evidence write failed: " + writeErr.Error()
}

func failDetail(outcome RunOutcome, expectation Expectation) string {
	if outcome.Err != nil {
		return outcome.Err.Error()
	}
	if outcome.ExitCode != 0 {
		return fmt.Sprintf("exit %d", outcome.ExitCode)
	}
	return fmt.Sprintf("EXPECT %q did not match output", expectation.Literal)
}

// writeResult re-reads the ledger, verifies the gate's bound oracle is
// unchanged since it ran, then flips the checkbox and writes the evidence
// line atomically. A definition that moved in flight discards the result
// instead of certifying a different oracle.
func writeResult(ledgerPath string, ran *Gate, evidence string, checked bool) error {
	text, err := ReadLedgerFile(ledgerPath)
	if err != nil {
		return err
	}
	doc := ParseLedger(text)
	var current *Gate
	for _, gate := range doc.Gates {
		if gate.ID == ran.ID {
			current = gate
			break
		}
	}
	if current == nil {
		return fmt.Errorf("%w: gate %s disappeared", ErrOracleMoved, ran.ID)
	}
	if current.Check != ran.Check || current.Expect != ran.Expect || current.Cwd != ran.Cwd || current.Inputs != ran.Inputs {
		return fmt.Errorf("%w: gate %s definition edited", ErrOracleMoved, ran.ID)
	}

	lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
	masked, err := markdowntext.Structural([]byte(text))
	if err != nil {
		return err
	}
	maskedLines := strings.Split(strings.ReplaceAll(string(masked), "\r\n", "\n"), "\n")
	row := current.Line - 1
	if checked {
		lines[row] = uncheckedRE.ReplaceAllString(lines[row], "${1}- [x] ")
	} else {
		lines[row] = checkedRE.ReplaceAllString(lines[row], "${1}- [ ] ")
	}

	// Attribute-block boundaries come from the masked text so a fenced
	// EVIDENCE example can never be mistaken for this gate's attribute.
	evidenceLine := "  EVIDENCE: " + evidence
	end := row + 1
	evidenceRow := -1
	for end < len(maskedLines) {
		line := maskedLines[end]
		if attrMatch := attrLineRE.FindStringSubmatch(line); attrMatch != nil {
			if attrMatch[2] == "EVIDENCE" {
				evidenceRow = end
			}
			end++
			continue
		}
		if strings.TrimSpace(line) == "" {
			end++
			continue
		}
		break
	}
	if evidenceRow >= 0 {
		lines[evidenceRow] = evidenceLine
	} else {
		insertAt := row + 1
		for insertAt < len(maskedLines) && attrLineRE.MatchString(maskedLines[insertAt]) {
			insertAt++
		}
		lines = append(lines[:insertAt], append([]string{evidenceLine}, lines[insertAt:]...)...)
	}
	out := strings.Join(lines, "\n")
	if doc.Newline() == "\r\n" {
		out = strings.ReplaceAll(out, "\n", "\r\n")
	}
	return state.AtomicWrite(ledgerPath, []byte(out), 0o644)
}
