use anyhow::{Context, Result};
use std::collections::HashSet;
use std::path::Path;

use crate::hooks::socket;

const MANAGED_HOOKS: [(&str, &str); 3] = [
    ("Stop", "cwt-stop.sh"),
    ("Notification", "cwt-notification.sh"),
    ("SubagentStop", "cwt-subagent_stop.sh"),
];

fn managed_hook_paths(hooks_dir: &Path) -> Vec<(String, String)> {
    MANAGED_HOOKS
        .iter()
        .map(|(event, script)| {
            (
                (*event).to_string(),
                hooks_dir.join(script).to_string_lossy().to_string(),
            )
        })
        .collect()
}

fn managed_hook_path_set(hooks_dir: &Path) -> HashSet<String> {
    managed_hook_paths(hooks_dir)
        .into_iter()
        .map(|(_, path)| path)
        .collect()
}

/// Install cwt hook scripts and patch .claude/settings.json.
pub fn install_hooks(repo_root: &Path) -> Result<()> {
    let hooks_dir = repo_root.join(".cwt/hooks");
    std::fs::create_dir_all(&hooks_dir)
        .with_context(|| format!("failed to create {}", hooks_dir.display()))?;

    let sock_path = socket::socket_path(repo_root);
    let sock_str = sock_path.to_string_lossy();

    // Write hook scripts for each event type
    write_hook_script(&hooks_dir, "stop", "session_stopped", &sock_str)?;
    write_hook_script(
        &hooks_dir,
        "notification",
        "session_notification",
        &sock_str,
    )?;
    write_hook_script(&hooks_dir, "subagent_stop", "subagent_stopped", &sock_str)?;

    // Patch .claude/settings.json to register the hooks
    patch_claude_settings(repo_root, &hooks_dir)?;

    println!("Installed cwt hooks to {}", hooks_dir.display());
    println!("Socket path: {}", sock_str);
    Ok(())
}

/// Uninstall cwt hook scripts and clean up .claude/settings.json entries.
pub fn uninstall_hooks(repo_root: &Path) -> Result<()> {
    let hooks_dir = repo_root.join(".cwt/hooks");

    // Remove hook scripts
    if hooks_dir.exists() {
        for (_, script_name) in MANAGED_HOOKS {
            let script_path = hooks_dir.join(script_name);
            if script_path.exists() {
                std::fs::remove_file(&script_path)
                    .with_context(|| format!("failed to remove {}", script_path.display()))?;
            }
        }
    }

    // Remove hooks from .claude/settings.json
    unpatch_claude_settings(repo_root)?;

    println!("Uninstalled cwt hooks");
    Ok(())
}

/// Write a single hook script that reads JSON from stdin, extracts the worktree
/// name, and writes a cwt event to the Unix socket.
fn write_hook_script(
    hooks_dir: &Path,
    claude_event_name: &str,
    cwt_event_name: &str,
    socket_path: &str,
) -> Result<()> {
    let script_name = format!("cwt-{}.sh", claude_event_name);
    let script_path = hooks_dir.join(&script_name);

    // The script reads Claude Code's JSON payload from stdin,
    // extracts relevant fields, and writes a cwt-formatted event
    // to the Unix socket.
    let script = format!(
        r#"#!/bin/sh
# cwt hook: {claude_event} -> {cwt_event}
# Auto-generated by `cwt hooks install`. Do not edit.
# Reads Claude Code hook JSON from stdin, writes cwt event to socket.

SOCKET="{socket}"

# Only proceed if the socket exists (cwt TUI is running)
[ -S "$SOCKET" ] || exit 0

# Read the full stdin payload
PAYLOAD=$(cat)

# Extract worktree name from the payload or from the working directory
WORKTREE=$(echo "$PAYLOAD" | grep -o '"worktree"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"worktree"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')

# If no worktree field, try to derive from cwd
if [ -z "$WORKTREE" ]; then
    # Check if we're in a cwt worktree directory
    CWD=$(pwd)
    case "$CWD" in
        */.claude/worktrees/*)
            WORKTREE=$(echo "$CWD" | sed 's|.*/.claude/worktrees/||' | cut -d'/' -f1)
            ;;
    esac
fi

# If we still have no worktree name, skip
[ -z "$WORKTREE" ] && exit 0

# Extract optional fields
SESSION_ID=$(echo "$PAYLOAD" | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# Build the cwt event JSON
EVENT='{{"event":"{cwt_event}","worktree":"'"$WORKTREE"'","session_id":"'"$SESSION_ID"'","timestamp":"'"$TIMESTAMP"'"}}'

# Write to socket (best-effort, non-blocking)
printf '%s\n' "$EVENT" | nc -U -w1 "$SOCKET" 2>/dev/null || true
"#,
        claude_event = claude_event_name,
        cwt_event = cwt_event_name,
        socket = socket_path,
    );

    std::fs::write(&script_path, script)
        .with_context(|| format!("failed to write {}", script_path.display()))?;

    // Make executable
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let perms = std::fs::Permissions::from_mode(0o755);
        std::fs::set_permissions(&script_path, perms)
            .with_context(|| format!("failed to chmod {}", script_path.display()))?;
    }

    Ok(())
}

