// clew-hook is a Claude Code hook binary that enforces Clew Engine policies.
//
// It handles two hook events:
//   - SessionStart: Fetches policies from the Clew API and caches them locally.
//   - PreToolUse:   Evaluates tool calls against cached policies.
//
// Exit codes: 0 = success/allow, 2 = block (deny), 1 = error (fail-open).
//
// Usage (called by Claude Code via hooks config):
//
//	echo '{"tool_name":"Bash","tool_input":{"command":"..."}}' | clew-hook pre-tool-use
//	echo '{"session_id":"..."}' | clew-hook session-start
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// --- Types ---

type Policy struct {
	ID        string  `json:"id"`
	Type      string  `json:"type"`
	Pattern   string  `json:"pattern"`
	Severity  string  `json:"severity"`
	Message   string  `json:"message"`
	PreceptID *string `json:"precept_id"`
}

type HookInput struct {
	ToolName  string          `json:"tool_name"`
	ToolInput json.RawMessage `json:"tool_input"`
	// SessionStart fields
	SessionID     string `json:"session_id"`
	HookEventName string `json:"hook_event_name"`
}

type ToolInputBash struct {
	Command string `json:"command"`
}

type ToolInputWrite struct {
	FilePath  string `json:"file_path"`
	Content   string `json:"content"`
	NewString string `json:"new_string"`
}

type HookOutput struct {
	HookSpecificOutput HookSpecificOutput `json:"hookSpecificOutput"`
}

type HookSpecificOutput struct {
	HookEventName          string `json:"hookEventName"`
	PermissionDecision     string `json:"permissionDecision,omitempty"`
	PermissionDecisionReason string `json:"permissionDecisionReason,omitempty"`
	AdditionalContext      string `json:"additionalContext,omitempty"`
}

// --- Paths ---

func cacheDir() string {
	home, _ := os.UserHomeDir()
	return filepath.Join(home, ".clew")
}

func cacheFile() string {
	return filepath.Join(cacheDir(), "policy-cache.json")
}

// --- Policy Cache ---

func loadPolicies() ([]Policy, error) {
	data, err := os.ReadFile(cacheFile())
	if err != nil {
		return nil, err
	}
	var policies []Policy
	if err := json.Unmarshal(data, &policies); err != nil {
		return nil, err
	}
	return policies, nil
}

func savePolicies(policies []Policy) error {
	if err := os.MkdirAll(cacheDir(), 0o755); err != nil {
		return err
	}
	data, err := json.Marshal(policies)
	if err != nil {
		return err
	}
	return os.WriteFile(cacheFile(), data, 0o644)
}

// --- Glob Matching ---

// matchGlob checks if a path matches a simple glob pattern.
// Supports: exact match, prefix/* (startswith), *.ext (endswith),
// and basic contains via split on *.
func matchGlob(pattern, path string) bool {
	if !strings.Contains(pattern, "*") {
		return path == pattern
	}
	if strings.HasSuffix(pattern, "*") {
		prefix := strings.TrimSuffix(pattern, "*")
		return strings.HasPrefix(path, prefix)
	}
	if strings.HasPrefix(pattern, "*") {
		suffix := strings.TrimPrefix(pattern, "*")
		return strings.HasSuffix(path, suffix)
	}
	// Fallback: split on first * and check contains
	parts := strings.SplitN(pattern, "*", 2)
	return strings.Contains(path, parts[0])
}

// --- PreToolUse ---

func handlePreToolUse(input []byte) int {
	var hook HookInput
	if err := json.Unmarshal(input, &hook); err != nil {
		return 1 // fail-open
	}

	// Only check Bash, Write, Edit
	switch hook.ToolName {
	case "Bash", "Write", "Edit":
		// continue
	default:
		return 0
	}

	policies, err := loadPolicies()
	if err != nil || len(policies) == 0 {
		return 0 // fail-open
	}

	var filePath, content string

	switch hook.ToolName {
	case "Write", "Edit":
		var ti ToolInputWrite
		json.Unmarshal(hook.ToolInput, &ti)
		filePath = ti.FilePath
		content = ti.Content
		if content == "" {
			content = ti.NewString
		}
	case "Bash":
		var ti ToolInputBash
		json.Unmarshal(hook.ToolInput, &ti)
		content = ti.Command
	}

	// Check protected_write and unguarded_path rules
	if filePath != "" {
		for _, p := range policies {
			if p.Severity != "block" {
				continue
			}
			if p.Type != "protected_write" && p.Type != "unguarded_path" {
				continue
			}
			if matchGlob(p.Pattern, filePath) {
				outputDeny("PreToolUse", p.ID, p.Message)
				return 0
			}
		}
	}

	// Check banned_import rules
	if content != "" {
		for _, p := range policies {
			if p.Severity != "block" {
				continue
			}
			if p.Type != "banned_import" {
				continue
			}
			if strings.Contains(content, p.Pattern) {
				outputDeny("PreToolUse", p.ID, p.Message)
				return 0
			}
		}
	}

	return 0
}

