use anyhow::Result;
use serde_json::Value;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};

use crate::change_intel::line_hash::{diff_with_hashes, hash_line, hashes_for_text, line_count};
use crate::change_intel::types::{LineHashCount, LineSide, WriteMode};
use crate::path_utils::{
    normalize_filesystem_path, path_to_string, resolve_path, strip_file_scheme,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClaudeMessageRole {
    User,
    Assistant,
}

impl ClaudeMessageRole {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::User => "user",
            Self::Assistant => "assistant",
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ClaudeVisibleMessage {
    pub role: ClaudeMessageRole,
    pub text: String,
    pub timestamp: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct ClaudeStructuredWrite {
    pub session_id: String,
    pub source_file: String,
    pub session_cwd: Option<String>,
    pub timestamp: Option<String>,
    pub call_id: String,
    pub abs_path: String,
    pub write_mode: WriteMode,
    pub before_known: bool,
    pub added_lines: i64,
    pub removed_lines: i64,
    pub line_hashes: Vec<LineHashCount>,
    pub parser_name: &'static str,
}

#[derive(Debug, Clone)]
pub(crate) struct ParsedClaudeSession {
    pub session_id: String,
    pub source_file: String,
    pub session_cwd: Option<String>,
    pub started_at: Option<String>,
    pub ended_at: Option<String>,
    pub model_name: Option<String>,
    pub visible_messages: Vec<ClaudeVisibleMessage>,
    pub usage_events: Vec<ClaudeUsageEvent>,
    pub structured_writes: Vec<ClaudeStructuredWrite>,
    pub tool_call_count: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ClaudeUsageEvent {
    pub input_tokens: i64,
    pub cache_creation_input_tokens: i64,
    pub cache_read_input_tokens: i64,
    pub output_tokens: i64,
    pub total_tokens: i64,
}

#[derive(Debug, Clone)]
struct PendingToolUse {
    timestamp: Option<String>,
    call_id: String,
    tool_name: String,
    input: Value,
    result_payload: Option<Value>,
    result_error: bool,
}

#[derive(Debug, Default)]
struct ParseState {
    model_name: Option<String>,
    visible_messages: Vec<ClaudeVisibleMessage>,
    usage_events: Vec<ClaudeUsageEvent>,
    pending: Vec<PendingToolUse>,
    pending_by_id: HashMap<String, usize>,
    tool_call_count: usize,
}

const CLAUDE_EDIT_PARSER_NAME: &str = "claude_edit_v1";
const CLAUDE_WRITE_PARSER_NAME: &str = "claude_write_v1";

pub(crate) fn plan_session_files() -> Result<Vec<PathBuf>> {
    let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Home directory not found"))?;
    let root = home.join(".claude").join("projects");
    if !root.exists() {
        return Ok(Vec::new());
    }

    let mut files = discover_top_level_jsonl_files(&root);
    files.sort();
    Ok(files)
}

pub(crate) fn parse_session_file(path: &Path) -> Result<ParsedClaudeSession> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let source_file = path.to_string_lossy().to_string();
    let fallback_session_id = path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .unwrap_or_default()
        .to_string();

    let mut session_id = fallback_session_id.clone();
    let mut session_cwd: Option<String> = None;
    let mut started_at: Option<String> = None;
    let mut ended_at: Option<String> = None;
    let mut state = ParseState::default();

    for line_result in reader.lines() {
        let raw = line_result?;
        if raw.trim().is_empty() {
            continue;
        }

        let parsed: Value = match serde_json::from_str(&raw) {
            Ok(value) => value,
            Err(_) => continue,
        };

        if let Some(found_session_id) = parsed.get("sessionId").and_then(Value::as_str)
            && !found_session_id.trim().is_empty()
        {
            session_id = found_session_id.to_string();
        }

        if let Some(found_cwd) = parsed.get("cwd").and_then(Value::as_str)
            && !found_cwd.trim().is_empty()
        {
            session_cwd = Some(found_cwd.to_string());
        }

        let outer_timestamp = parsed
            .get("timestamp")
            .and_then(Value::as_str)
            .map(ToOwned::to_owned);
        update_time_bounds(&mut started_at, &mut ended_at, outer_timestamp.as_deref());

        process_top_level_record(&parsed, &mut state);

        if parsed.get("type").and_then(Value::as_str) == Some("progress")
            && parsed
                .get("data")
                .and_then(|value| value.get("type"))
                .and_then(Value::as_str)
                == Some("agent_progress")
            && let Some(nested) = parsed.get("data").and_then(|value| value.get("message"))
        {
            process_nested_progress_message(nested, &parsed, &mut state);
        }
    }

    if session_id.is_empty() {
        session_id = fallback_session_id;
    }

    let structured_writes = state
        .pending
        .into_iter()
        .filter_map(|tool| {
            build_structured_write(tool, &session_id, &source_file, session_cwd.as_deref())
        })
        .collect();

    Ok(ParsedClaudeSession {
        session_id,
        source_file,
        session_cwd,
        started_at,
        ended_at,
        model_name: state.model_name,
        visible_messages: state.visible_messages,
        usage_events: state.usage_events,
        structured_writes,
        tool_call_count: state.tool_call_count,
    })
}

fn discover_top_level_jsonl_files(root: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    let Ok(entries) = std::fs::read_dir(root) else {
        return files;
    };

    for entry in entries.flatten() {
        let project_dir = entry.path();
        if !project_dir.is_dir() {
            continue;
        }

        let Ok(project_entries) = std::fs::read_dir(&project_dir) else {
            continue;
        };
        for project_entry in project_entries.flatten() {
            let candidate = project_entry.path();
            if candidate.extension().and_then(|ext| ext.to_str()) == Some("jsonl") {
                files.push(candidate);
            }
        }
    }

    files
}

fn process_top_level_record(parsed: &Value, state: &mut ParseState) {
    let Some(kind) = parsed.get("type").and_then(Value::as_str) else {
        return;
    };
    let Some(message) = parsed.get("message") else {
        return;
    };
    let timestamp = parsed
        .get("timestamp")
        .and_then(Value::as_str)
        .map(ToOwned::to_owned);
    let is_meta = parsed
        .get("isMeta")
        .and_then(Value::as_bool)
        .unwrap_or(false);

    match kind {
        "assistant" => {
            if let Some(found_model) = extract_real_model_name(message) {
                state.model_name = Some(found_model);
            }
            if let Some(usage) = extract_claude_usage(message) {
                state.usage_events.push(usage);
            }
            if let Some(text) = extract_assistant_visible_text(message)
                && !text.trim().is_empty()
            {
                state.visible_messages.push(ClaudeVisibleMessage {
                    role: ClaudeMessageRole::Assistant,
                    text,
                    timestamp: timestamp.clone(),
                });
            }
            register_tool_uses(message, timestamp, state);
        }
        "user" => {
            attach_tool_results(parsed, message, state);
            if !is_meta
                && let Some(text) = extract_user_visible_text(message)
                && !should_ignore_user_text(&text)
            {
                state.visible_messages.push(ClaudeVisibleMessage {
                    role: ClaudeMessageRole::User,
                    text,
                    timestamp,
                });
            }
        }
        _ => {}
    }
}

fn extract_claude_usage(message: &Value) -> Option<ClaudeUsageEvent> {
    let usage = message.get("usage")?;
    let input_tokens = usage
        .get("input_tokens")
        .and_then(Value::as_i64)
        .unwrap_or(0);
    let cache_creation_input_tokens = usage
        .get("cache_creation_input_tokens")
        .and_then(Value::as_i64)
        .unwrap_or(0);
    let cache_read_input_tokens = usage
        .get("cache_read_input_tokens")
        .and_then(Value::as_i64)
        .unwrap_or(0);
    let output_tokens = usage
        .get("output_tokens")
        .and_then(Value::as_i64)
        .unwrap_or(0);
    let total_tokens =
        input_tokens + cache_creation_input_tokens + cache_read_input_tokens + output_tokens;

    Some(ClaudeUsageEvent {
        input_tokens,
        cache_creation_input_tokens,
        cache_read_input_tokens,
        output_tokens,
        total_tokens,
    })
}

fn process_nested_progress_message(nested: &Value, outer: &Value, state: &mut ParseState) {
    let Some(kind) = nested.get("type").and_then(Value::as_str) else {
        return;
    };
    let Some(message) = nested.get("message") else {
        return;
    };
    let timestamp = nested
        .get("timestamp")
        .and_then(Value::as_str)
        .or_else(|| outer.get("timestamp").and_then(Value::as_str))
        .map(ToOwned::to_owned);

    match kind {
        "assistant" => register_tool_uses(message, timestamp, state),
        "user" => attach_tool_results(outer, message, state),
        _ => {}
    }
}

fn register_tool_uses(message: &Value, timestamp: Option<String>, state: &mut ParseState) {
    let Some(items) = message.get("content").and_then(Value::as_array) else {
        return;
    };

    for item in items {
        if item.get("type").and_then(Value::as_str) != Some("tool_use") {
            continue;
        }

        let Some(call_id) = item.get("id").and_then(Value::as_str) else {
            continue;
        };
        let Some(tool_name) = item.get("name").and_then(Value::as_str) else {
            continue;
        };

        state.tool_call_count += 1;
        let pending_index = state.pending.len();
        state.pending.push(PendingToolUse {
            timestamp: timestamp.clone(),
            call_id: call_id.to_string(),
            tool_name: tool_name.to_string(),
            input: item.get("input").cloned().unwrap_or(Value::Null),
            result_payload: None,
            result_error: false,
        });
        state
            .pending_by_id
            .insert(call_id.to_string(), pending_index);
    }
}

fn attach_tool_results(outer: &Value, message: &Value, state: &mut ParseState) {
    let Some(items) = message.get("content").and_then(Value::as_array) else {
        return;
    };

    for item in items {
        if item.get("type").and_then(Value::as_str) != Some("tool_result") {
            continue;
        }

        let Some(tool_use_id) = item
            .get("tool_use_id")
            .and_then(Value::as_str)
            .or_else(|| item.get("toolUseId").and_then(Value::as_str))
        else {
            continue;
        };
        let Some(index) = state.pending_by_id.get(tool_use_id).copied() else {
            continue;
        };
        let Some(tool) = state.pending.get_mut(index) else {
            continue;
        };

        tool.result_error = item
            .get("is_error")
            .and_then(Value::as_bool)
            .unwrap_or(false);
        tool.result_payload = outer.get("toolUseResult").cloned();
        if tool.result_error
            && tool.result_payload.is_none()
            && let Some(text) = item.get("content").and_then(Value::as_str)
        {
            tool.result_payload = Some(Value::String(text.to_string()));
        }
    }
}

fn extract_real_model_name(message: &Value) -> Option<String> {
    let model = message.get("model").and_then(Value::as_str)?;
    let trimmed = model.trim();
    if trimmed.is_empty() || trimmed == "<synthetic>" {
        return None;
    }
    Some(trimmed.to_string())
}

fn extract_assistant_visible_text(message: &Value) -> Option<String> {
    let items = message.get("content").and_then(Value::as_array)?;
    let text = items
        .iter()
        .filter(|item| item.get("type").and_then(Value::as_str) == Some("text"))
        .filter_map(|item| item.get("text").and_then(Value::as_str))
        .collect::<Vec<_>>()
        .join("\n");
    (!text.trim().is_empty()).then_some(text)
}

fn extract_user_visible_text(message: &Value) -> Option<String> {
    match message.get("content") {
        Some(Value::String(text)) => Some(text.to_string()),
        Some(Value::Array(items)) => {
            if items
                .iter()
                .any(|item| item.get("type").and_then(Value::as_str) == Some("tool_result"))
            {
                return None;
            }

            let text = items
                .iter()
                .filter(|item| item.get("type").and_then(Value::as_str) == Some("text"))
                .filter_map(|item| item.get("text").and_then(Value::as_str))
                .collect::<Vec<_>>()
                .join("\n");
            (!text.trim().is_empty()).then_some(text)
        }
        _ => None,
    }
}

fn should_ignore_user_text(text: &str) -> bool {
    let trimmed = text.trim();
    trimmed.is_empty()
        || trimmed == "[Request interrupted by user for tool use]"
        || trimmed.contains("<local-command-caveat>")
        || trimmed.contains("<command-name>/")
        || trimmed.starts_with("<command-")
}

fn build_structured_write(
    tool: PendingToolUse,
    session_id: &str,
    source_file: &str,
    session_cwd: Option<&str>,
) -> Option<ClaudeStructuredWrite> {
    if tool.result_error {
        return None;
    }

    match tool.tool_name.as_str() {
        "Edit" => build_edit_write(tool, session_id, source_file, session_cwd),
        "Write" => build_file_write(tool, session_id, source_file, session_cwd),
        _ => None,
    }
}

fn build_edit_write(
    tool: PendingToolUse,
    session_id: &str,
    source_file: &str,
    session_cwd: Option<&str>,
) -> Option<ClaudeStructuredWrite> {
    let payload = tool.result_payload.as_ref()?.as_object()?;
    let raw_path = payload
        .get("filePath")
        .and_then(Value::as_str)
        .or_else(|| tool.input.get("file_path").and_then(Value::as_str))?;
    let abs_path = resolve_claude_path(raw_path, session_cwd)?;
    if should_ignore_write_path(&abs_path) {
        return None;
    }

    let old_string = payload
        .get("oldString")
        .and_then(Value::as_str)
        .or_else(|| tool.input.get("old_string").and_then(Value::as_str))?;
    let new_string = payload
        .get("newString")
        .and_then(Value::as_str)
        .or_else(|| tool.input.get("new_string").and_then(Value::as_str))?;

    let diff = diff_with_hashes(old_string, new_string);
    if diff.added_lines == 0 && diff.removed_lines == 0 {
        return None;
    }

    Some(ClaudeStructuredWrite {
        session_id: session_id.to_string(),
        source_file: source_file.to_string(),
        session_cwd: session_cwd.map(ToOwned::to_owned),
        timestamp: tool.timestamp,
        call_id: tool.call_id,
        abs_path,
        write_mode: WriteMode::Patch,
        before_known: true,
        added_lines: diff.added_lines,
        removed_lines: diff.removed_lines,
        line_hashes: diff.line_hashes,
        parser_name: CLAUDE_EDIT_PARSER_NAME,
    })
}

fn build_file_write(
    tool: PendingToolUse,
    session_id: &str,
    source_file: &str,
    session_cwd: Option<&str>,
) -> Option<ClaudeStructuredWrite> {
    let payload = tool.result_payload.as_ref()?.as_object()?;
    let raw_path = payload
        .get("filePath")
        .and_then(Value::as_str)
        .or_else(|| tool.input.get("file_path").and_then(Value::as_str))?;
    let abs_path = resolve_claude_path(raw_path, session_cwd)?;
    if should_ignore_write_path(&abs_path) {
        return None;
    }

    let write_type = payload
        .get("type")
        .and_then(Value::as_str)
        .or_else(|| tool.input.get("type").and_then(Value::as_str));
    let after = payload
        .get("content")
        .and_then(Value::as_str)
        .or_else(|| tool.input.get("content").and_then(Value::as_str))?;

    let (write_mode, before_known, added_lines, removed_lines, line_hashes) =
        if write_type == Some("create") {
            (
                WriteMode::Overwrite,
                false,
                line_count(after),
                0,
                hashes_for_text(after, LineSide::Added),
            )
        } else if let Some(summary) = structured_patch_summary(payload.get("structuredPatch")) {
            (
                WriteMode::Patch,
                true,
                summary.added_lines,
                summary.removed_lines,
                summary.line_hashes,
            )
        } else if let Some(original) = payload.get("originalFile").and_then(Value::as_str) {
            let diff = diff_with_hashes(original, after);
            (
                WriteMode::Overwrite,
                true,
                diff.added_lines,
                diff.removed_lines,
                diff.line_hashes,
            )
        } else {
            (
                WriteMode::Overwrite,
                false,
                line_count(after),
                0,
                hashes_for_text(after, LineSide::Added),
            )
        };

    if added_lines == 0 && removed_lines == 0 {
        return None;
    }

    Some(ClaudeStructuredWrite {
        session_id: session_id.to_string(),
        source_file: source_file.to_string(),
        session_cwd: session_cwd.map(ToOwned::to_owned),
        timestamp: tool.timestamp,
        call_id: tool.call_id,
        abs_path,
        write_mode,
        before_known,
        added_lines,
        removed_lines,
        line_hashes,
        parser_name: CLAUDE_WRITE_PARSER_NAME,
    })
}

#[derive(Debug)]
struct StructuredPatchSummary {
    added_lines: i64,
    removed_lines: i64,
    line_hashes: Vec<LineHashCount>,
}

fn structured_patch_summary(value: Option<&Value>) -> Option<StructuredPatchSummary> {
    let hunks = value?.as_array()?;
    let mut counts: HashMap<(LineSide, String), i64> = HashMap::new();
    let mut added_lines = 0i64;
    let mut removed_lines = 0i64;

    for hunk in hunks {
        let Some(lines) = hunk.get("lines").and_then(Value::as_array) else {
            continue;
        };

        for line in lines.iter().filter_map(Value::as_str) {
            if let Some(rest) = line.strip_prefix('+') {
                added_lines += 1;
                *counts
                    .entry((LineSide::Added, hash_line(rest)))
                    .or_insert(0) += 1;
            } else if let Some(rest) = line.strip_prefix('-') {
                removed_lines += 1;
                *counts
                    .entry((LineSide::Removed, hash_line(rest)))
                    .or_insert(0) += 1;
            }
        }
    }

    if added_lines == 0 && removed_lines == 0 {
        return None;
    }

    let line_hashes = counts
        .into_iter()
        .map(|((side, line_hash), count)| LineHashCount {
            side,
            line_hash,
            count,
        })
        .collect();

    Some(StructuredPatchSummary {
        added_lines,
        removed_lines,
        line_hashes,
    })
}

fn resolve_claude_path(raw_path: &str, session_cwd: Option<&str>) -> Option<String> {
    let stripped = strip_file_scheme(raw_path);
    let normalized = normalize_filesystem_path(&stripped);
    let resolved = resolve_path(&normalized, None, session_cwd);
    Some(path_to_string(&resolved))
}

fn should_ignore_write_path(path: &str) -> bool {
    let normalized = path.replace('\\', "/");
    normalized.contains("/.claude/plans/")
}

fn update_time_bounds(
    started_at: &mut Option<String>,
    ended_at: &mut Option<String>,
    timestamp: Option<&str>,
) {
    let Some(timestamp) = timestamp else {
        return;
    };
    if started_at
        .as_ref()
        .map(|current| timestamp < current.as_str())
        .unwrap_or(true)
    {
        *started_at = Some(timestamp.to_string());
    }
    if ended_at
        .as_ref()
        .map(|current| timestamp > current.as_str())
        .unwrap_or(true)
    {
        *ended_at = Some(timestamp.to_string());
    }
}

#[cfg(test)]
mod tests {
    use super::{
        ClaudeMessageRole, ClaudeUsageEvent, discover_top_level_jsonl_files, parse_session_file,
        should_ignore_user_text, structured_patch_summary,
    };
    use anyhow::Result;
    use serde_json::json;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn structured_patch_summary_counts_and_hashes_changes() {
        let summary = structured_patch_summary(Some(&json!([
            {"lines": [" line 1", "-old value", "+new value", "+second line"]}
        ])))
        .expect("summary should be produced");

        assert_eq!(summary.added_lines, 2);
        assert_eq!(summary.removed_lines, 1);
        assert_eq!(summary.line_hashes.len(), 3);
    }

    #[test]
    fn ignore_user_text_filters_local_command_noise() {
        assert!(should_ignore_user_text(
            "<local-command-caveat>ignore me</local-command-caveat>"
        ));
        assert!(should_ignore_user_text(
            "[Request interrupted by user for tool use]"
        ));
        assert!(!should_ignore_user_text("please implement this"));
    }

    #[test]
    fn parser_extracts_visible_messages_and_structured_writes() -> Result<()> {
        let tempdir = tempdir()?;
        let path = tempdir.path().join("session.jsonl");
        fs::write(
            &path,
            [
                json!({
                    "type": "user",
                    "timestamp": "2026-04-22T09:00:00Z",
                    "sessionId": "claude-visible",
                    "cwd": "/tmp/repo",
                    "message": {"role": "user", "content": "please update the matcher"}
                })
                .to_string(),
                json!({
                    "type": "assistant",
                    "timestamp": "2026-04-22T09:00:01Z",
                    "sessionId": "claude-visible",
                    "cwd": "/tmp/repo",
                    "message": {
                        "model": "claude-sonnet-4-5-20250929",
                        "role": "assistant",
                        "content": [
                            {"type": "text", "text": "I will update the matcher."},
                            {"type": "tool_use", "id": "toolu_edit", "name": "Edit", "input": {
                                "file_path": "/tmp/repo/src/lib.rs",
                                "old_string": "old",
                                "new_string": "new"
                            }}
                        ]
                    }
                })
                .to_string(),
                json!({
                    "type": "user",
                    "timestamp": "2026-04-22T09:00:02Z",
                    "sessionId": "claude-visible",
                    "cwd": "/tmp/repo",
                    "message": {
                        "role": "user",
                        "content": [{"type": "tool_result", "tool_use_id": "toolu_edit", "content": "updated", "is_error": false}]
                    },
                    "toolUseResult": {
                        "filePath": "/tmp/repo/src/lib.rs",
                        "oldString": "old",
                        "newString": "new",
                        "originalFile": "old\n"
                    }
                })
                .to_string(),
            ]
            .join("\n"),
        )?;

        let parsed = parse_session_file(&path)?;
        assert_eq!(parsed.session_id, "claude-visible");
        assert_eq!(
            parsed.visible_messages.len(),
            2,
            "top-level user + assistant text should be visible"
        );
        assert_eq!(parsed.visible_messages[0].role, ClaudeMessageRole::User);
        assert_eq!(
            parsed.visible_messages[1].role,
            ClaudeMessageRole::Assistant
        );
        assert_eq!(
            parsed.model_name.as_deref(),
            Some("claude-sonnet-4-5-20250929")
        );
        assert_eq!(parsed.structured_writes.len(), 1);
        assert_eq!(parsed.structured_writes[0].abs_path, "/tmp/repo/src/lib.rs");
        Ok(())
    }

    #[test]
    fn parser_extracts_usage_events() -> Result<()> {
        let tempdir = tempdir()?;
        let path = tempdir.path().join("usage.jsonl");
        fs::write(
            &path,
            json!({
                "type": "assistant",
                "timestamp": "2026-04-22T09:00:01Z",
                "sessionId": "claude-usage",
                "cwd": "/tmp/repo",
                "message": {
                    "model": "claude-opus-4-6",
                    "role": "assistant",
                    "usage": {
                        "input_tokens": 3,
                        "cache_creation_input_tokens": 1390,
                        "cache_read_input_tokens": 14316,
                        "output_tokens": 2
                    },
                    "content": [{"type": "text", "text": "done"}]
                }
            })
            .to_string(),
        )?;

        let parsed = parse_session_file(&path)?;

        assert_eq!(
            parsed.usage_events,
            vec![ClaudeUsageEvent {
                input_tokens: 3,
                cache_creation_input_tokens: 1390,
                cache_read_input_tokens: 14316,
                output_tokens: 2,
                total_tokens: 15711,
            }]
        );
        Ok(())
    }

    #[test]
    fn discover_only_top_level_project_sessions() -> Result<()> {
        let tempdir = tempdir()?;
        let project_root = tempdir.path().join("project-a");
        let subagents = project_root.join("subagents");
        fs::create_dir_all(&subagents)?;
        fs::write(project_root.join("top.jsonl"), "")?;
        fs::write(subagents.join("agent.jsonl"), "")?;

        let planned = discover_top_level_jsonl_files(tempdir.path());

        assert_eq!(planned.len(), 1);
        assert!(planned[0].ends_with("top.jsonl"));
        Ok(())
    }
}
