package hooks

import (
	"encoding/json"
	"fmt"
	"strings"
)

// PostTaskInput is the JSON structure Claude Code sends to Stop hooks.
// stop_hook_active is true when the hook is already being rerun (another
// Stop hook asked the agent to continue) — we must skip in that case to
// avoid recursion.
type PostTaskInput struct {
	HookEventName  string `json:"hook_event_name"`
	SessionID      string `json:"session_id"`
	TranscriptPath string `json:"transcript_path"`
	CWD            string `json:"cwd"`
	StopHookActive bool   `json:"stop_hook_active"`
}

// runPostTask handles a Stop hook invocation with the raw stdin bytes.
// It runs diagnostics on any changed symbols (dead-code, test targets,
// guards, contract violations) and injects the findings as
// additionalContext so the agent self-corrects before handing off.
//
// Degrades silently when: the bridge is unreachable, there are no
// changes, or stop_hook_active is true.
func runPostTask(data []byte, port int) {
	var input PostTaskInput
	if err := json.Unmarshal(data, &input); err != nil {
		return
	}
	if input.HookEventName != "Stop" {
		return
	}
	// Prevent recursion — if we're already rerunning a Stop hook, don't fire again.
	if input.StopHookActive {
		return
	}

	briefing := buildPostTaskBriefing(port)
	if briefing == "" {
		return
	}

	output := HookOutput{
		HookSpecificOutput: &HookSpecificOutput{
			HookEventName:     "Stop",
			AdditionalContext: briefing,
		},
	}
	out, err := json.Marshal(output)
	if err != nil {
		return
	}
	fmt.Print(string(out))
}

// buildPostTaskBriefing runs diagnostics on the current working tree and
// returns a compact markdown summary. Returns empty string when there's
// nothing to report or the bridge is unreachable.
func buildPostTaskBriefing(port int) string {
	raw := callServerTool(port, "detect_changes", map[string]any{
		"scope": "unstaged",
	})
	if raw == "" {
		return ""
	}

	var changes struct {
		ChangedFiles   []string `json:"changed_files"`
		ChangedSymbols []struct {
			ID   string `json:"id"`
			Name string `json:"name"`
			Kind string `json:"kind"`
		} `json:"changed_symbols"`
		Risk    string `json:"risk"`
		Summary string `json:"summary"`
	}
	if err := json.Unmarshal([]byte(raw), &changes); err != nil {
		return ""
	}

	if len(changes.ChangedSymbols) == 0 {
		// No indexed symbols touched — skip silently. The agent doesn't need
		// a post-task briefing when nothing meaningful changed in the graph.
		return ""
	}

	ids := make([]string, len(changes.ChangedSymbols))
	for i, cs := range changes.ChangedSymbols {
		ids[i] = cs.ID
	}
	idsCSV := strings.Join(ids, ",")

	var sb strings.Builder
	sb.WriteString("## Gortex Post-Task Diagnostics\n\n")
	fmt.Fprintf(&sb, "**Changed:** %d symbols across %d files — risk `%s`.\n\n",
		len(changes.ChangedSymbols), len(changes.ChangedFiles), changes.Risk)

	// Test targets — what to run.
	if tests := renderTestTargets(port, idsCSV); tests != "" {
		sb.WriteString("### Tests to Run\n\n")
		sb.WriteString(tests)
		sb.WriteString("\n")
	}

	// Guard rule violations.
	if guards := renderGuardViolations(port, idsCSV); guards != "" {
		sb.WriteString("### Guard Violations\n\n")
		sb.WriteString(guards)
		sb.WriteString("\n")
	}

	// Dead code — specifically whether any of the changed symbols are now orphaned.
	if dead := renderDeadCodeHits(port, ids); dead != "" {
		sb.WriteString("### Potential Dead Code (among changed symbols)\n\n")
		sb.WriteString(dead)
		sb.WriteString("\n")
	}

	// Coverage gaps among changed symbols. Silent when the graph carries
	// no coverage_pct meta (the analyzer returns "no coverage gaps
	// matched" or just an empty body) — we don't want to nag when the
	// repo simply doesn't run `gortex enrich coverage`.
	if gaps := renderCoverageGapsHits(port, ids); gaps != "" {
		sb.WriteString("### Coverage Gaps (among changed symbols)\n\n")
		sb.WriteString(gaps)
		sb.WriteString("\n")
	}

	// Stale-flag regression check. Silent unless one of the changed
	// symbols is itself a flag site or toggles one — pointing the agent
	// at flag rollouts they may have just touched.
	if flags := renderStaleFlagHits(port, ids); flags != "" {
		sb.WriteString("### Stale Flag Sites Touched\n\n")
		sb.WriteString(flags)
		sb.WriteString("\n")
	}

	// Contract mismatches.
	if contracts := renderContractMismatches(port); contracts != "" {
		sb.WriteString("### API Contract Issues\n\n")
		sb.WriteString(contracts)
		sb.WriteString("\n")
	}

	sb.WriteString("_Run the tests above and review any flagged items before handoff._\n")
	return sb.String()
}