func outputDeny(event, ruleID, message string) {
	out := HookOutput{
		HookSpecificOutput: HookSpecificOutput{
			HookEventName:          event,
			PermissionDecision:     "deny",
			PermissionDecisionReason: fmt.Sprintf("Policy violation [%s]: %s", ruleID, message),
		},
	}
	json.NewEncoder(os.Stdout).Encode(out)
}

// --- SessionStart ---

func handleSessionStart(_ []byte) int {
	apiURL := os.Getenv("CLEW_API_URL")
	if apiURL == "" {
		apiURL = "http://localhost:8000/v1"
	}

	client := &http.Client{Timeout: 5 * time.Second}

	req, err := http.NewRequest("GET", apiURL+"/policies/export", nil)
	if err != nil {
		return failOpenSessionStart("API request failed")
	}

	if key := os.Getenv("CLEW_API_KEY"); key != "" {
		req.Header.Set("Authorization", "Bearer "+key)
	}

	resp, err := client.Do(req)
	if err != nil {
		return failOpenSessionStart("API unreachable")
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil || resp.StatusCode != 200 {
		return failOpenSessionStart("API error")
	}

	var policies []Policy
	if err := json.Unmarshal(body, &policies); err != nil {
		return failOpenSessionStart("Invalid API response")
	}

	if err := savePolicies(policies); err != nil {
		return failOpenSessionStart("Cache write failed")
	}

	// Build context summary
	summary := buildPolicySummary(policies)
	out := HookOutput{
		HookSpecificOutput: HookSpecificOutput{
			HookEventName:    "SessionStart",
			AdditionalContext: summary,
		},
	}
	json.NewEncoder(os.Stdout).Encode(out)
	return 0
}

func buildPolicySummary(policies []Policy) string {
	if len(policies) == 0 {
		return "Clew policy cache loaded: no active policies."
	}

	var blockCount, warnCount, auditCount int
	var blockRules []string

	for _, p := range policies {
		switch p.Severity {
		case "block":
			blockCount++
			blockRules = append(blockRules, fmt.Sprintf("- [%s] %s: %s", p.Type, p.Pattern, p.Message))
		case "warn":
			warnCount++
		case "audit":
			auditCount++
		}
	}

	summary := fmt.Sprintf("Clew policy cache loaded: %d policies (%d block, %d warn, %d audit).",
		len(policies), blockCount, warnCount, auditCount)

	if len(blockRules) > 0 {
		summary += " Blocking rules: " + strings.Join(blockRules, " ")
	}

	return summary
}

func failOpenSessionStart(reason string) int {
	_ = savePolicies([]Policy{})
	out := HookOutput{
		HookSpecificOutput: HookSpecificOutput{
			HookEventName:    "SessionStart",
			AdditionalContext: fmt.Sprintf("Clew policy cache: %s, running without policy enforcement.", reason),
		},
	}
	json.NewEncoder(os.Stdout).Encode(out)
	return 0
}

// --- PostToolUse Audit ---

type AuditEntry struct {
	Timestamp string `json:"timestamp"`
	ToolName  string `json:"tool_name"`
	FilePath  string `json:"file_path,omitempty"`
	Decision  string `json:"decision"`
	RuleID    string `json:"rule_id,omitempty"`
}

func auditLogFile() string {
	return filepath.Join(cacheDir(), "audit.jsonl")
}

func handlePostToolUse(input []byte) int {
	var hook HookInput
	if err := json.Unmarshal(input, &hook); err != nil {
		return 0 // fail-open, non-blocking
	}

	entry := AuditEntry{
		Timestamp: time.Now().UTC().Format(time.RFC3339),
		ToolName:  hook.ToolName,
		Decision:  "allow",
	}

	// Extract file path if present
	switch hook.ToolName {
	case "Write", "Edit":
		var ti ToolInputWrite
		json.Unmarshal(hook.ToolInput, &ti)
		entry.FilePath = ti.FilePath
	}

	// Write audit log entry (append, non-blocking)
	if err := os.MkdirAll(cacheDir(), 0o755); err != nil {
		return 0
	}
	f, err := os.OpenFile(auditLogFile(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return 0
	}
	defer f.Close()
	json.NewEncoder(f).Encode(entry)

	return 0
}

// --- Main ---

func main() {
	if len(os.Args) < 2 {
		fmt.Fprintf(os.Stderr, "Usage: clew-hook <session-start|pre-tool-use|post-tool-use>\n")
		os.Exit(1)
	}

	input, err := io.ReadAll(os.Stdin)
	if err != nil {
		os.Exit(1)
	}

	var exitCode int
	switch os.Args[1] {
	case "session-start":
		exitCode = handleSessionStart(input)
	case "pre-tool-use":
		exitCode = handlePreToolUse(input)
	case "post-tool-use":
		exitCode = handlePostToolUse(input)
	default:
		fmt.Fprintf(os.Stderr, "Unknown event: %s\n", os.Args[1])
		exitCode = 1
	}

	os.Exit(exitCode)
}
