//! Incremental Codex session file parser.
//!
//! Parses Codex CLI JSONL session files efficiently by tracking
//! file positions and only reading new content.

use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::Path;

use crate::model::error::TranscriptError;
use crate::model::{Event, EventBuilder, EventType};

use super::{CodexSessionEntry, FunctionCallMap, SessionMeta, parse_session_meta};

/// Tracks file read position for incremental parsing.
#[derive(Debug, Clone, Default)]
pub struct FilePosition {
    /// Byte offset in file
    pub offset: u64,
    /// Last line number processed (1-based, for debugging)
    pub line_number: u64,
    /// Last dedup key processed (for validation)
    pub last_key: Option<String>,
}

impl FilePosition {
    /// Create a new position at the start of file.
    pub fn new() -> Self {
        Self::default()
    }
}

/// Result of parsing a Codex session file.
#[derive(Debug)]
pub struct ParseResult {
    /// Events extracted from the session file
    pub events: Vec<Event>,
    /// New file position after parsing
    pub position: FilePosition,
    /// Dedup keys seen during this parse
    pub keys: HashSet<String>,
    /// Number of lines parsed into events
    pub lines_parsed: u64,
    /// Number of lines skipped (non-event entries)
    pub lines_skipped: u64,
    /// Number of lines that failed to parse as JSON
    pub parse_errors: u64,
    /// Session metadata if found
    pub session_meta: Option<SessionMeta>,
}

/// Parser for Codex CLI session JSONL files.
pub struct CodexSessionParser {
    machine_id: String,
}

impl CodexSessionParser {
    /// Create a new parser with the given machine ID.
    pub fn new(machine_id: impl Into<String>) -> Self {
        Self {
            machine_id: machine_id.into(),
        }
    }

    /// Parse new entries from a session file starting at the given position.
    ///
    /// Returns events, updated position, and dedup keys seen.
    pub fn parse_incremental(
        &self,
        path: &Path,
        session_id: &str,
        start_position: &FilePosition,
    ) -> Result<ParseResult, TranscriptError> {
        let file = File::open(path).map_err(|e| {
            if e.kind() == std::io::ErrorKind::NotFound {
                TranscriptError::NotFound(path.display().to_string())
            } else {
                TranscriptError::Io(e)
            }
        })?;

        let file_len = file.metadata()?.len();

        // If position is past file end, file may have been truncated - reset
        let start_offset = if start_position.offset > file_len {
            0
        } else {
            start_position.offset
        };

        let mut reader = BufReader::new(file);
        reader.seek(SeekFrom::Start(start_offset))?;

        let mut events = Vec::new();
        let mut keys = HashSet::new();
        let mut current_offset = start_offset;
        let mut line_number = start_position.line_number;
        let mut last_key = start_position.last_key.clone();
        let mut lines_parsed = 0u64;
        let mut lines_skipped = 0u64;
        let mut parse_errors = 0u64;

        // Track state across entries
        let mut function_call_map: FunctionCallMap = Default::default();
        let mut session_meta: Option<SessionMeta> = None;
        let mut turn_model: Option<String> = None;
        // Track which function calls have completed (have function_call_output)
        let mut completed_call_ids: HashSet<String> = HashSet::new();

        // If starting from offset 0, we need to scan for session_meta first
        // to get context. We'll do this in a first pass if at start.
        if start_offset == 0 {
            // Quick scan for session_meta (usually first line)
            let scan_file = File::open(path)?;
            let scan_reader = BufReader::new(scan_file);
            for line in scan_reader.lines() {
                let line = match line {
                    Ok(l) => l,
                    Err(_) => break,
                };
                let trimmed = line.trim();
                if trimmed.is_empty() {
                    continue;
                }
                if let Ok(entry) = serde_json::from_str::<CodexSessionEntry>(trimmed)
                    && entry.entry_type == "session_meta"
                {
                    session_meta = parse_session_meta(&entry);
                    break;
                }
            }
        }

        let mut line = String::new();
        loop {
            line.clear();
            let bytes_read = reader.read_line(&mut line)?;
            if bytes_read == 0 {
                break; // EOF
            }

            let line_bytes = bytes_read as u64;
            line_number += 1;

            // Skip empty lines
            let trimmed = line.trim();
            if trimmed.is_empty() {
                current_offset += line_bytes;
                continue;
            }

            // Try to parse as CodexSessionEntry
            match serde_json::from_str::<CodexSessionEntry>(trimmed) {
                Ok(entry) => {
                    // Track key for deduplication
                    let key = entry.dedup_key();
                    keys.insert(key.clone());
                    last_key = Some(key);

                    // Track completed function calls (those with function_call_output)
                    if entry.entry_type == "response_item"
                        && let Some(payload_type) =
                            entry.payload.get("type").and_then(|v| v.as_str())
                        && payload_type == "function_call_output"
                        && let Some(call_id) = entry.payload.get("call_id").and_then(|v| v.as_str())
                    {
                        completed_call_ids.insert(call_id.to_string());
                    }

                    // Check for session_meta to update context
                    if entry.entry_type == "session_meta" && session_meta.is_none() {
                        session_meta = parse_session_meta(&entry);
                        lines_skipped += 1;
                    } else {
                        // Convert to events
                        let entry_events = entry.into_events(
                            &self.machine_id,
                            session_id,
                            &mut function_call_map,
                            &session_meta,
                            &mut turn_model,
                        );

                        if entry_events.is_empty() {
                            lines_skipped += 1;
                        } else {
                            events.extend(entry_events);
                            lines_parsed += 1;
                        }
                    }
                }
                Err(_) => {
                    parse_errors += 1;
                }
            }

            current_offset += line_bytes;
        }

        // Generate PermissionRequest events for pending approval calls
        // A pending approval call is one that:
        // 1. Has requires_approval = true (sandbox_permissions: "require_escalated")
        // 2. Does not have a corresponding function_call_output
        for (call_id, call_info) in &function_call_map {
            if call_info.requires_approval && !completed_call_ids.contains(call_id) {
                let mut builder =
                    EventBuilder::new(&self.machine_id, EventType::PermissionRequest, session_id)
                        .source("codex_session")
                        .framework("codex")
                        .timestamp_opt(call_info.timestamp)
                        .tool(call_id.clone(), call_info.name.clone());

                // Add cwd and git from session meta
                if let Some(meta) = &session_meta {
                    builder = builder.cwd_opt(meta.cwd.clone());
                    if let Some(ref git) = meta.git {
                        builder = builder.git_branch_opt(git.branch.clone());
                    }
                }

                events.push(builder.build());
            }
        }

        let new_position = FilePosition {
            offset: current_offset,
            line_number,
            last_key,
        };

        Ok(ParseResult {
            events,
            position: new_position,
            keys,
            lines_parsed,
            lines_skipped,
            parse_errors,
            session_meta,
        })
    }