/// Patch .claude/settings.json to register cwt hooks.
/// Merges with existing hooks — does not overwrite.
fn patch_claude_settings(repo_root: &Path, hooks_dir: &Path) -> Result<()> {
    let settings_path = repo_root.join(".claude/settings.json");

    // Load existing settings or create new
    let mut settings: serde_json::Value = if settings_path.exists() {
        let content = std::fs::read_to_string(&settings_path)
            .with_context(|| format!("failed to read {}", settings_path.display()))?;
        serde_json::from_str(&content)
            .with_context(|| format!("failed to parse {}", settings_path.display()))?
    } else {
        // Ensure .claude directory exists
        if let Some(parent) = settings_path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        serde_json::json!({})
    };

    let hooks_obj = settings
        .as_object_mut()
        .context("settings.json root is not an object")?
        .entry("hooks")
        .or_insert_with(|| serde_json::json!({}));

    let hooks_map = hooks_obj
        .as_object_mut()
        .context("hooks field is not an object")?;

    for (event_name, script_str) in managed_hook_paths(hooks_dir) {
        let event_hooks = hooks_map
            .entry(event_name)
            .or_insert_with(|| serde_json::json!([]));

        if let Some(arr) = event_hooks.as_array_mut() {
            // Don't add duplicate entries for this exact script path on this event.
            let already_registered = arr.iter().any(|v| {
                v.as_object()
                    .and_then(|obj| obj.get("command"))
                    .and_then(|c| c.as_str())
                    .is_some_and(|c| c == script_str)
            });

            if !already_registered {
                arr.push(serde_json::json!({
                    "type": "command",
                    "command": script_str,
                }));
            }
        }
    }

    let content =
        serde_json::to_string_pretty(&settings).context("failed to serialize settings.json")?;
    std::fs::write(&settings_path, content)
        .with_context(|| format!("failed to write {}", settings_path.display()))?;

    Ok(())
}

/// Remove cwt hook entries from .claude/settings.json.
fn unpatch_claude_settings(repo_root: &Path) -> Result<()> {
    let settings_path = repo_root.join(".claude/settings.json");

    if !settings_path.exists() {
        return Ok(());
    }

    let content = std::fs::read_to_string(&settings_path)
        .with_context(|| format!("failed to read {}", settings_path.display()))?;

    let mut settings: serde_json::Value = serde_json::from_str(&content)
        .with_context(|| format!("failed to parse {}", settings_path.display()))?;

    if let Some(hooks) = settings
        .as_object_mut()
        .and_then(|obj| obj.get_mut("hooks"))
        .and_then(|h| h.as_object_mut())
    {
        let managed_paths = managed_hook_path_set(&repo_root.join(".cwt/hooks"));

        for (_event_name, event_hooks) in hooks.iter_mut() {
            if let Some(arr) = event_hooks.as_array_mut() {
                arr.retain(|v| {
                    let command = v
                        .as_object()
                        .and_then(|obj| obj.get("command"))
                        .and_then(|c| c.as_str());
                    match command {
                        Some(c) => !managed_paths.contains(c),
                        None => true,
                    }
                });
            }
        }
    }

    let content =
        serde_json::to_string_pretty(&settings).context("failed to serialize settings.json")?;
    std::fs::write(&settings_path, content)
        .with_context(|| format!("failed to write {}", settings_path.display()))?;

    Ok(())
}
