use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;

use anyhow::{Context, Result};
use serde_json::Value;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::process::Command;
use tokio::sync::Mutex;
use tokio::time;

use crate::config::{HookCommandConfig, HooksConfig, PermissionMode};
use crate::exec::events::{CompactionMode, CompactionTrigger};
use crate::permissions::PermissionRequest;

use crate::hooks::lifecycle::compiled::CompiledLifecycleHooks;
use crate::hooks::lifecycle::interpret::{
    HookCommandResult, interpret_permission_request, interpret_post_tool, interpret_pre_tool,
    interpret_session_end, interpret_session_start, interpret_stop, interpret_user_prompt,
};
use crate::hooks::lifecycle::types::{
    HookMessage, NotificationHookType, PermissionRequestHookOutcome, PostToolHookOutcome,
    PreCompactHookOutcome, PreToolHookDecision, PreToolHookOutcome, SessionEndReason,
    SessionStartHookOutcome, SessionStartTrigger, StopHookOutcome, UserPromptHookOutcome,
};
use crate::hooks::lifecycle::utils::{generate_session_id, path_to_string};

const DEFAULT_TIMEOUT_SECS: u64 = 60;

mod payloads;

#[derive(Clone)]
pub struct LifecycleHookEngine {
    inner: Arc<LifecycleHookInner>,
}

impl LifecycleHookEngine {
    pub fn new(
        workspace: PathBuf,
        config: &HooksConfig,
        trigger: SessionStartTrigger,
    ) -> Result<Option<Self>> {
        Self::new_with_session(
            workspace,
            config,
            trigger,
            generate_session_id(),
            PermissionMode::Default,
        )
    }

    pub fn new_with_session(
        workspace: PathBuf,
        config: &HooksConfig,
        trigger: SessionStartTrigger,
        session_id: impl Into<String>,
        permission_mode: PermissionMode,
    ) -> Result<Option<Self>> {
        if config.lifecycle.is_empty() {
            return Ok(None);
        }

        let compiled = CompiledLifecycleHooks::from_config(&config.lifecycle)?;
        if compiled.is_empty() {
            return Ok(None);
        }

        Ok(Some(Self {
            inner: Arc::new(LifecycleHookInner {
                workspace,
                session_id: session_id.into(),
                permission_mode: tokio::sync::RwLock::new(permission_mode),
                trigger,
                hooks: compiled,
                state: Mutex::new(LifecycleHookState {
                    transcript_path: None,
                }),
            }),
        }))
    }