    /// Parse entire session file from the beginning.
    pub fn parse_full(
        &self,
        path: &Path,
        session_id: &str,
    ) -> Result<ParseResult, TranscriptError> {
        self.parse_incremental(path, session_id, &FilePosition::new())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn create_test_file(content: &str) -> NamedTempFile {
        let mut file = NamedTempFile::new().unwrap();
        file.write_all(content.as_bytes()).unwrap();
        file.flush().unwrap();
        file
    }

    #[test]
    fn test_parse_empty_file() {
        let file = create_test_file("");
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        assert!(result.events.is_empty());
        assert_eq!(result.lines_parsed, 0);
    }

    #[test]
    fn test_parse_session_with_token_count() {
        let content = r#"{"timestamp":"2025-11-27T01:55:56.451Z","type":"session_meta","payload":{"id":"test-123","timestamp":"2025-11-27T01:55:56.369Z","cwd":"/test"}}
{"timestamp":"2025-11-27T01:56:10.186Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":25},"model_context_window":128000}}}"#;

        let file = create_test_file(content);
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        assert_eq!(result.events.len(), 1);
        assert!(result.session_meta.is_some());
        assert_eq!(result.session_meta.as_ref().unwrap().id, "test-123");

        let event = &result.events[0];
        assert_eq!(event.tokens_input, Some(100));
        assert_eq!(event.tokens_output, Some(25));
        assert_eq!(event.tokens_cache_read, Some(50));
    }

    #[test]
    fn test_parse_function_calls() {
        let content = r#"{"timestamp":"2025-11-27T01:56:10.186Z","type":"response_item","payload":{"type":"function_call","name":"shell_command","arguments":"{\"command\":\"ls\"}","call_id":"call_1"}}
{"timestamp":"2025-11-27T01:56:10.300Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_1","output":"file.txt"}}"#;

        let file = create_test_file(content);
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        assert_eq!(result.events.len(), 2);
        assert_eq!(
            result.events[0].event_type,
            crate::model::EventType::PreToolUse
        );
        assert_eq!(
            result.events[1].event_type,
            crate::model::EventType::PostToolUse
        );
    }

    #[test]
    fn test_pending_approval_generates_permission_request() {
        // Function call with require_escalated but no output = waiting for approval
        let content = r#"{"timestamp":"2025-11-27T01:56:10.186Z","type":"response_item","payload":{"type":"function_call","name":"shell_command","arguments":"{\"command\":\"rm /tmp/test\",\"sandbox_permissions\":\"require_escalated\"}","call_id":"call_1"}}"#;

        let file = create_test_file(content);
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        // Should have PreToolUse + PermissionRequest
        assert_eq!(result.events.len(), 2);
        assert_eq!(
            result.events[0].event_type,
            crate::model::EventType::PreToolUse
        );
        assert_eq!(
            result.events[1].event_type,
            crate::model::EventType::PermissionRequest
        );
        // PermissionRequest should have the tool name
        assert_eq!(
            result.events[1].tool_name,
            Some("shell_command".to_string())
        );
    }

    #[test]
    fn test_completed_approval_no_permission_request() {
        // Function call with require_escalated AND output = completed, no PermissionRequest
        let content = r#"{"timestamp":"2025-11-27T01:56:10.186Z","type":"response_item","payload":{"type":"function_call","name":"shell_command","arguments":"{\"command\":\"rm /tmp/test\",\"sandbox_permissions\":\"require_escalated\"}","call_id":"call_1"}}
{"timestamp":"2025-11-27T01:56:10.300Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_1","output":"done"}}"#;

        let file = create_test_file(content);
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        // Should have PreToolUse + PostToolUse only (no PermissionRequest)
        assert_eq!(result.events.len(), 2);
        assert_eq!(
            result.events[0].event_type,
            crate::model::EventType::PreToolUse
        );
        assert_eq!(
            result.events[1].event_type,
            crate::model::EventType::PostToolUse
        );
    }

    #[test]
    fn test_pending_call_without_escalation_no_permission_request() {
        // Function call WITHOUT require_escalated and no output = executing (not waiting)
        let content = r#"{"timestamp":"2025-11-27T01:56:10.186Z","type":"response_item","payload":{"type":"function_call","name":"shell_command","arguments":"{\"command\":\"ls\"}","call_id":"call_1"}}"#;

        let file = create_test_file(content);
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        // Should have PreToolUse only (no PermissionRequest because no require_escalated)
        assert_eq!(result.events.len(), 1);
        assert_eq!(
            result.events[0].event_type,
            crate::model::EventType::PreToolUse
        );
    }

    #[test]
    fn test_incremental_parsing() {
        let line1 = r#"{"timestamp":"2025-11-27T01:56:10.186Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":100,"output_tokens":25}}}}"#;
        let content = format!("{}\n", line1);

        let file = create_test_file(&content);
        let parser = CodexSessionParser::new("machine-1");

        // First parse
        let result1 = parser.parse_full(file.path(), "test-session").unwrap();
        assert_eq!(result1.events.len(), 1);

        // Append new content
        let mut f = std::fs::OpenOptions::new()
            .append(true)
            .open(file.path())
            .unwrap();
        let line2 = r#"{"timestamp":"2025-11-27T01:56:11.000Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":200,"output_tokens":50}}}}"#;
        writeln!(f, "{}", line2).unwrap();

        // Incremental parse
        let result2 = parser
            .parse_incremental(file.path(), "test-session", &result1.position)
            .unwrap();
        assert_eq!(result2.events.len(), 1);
        assert_eq!(result2.events[0].tokens_input, Some(200));
    }

    #[test]
    fn test_file_not_found() {
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(Path::new("/nonexistent/path.jsonl"), "test");

        assert!(matches!(result, Err(TranscriptError::NotFound(_))));
    }

    #[test]
    fn test_parse_errors_tracked() {
        let content = r#"{"timestamp":"2025-11-27T01:56:10.186Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":100,"output_tokens":25}}}}
not valid json
{"incomplete"#;

        let file = create_test_file(content);
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        assert_eq!(result.events.len(), 1);
        assert_eq!(result.parse_errors, 2);
    }

    #[test]
    fn test_user_message_parsing() {
        let content = r#"{"timestamp":"2025-11-27T01:56:07.612Z","type":"event_msg","payload":{"type":"user_message","message":"Hello!","images":[]}}"#;

        let file = create_test_file(content);
        let parser = CodexSessionParser::new("machine-1");
        let result = parser.parse_full(file.path(), "test-session").unwrap();

        assert_eq!(result.events.len(), 1);
        assert_eq!(
            result.events[0].event_type,
            crate::model::EventType::UserPromptSubmit
        );
    }
}