// renderTestTargets asks the bridge for test files that exercise the changed symbols.
func renderTestTargets(port int, idsCSV string) string {
	raw := callServerTool(port, "get_test_targets", map[string]any{
		"ids":     idsCSV,
		"compact": true,
	})
	raw = strings.TrimSpace(raw)
	if raw == "" || raw == "no test targets found" {
		return ""
	}
	return cappedLines(raw, 15)
}

// renderGuardViolations asks the bridge for .gortex.yaml guard rule violations.
func renderGuardViolations(port int, idsCSV string) string {
	raw := callServerTool(port, "check_guards", map[string]any{
		"ids":     idsCSV,
		"compact": true,
	})
	raw = strings.TrimSpace(raw)
	if raw == "" || strings.HasPrefix(raw, "no guard rule violations") {
		return ""
	}
	return cappedLines(raw, 10)
}

// renderDeadCodeHits filters analyze:dead_code results to the intersection
// with the currently-changed symbols (i.e. "did this task leave anything
// orphaned"). Emits nothing when the intersection is empty.
func renderDeadCodeHits(port int, ids []string) string {
	raw := callServerTool(port, "analyze", map[string]any{
		"kind":    "dead_code",
		"compact": true,
	})
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}

	idSet := make(map[string]bool, len(ids))
	for _, id := range ids {
		idSet[id] = true
	}

	var hits []string
	for _, line := range strings.Split(raw, "\n") {
		if line == "" {
			continue
		}
		// Each compact line starts with the symbol descriptor including the
		// file path — we substring-match any of the changed IDs.
		for id := range idSet {
			if strings.Contains(line, id) {
				hits = append(hits, line)
				break
			}
		}
	}
	if len(hits) == 0 {
		return ""
	}
	if len(hits) > 8 {
		hits = hits[:8]
	}
	return strings.Join(hits, "\n") + "\n"
}

// renderCoverageGapsHits filters analyze:coverage_gaps results to
// the intersection with currently-changed symbols. Silent when no
// changed symbol carries coverage_pct (i.e. coverage hasn't been
// hydrated for this graph) or none of the changed symbols are
// undertested. The agent gets a nudge precisely when a coverage
// gap was just touched — not a noisy package-wide rollup.
func renderCoverageGapsHits(port int, ids []string) string {
	raw := callServerTool(port, "analyze", map[string]any{
		"kind":    "coverage_gaps",
		"compact": true,
	})
	raw = strings.TrimSpace(raw)
	if raw == "" || strings.HasPrefix(raw, "no coverage gaps") {
		return ""
	}

	idSet := make(map[string]bool, len(ids))
	for _, id := range ids {
		idSet[id] = true
	}
	var hits []string
	for _, line := range strings.Split(raw, "\n") {
		if line == "" {
			continue
		}
		for id := range idSet {
			if strings.Contains(line, id) {
				hits = append(hits, line)
				break
			}
		}
	}
	if len(hits) == 0 {
		return ""
	}
	if len(hits) > 8 {
		hits = hits[:8]
	}
	return strings.Join(hits, "\n") + "\n"
}

// renderStaleFlagHits filters analyze:stale_flags results to the
// intersection with currently-changed symbols. Silent unless one
// of the changed symbols is a flag site (its id appears in the
// stale_flags output). Useful for catching a refactor that
// inadvertently touched a flag rollout that's been quiet for ages.
func renderStaleFlagHits(port int, ids []string) string {
	raw := callServerTool(port, "analyze", map[string]any{
		"kind":    "stale_flags",
		"compact": true,
	})
	raw = strings.TrimSpace(raw)
	if raw == "" || strings.HasPrefix(raw, "no stale flags") {
		return ""
	}

	idSet := make(map[string]bool, len(ids))
	for _, id := range ids {
		idSet[id] = true
	}
	var hits []string
	for _, line := range strings.Split(raw, "\n") {
		if line == "" {
			continue
		}
		for id := range idSet {
			if strings.Contains(line, id) {
				hits = append(hits, line)
				break
			}
		}
	}
	if len(hits) == 0 {
		return ""
	}
	if len(hits) > 8 {
		hits = hits[:8]
	}
	return strings.Join(hits, "\n") + "\n"
}

// renderContractMismatches runs the contracts check and returns a short list
// of orphan providers/consumers. Empty when all contracts are matched.
func renderContractMismatches(port int) string {
	raw := callServerTool(port, "contracts", map[string]any{
		"action":  "check",
		"compact": true,
	})
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	// The compact summary prefixes "no contract issues" when clean.
	if strings.HasPrefix(strings.ToLower(raw), "no contract") {
		return ""
	}
	return cappedLines(raw, 10)
}