    pub async fn run_session_start(&self) -> Result<SessionStartHookOutcome> {
        let mut messages = Vec::new();
        let mut additional_context = Vec::new();

        if self.inner.hooks.session_start.is_empty() {
            return Ok(SessionStartHookOutcome {
                messages,
                additional_context,
            });
        }

        let trigger_value = self.inner.trigger.as_str().to_owned();
        let payload = self.build_session_start_payload().await?;

        for group in &self.inner.hooks.session_start {
            if !group.matcher.matches(&trigger_value) {
                continue;
            }

            for command in &group.commands {
                match self
                    .execute_command("SessionStart", command, &payload)
                    .await
                {
                    Ok(result) => interpret_session_start(
                        command,
                        &result,
                        &mut messages,
                        &mut additional_context,
                        self.inner.hooks.quiet_success_output,
                    ),
                    Err(err) => messages.push(HookMessage::error(format!(
                        "SessionStart hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(SessionStartHookOutcome {
            messages,
            additional_context,
        })
    }

    pub async fn run_session_end(
        &self,
        turn_id: &str,
        reason: SessionEndReason,
    ) -> Result<Vec<HookMessage>> {
        let mut messages = Vec::new();

        if self.inner.hooks.session_end.is_empty() {
            return Ok(messages);
        }

        let payload = self.build_session_end_payload(turn_id, reason).await?;
        let reason_value = reason.as_str().to_owned();

        for group in &self.inner.hooks.session_end {
            if !group.matcher.matches(&reason_value) {
                continue;
            }

            for command in &group.commands {
                match self.execute_command("SessionEnd", command, &payload).await {
                    Ok(result) => interpret_session_end(
                        command,
                        &result,
                        &mut messages,
                        self.inner.hooks.quiet_success_output,
                    ),
                    Err(err) => messages.push(HookMessage::error(format!(
                        "SessionEnd hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(messages)
    }

    #[expect(clippy::too_many_arguments)]
    pub async fn run_subagent_start(
        &self,
        parent_session_id: &str,
        child_thread_id: &str,
        agent_name: &str,
        display_label: &str,
        background: bool,
        status: &str,
        transcript_path: Option<&std::path::Path>,
    ) -> Result<Vec<HookMessage>> {
        let mut messages = Vec::new();

        if self.inner.hooks.subagent_start.is_empty() {
            return Ok(messages);
        }

        let payload = self
            .build_subagent_start_payload(
                parent_session_id,
                child_thread_id,
                agent_name,
                display_label,
                background,
                status,
                transcript_path,
            )
            .await?;
        let matcher_value = agent_name.to_owned();

        for group in &self.inner.hooks.subagent_start {
            if !group.matcher.matches(&matcher_value) {
                continue;
            }

            for command in &group.commands {
                match self
                    .execute_command("SubagentStart", command, &payload)
                    .await
                {
                    Ok(result) => interpret_session_end(
                        command,
                        &result,
                        &mut messages,
                        self.inner.hooks.quiet_success_output,
                    ),
                    Err(err) => messages.push(HookMessage::error(format!(
                        "SubagentStart hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(messages)
    }

    #[expect(clippy::too_many_arguments)]
    pub async fn run_subagent_stop(
        &self,
        parent_session_id: &str,
        child_thread_id: &str,
        agent_name: &str,
        display_label: &str,
        background: bool,
        status: &str,
        transcript_path: Option<&std::path::Path>,
    ) -> Result<Vec<HookMessage>> {
        let mut messages = Vec::new();

        if self.inner.hooks.subagent_stop.is_empty() {
            return Ok(messages);
        }

        let payload = self
            .build_subagent_stop_payload(
                parent_session_id,
                child_thread_id,
                agent_name,
                display_label,
                background,
                status,
                transcript_path,
            )
            .await?;
        let matcher_value = agent_name.to_owned();

        for group in &self.inner.hooks.subagent_stop {
            if !group.matcher.matches(&matcher_value) {
                continue;
            }

            for command in &group.commands {
                match self
                    .execute_command("SubagentStop", command, &payload)
                    .await
                {
                    Ok(result) => interpret_session_end(
                        command,
                        &result,
                        &mut messages,
                        self.inner.hooks.quiet_success_output,
                    ),
                    Err(err) => messages.push(HookMessage::error(format!(
                        "SubagentStop hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(messages)
    }

    pub async fn run_user_prompt_submit(
        &self,
        turn_id: &str,
        prompt: &str,
    ) -> Result<UserPromptHookOutcome> {
        let mut outcome = UserPromptHookOutcome::default();

        if self.inner.hooks.user_prompt_submit.is_empty() {
            return Ok(outcome);
        }

        let payload = self.build_user_prompt_payload(turn_id, prompt).await?;

        for group in &self.inner.hooks.user_prompt_submit {
            if !group.matcher.matches(prompt) {
                continue;
            }

            for command in &group.commands {
                match self
                    .execute_command("UserPromptSubmit", command, &payload)
                    .await
                {
                    Ok(result) => {
                        interpret_user_prompt(
                            command,
                            &result,
                            &mut outcome,
                            self.inner.hooks.quiet_success_output,
                        );
                        if !outcome.allow_prompt {
                            return Ok(outcome);
                        }
                    }
                    Err(err) => outcome.messages.push(HookMessage::error(format!(
                        "UserPromptSubmit hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(outcome)
    }

    pub async fn run_permission_request(
        &self,
        tool_name: &str,
        tool_input: Option<&Value>,
        permission_request: &PermissionRequest,
        permission_suggestions: &[Value],
    ) -> Result<PermissionRequestHookOutcome> {
        let mut outcome = PermissionRequestHookOutcome::default();

        if self.inner.hooks.permission_request.is_empty() {
            return Ok(outcome);
        }

        let payload = self
            .build_permission_request_payload(
                tool_name,
                tool_input,
                permission_request,
                permission_suggestions,
            )
            .await?;

        for group in &self.inner.hooks.permission_request {
            if !group.matcher.matches(tool_name) {
                continue;
            }

            for command in &group.commands {
                match self
                    .execute_command("PermissionRequest", command, &payload)
                    .await
                {
                    Ok(result) => {
                        interpret_permission_request(
                            command,
                            &result,
                            &mut outcome,
                            self.inner.hooks.quiet_success_output,
                        );
                        if outcome.decision.is_some() {
                            return Ok(outcome);
                        }
                    }
                    Err(err) => outcome.messages.push(HookMessage::error(format!(
                        "PermissionRequest hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(outcome)
    }

    pub async fn run_pre_tool_use(
        &self,
        tool_name: &str,
        tool_input: Option<&Value>,
        tool_call_id: Option<&str>,
    ) -> Result<PreToolHookOutcome> {
        let mut outcome = PreToolHookOutcome::default();

        if self.inner.hooks.pre_tool_use.is_empty() {
            return Ok(outcome);
        }

        let payload = self
            .build_pre_tool_payload(tool_name, tool_input, tool_call_id)
            .await?;

        for group in &self.inner.hooks.pre_tool_use {
            if !group.matcher.matches(tool_name) {
                continue;
            }

            for command in &group.commands {
                match self.execute_command("PreToolUse", command, &payload).await {
                    Ok(result) => {
                        interpret_pre_tool(
                            command,
                            &result,
                            &mut outcome,
                            self.inner.hooks.quiet_success_output,
                        );
                        match outcome.decision {
                            PreToolHookDecision::Allow | PreToolHookDecision::Deny => {
                                return Ok(outcome);
                            }
                            _ => {}
                        }
                    }
                    Err(err) => outcome.messages.push(HookMessage::error(format!(
                        "PreToolUse hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(outcome)
    }

    pub async fn run_post_tool_use(
        &self,
        tool_name: &str,
        tool_input: Option<&Value>,
        tool_output: &Value,
        tool_call_id: Option<&str>,
    ) -> Result<PostToolHookOutcome> {
        let mut outcome = PostToolHookOutcome::default();

        if self.inner.hooks.post_tool_use.is_empty() {
            return Ok(outcome);
        }

        let payload = self
            .build_post_tool_payload(tool_name, tool_input, tool_output, tool_call_id)
            .await?;

        for group in &self.inner.hooks.post_tool_use {
            if !group.matcher.matches(tool_name) {
                continue;
            }

            for command in &group.commands {
                match self.execute_command("PostToolUse", command, &payload).await {
                    Ok(result) => interpret_post_tool(
                        command,
                        &result,
                        &mut outcome,
                        self.inner.hooks.quiet_success_output,
                    ),
                    Err(err) => outcome.messages.push(HookMessage::error(format!(
                        "PostToolUse hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(outcome)
    }

    pub async fn run_pre_compact(
        &self,
        trigger: CompactionTrigger,
        mode: CompactionMode,
        original_message_count: usize,
        compacted_message_count: usize,
        history_artifact_path: Option<&str>,
    ) -> Result<PreCompactHookOutcome> {
        let mut outcome = PreCompactHookOutcome::default();

        if self.inner.hooks.pre_compact.is_empty() {
            return Ok(outcome);
        }

        let payload = self
            .build_pre_compact_payload(
                trigger,
                mode,
                original_message_count,
                compacted_message_count,
                history_artifact_path,
            )
            .await?;
        let trigger_value = trigger.as_str().to_owned();

        for group in &self.inner.hooks.pre_compact {
            if !group.matcher.matches(&trigger_value) {
                continue;
            }

            for command in &group.commands {
                match self.execute_command("PreCompact", command, &payload).await {
                    Ok(result) => {
                        interpret_session_end(
                            command,
                            &result,
                            &mut outcome.messages,
                            self.inner.hooks.quiet_success_output,
                        );
                    }
                    Err(err) => outcome.messages.push(HookMessage::error(format!(
                        "PreCompact hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(outcome)
    }

    pub async fn run_notification(
        &self,
        notification_type: NotificationHookType,
        title: &str,
        message: &str,
    ) -> Result<Vec<HookMessage>> {
        let mut messages = Vec::new();

        if self.inner.hooks.notification.is_empty() {
            return Ok(messages);
        }

        let payload = self
            .build_notification_payload(notification_type, title, message)
            .await?;
        let matcher_value = notification_type.as_str().to_owned();

        for group in &self.inner.hooks.notification {
            if !group.matcher.matches(&matcher_value) {
                continue;
            }

            for command in &group.commands {
                match self
                    .execute_command("Notification", command, &payload)
                    .await
                {
                    Ok(result) => interpret_session_end(
                        command,
                        &result,
                        &mut messages,
                        self.inner.hooks.quiet_success_output,
                    ),
                    Err(err) => messages.push(HookMessage::error(format!(
                        "Notification hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(messages)
    }

    pub async fn run_stop(
        &self,
        last_assistant_message: &str,
        stop_hook_active: bool,
    ) -> Result<StopHookOutcome> {
        let mut outcome = StopHookOutcome::default();

        if self.inner.hooks.stop.is_empty() {
            return Ok(outcome);
        }

        let payload = self
            .build_stop_payload(last_assistant_message, stop_hook_active)
            .await?;

        for group in &self.inner.hooks.stop {
            if !group.matcher.matches("stop") {
                continue;
            }

            for command in &group.commands {
                match self.execute_command("Stop", command, &payload).await {
                    Ok(result) => {
                        interpret_stop(
                            command,
                            &result,
                            &mut outcome,
                            self.inner.hooks.quiet_success_output,
                        );
                        if outcome.block_reason.is_some() {
                            return Ok(outcome);
                        }
                    }
                    Err(err) => outcome.messages.push(HookMessage::error(format!(
                        "Stop hook `{}` failed: {err}",
                        command.command
                    ))),
                }
            }
        }

        Ok(outcome)
    }

    pub async fn update_transcript_path(&self, path: Option<PathBuf>) {
        let mut state = self.inner.state.lock().await;
        state.transcript_path = path;
    }

    pub async fn update_permission_mode(&self, permission_mode: PermissionMode) {
        let mut current = self.inner.permission_mode.write().await;
        *current = permission_mode;
    }

    async fn execute_command(
        &self,
        event_name: &str,
        command: &HookCommandConfig,
        payload: &Value,
    ) -> Result<HookCommandResult> {
        let mut process = Command::new("sh");
        process.arg("-c").arg(&command.command);
        process.current_dir(&self.inner.workspace);
        process.stdin(Stdio::piped());
        process.stdout(Stdio::piped());
        process.stderr(Stdio::piped());
        process.kill_on_drop(true);

        let workspace_str = self.inner.workspace.to_string_lossy().into_owned();
        process.env("VT_PROJECT_DIR", &workspace_str);
        process.env("CLAUDE_PROJECT_DIR", &workspace_str);
        process.env("VT_SESSION_ID", &self.inner.session_id);
        process.env("CLAUDE_SESSION_ID", &self.inner.session_id);
        process.env("VT_HOOK_EVENT", event_name);

        if let Some(transcript_path) = self.current_transcript_path().await {
            process.env("VT_TRANSCRIPT_PATH", &transcript_path);
            process.env("CLAUDE_TRANSCRIPT_PATH", &transcript_path);
        }

        let mut child = process
            .spawn()
            .with_context(|| format!("failed to spawn lifecycle hook `{}`", command.command))?;

        if let Some(mut stdin) = child.stdin.take() {
            let mut payload_bytes = serde_json::to_vec(payload)
                .context("failed to serialize lifecycle hook payload")?;
            payload_bytes.push(b'\n');
            stdin
                .write_all(&payload_bytes)
                .await
                .context("failed to write lifecycle hook payload")?;
            stdin
                .shutdown()
                .await
                .context("failed to close lifecycle hook stdin")?;
        }

        let mut stdout_pipe = child
            .stdout
            .take()
            .context("lifecycle hook missing stdout pipe")?;
        let mut stderr_pipe = child
            .stderr
            .take()
            .context("lifecycle hook missing stderr pipe")?;

        let stdout_task = tokio::spawn(async move {
            let mut buffer = Vec::new();
            stdout_pipe.read_to_end(&mut buffer).await.map(|_| buffer)
        });
        let stderr_task = tokio::spawn(async move {
            let mut buffer = Vec::new();
            stderr_pipe.read_to_end(&mut buffer).await.map(|_| buffer)
        });

        let timeout_secs = command
            .timeout_seconds
            .unwrap_or(DEFAULT_TIMEOUT_SECS)
            .max(1);
        let wait_result = time::timeout(Duration::from_secs(timeout_secs), child.wait()).await;

        let (exit_code, timed_out) = match wait_result {
            Ok(status_res) => {
                let status = status_res.context("failed to wait for lifecycle hook")?;
                (status.code(), false)
            }
            Err(_) => {
                let _ = child.start_kill();
                let _ = child.wait().await;
                (None, true)
            }
        };

        let stdout_bytes = stdout_task
            .await
            .unwrap_or_else(|_| Ok(Vec::new()))
            .unwrap_or_default();
        let stderr_bytes = stderr_task
            .await
            .unwrap_or_else(|_| Ok(Vec::new()))
            .unwrap_or_default();

        Ok(HookCommandResult {
            exit_code,
            stdout: String::from_utf8_lossy(&stdout_bytes).into_owned(),
            stderr: String::from_utf8_lossy(&stderr_bytes).into_owned(),
            timed_out,
            timeout_seconds: timeout_secs,
        })
    }

    async fn current_transcript_path(&self) -> Option<String> {
        let state = self.inner.state.lock().await;
        state
            .transcript_path
            .as_ref()
            .and_then(|path| path_to_string(path))
    }
}

struct LifecycleHookInner {
    workspace: PathBuf,
    session_id: String,
    permission_mode: tokio::sync::RwLock<PermissionMode>,
    trigger: SessionStartTrigger,
    hooks: CompiledLifecycleHooks,
    state: Mutex<LifecycleHookState>,
}

struct LifecycleHookState {
    transcript_path: Option<PathBuf>,
}
