//! Structured progress logging for task execution.
//!
//! This module provides an append-only progress log file system that persists
//! learnings, patterns, and context across task iterations. The log is stored
//! in the task's worktree directory as `progress.log`.
//!
//! Format:
//! ```markdown
//! # progress.log
//! # Auto-generated by Makima - DO NOT EDIT MANUALLY
//!
//! ## [2024-01-15T10:30:00Z] - Task [fc09b908-...]: Implement user authentication
//! - Status: done
//! - Files changed: src/auth.rs, src/lib.rs
//! - **Learnings:**
//!   - Pattern: Use bcrypt for password hashing
//!   - Gotcha: Need to handle expired tokens gracefully
//! ---
//! ```

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs::{self, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use uuid::Uuid;

/// Status of a completed task entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProgressEntryStatus {
    /// Task completed successfully.
    Done,
    /// Task failed.
    Failed,
}

impl std::fmt::Display for ProgressEntryStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ProgressEntryStatus::Done => write!(f, "done"),
            ProgressEntryStatus::Failed => write!(f, "failed"),
        }
    }
}

impl std::str::FromStr for ProgressEntryStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "done" | "completed" => Ok(ProgressEntryStatus::Done),
            "failed" | "error" => Ok(ProgressEntryStatus::Failed),
            _ => Err(format!("Unknown status: {}", s)),
        }
    }
}

/// A single progress log entry representing a completed task iteration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgressEntry {
    /// Timestamp when the entry was created.
    pub timestamp: DateTime<Utc>,
    /// Task ID.
    pub task_id: Uuid,
    /// Human-readable task name.
    pub task_name: String,
    /// Completion status (done/failed).
    pub status: ProgressEntryStatus,
    /// List of files that were changed.
    pub files_changed: Vec<String>,
    /// Learnings discovered during task execution.
    pub learnings: Vec<String>,
}

impl ProgressEntry {
    /// Create a new progress entry.
    pub fn new(
        task_id: Uuid,
        task_name: String,
        status: ProgressEntryStatus,
        files_changed: Vec<String>,
        learnings: Vec<String>,
    ) -> Self {
        Self {
            timestamp: Utc::now(),
            task_id,
            task_name,
            status,
            files_changed,
            learnings,
        }
    }

    /// Format the entry as human-readable markdown.
    pub fn to_markdown(&self) -> String {
        let mut output = String::new();

        // Header with timestamp, task ID, and name
        output.push_str(&format!(
            "## [{}] - Task [{}]: {}\n",
            self.timestamp.format("%Y-%m-%dT%H:%M:%SZ"),
            &self.task_id.to_string()[..8], // Short task ID
            self.task_name
        ));

        // Status
        output.push_str(&format!("- Status: {}\n", self.status));

        // Files changed
        if self.files_changed.is_empty() {
            output.push_str("- Files changed: (none)\n");
        } else {
            output.push_str(&format!(
                "- Files changed: {}\n",
                self.files_changed.join(", ")
            ));
        }

        // Learnings
        output.push_str("- **Learnings:**\n");
        if self.learnings.is_empty() {
            output.push_str("  - (none recorded)\n");
        } else {
            for learning in &self.learnings {
                output.push_str(&format!("  - {}\n", learning));
            }
        }

        // Separator
        output.push_str("---\n");

        output
    }

    /// Parse a progress entry from markdown text.
    ///
    /// Returns None if the text doesn't contain a valid progress entry.
    pub fn from_markdown(text: &str) -> Option<Self> {
        let lines: Vec<&str> = text.lines().collect();

        // Parse header: ## [timestamp] - Task [id]: name
        let header = lines.first()?;
        if !header.starts_with("## [") {
            return None;
        }

        // Extract timestamp
        let timestamp_end = header.find(']')?;
        let timestamp_str = &header[4..timestamp_end];
        let timestamp = DateTime::parse_from_rfc3339(timestamp_str)
            .ok()?
            .with_timezone(&Utc);

        // Extract task ID
        let task_start = header.find("Task [")? + 6;
        let task_end = header[task_start..].find(']')? + task_start;
        let task_id_short = &header[task_start..task_end];

        // Try to parse as UUID (we stored short version, need to handle that)
        // For now, we'll generate a placeholder UUID if we can't parse the short version
        let task_id = Uuid::parse_str(task_id_short)
            .or_else(|_| {
                // Try to parse with padding (short IDs are just first 8 chars)
                Uuid::parse_str(&format!("{}-0000-0000-0000-000000000000", task_id_short))
            })
            .ok()?;

        // Extract task name
        let name_start = header.find("]: ")? + 3;
        let task_name = header[name_start..].to_string();

        // Parse remaining fields
        let mut status = ProgressEntryStatus::Done;
        let mut files_changed = Vec::new();
        let mut learnings = Vec::new();
        let mut in_learnings = false;

        for line in &lines[1..] {
            let line = line.trim();

            if line.starts_with("- Status:") {
                let status_str = line.trim_start_matches("- Status:").trim();
                status = status_str.parse().unwrap_or(ProgressEntryStatus::Done);
                in_learnings = false;
            } else if line.starts_with("- Files changed:") {
                let files_str = line.trim_start_matches("- Files changed:").trim();
                if files_str != "(none)" {
                    files_changed = files_str
                        .split(',')
                        .map(|s| s.trim().to_string())
                        .filter(|s| !s.is_empty())
                        .collect();
                }
                in_learnings = false;
            } else if line.starts_with("- **Learnings:**") {
                in_learnings = true;
            } else if in_learnings && line.starts_with("- ") {
                let learning = line.trim_start_matches("- ").trim();
                if learning != "(none recorded)" {
                    learnings.push(learning.to_string());
                }
            } else if line == "---" {
                break;
            }
        }

        Some(ProgressEntry {
            timestamp,
            task_id,
            task_name,
            status,
            files_changed,
            learnings,
        })
    }
}

/// Progress log manager for a task's worktree.
pub struct ProgressLog {
    /// Path to the progress.log file.
    log_path: PathBuf,
}

/// Default file name for the progress log.
pub const PROGRESS_LOG_FILENAME: &str = "progress.log";

/// Default maximum number of entries to inject into prompts.
pub const DEFAULT_MAX_ENTRIES_INJECTED: usize = 20;

impl ProgressLog {
    /// Create a new ProgressLog for a worktree directory.
    pub fn new(worktree_path: &Path) -> Self {
        Self {
            log_path: Self::get_log_path(worktree_path),
        }
    }

    /// Get the path to the progress log file for a worktree.
    pub fn get_log_path(worktree_path: &Path) -> PathBuf {
        worktree_path.join(PROGRESS_LOG_FILENAME)
    }

    /// Get the path to this progress log file.
    pub fn path(&self) -> &Path {
        &self.log_path
    }

    /// Check if the progress log file exists.
    pub fn exists(&self) -> bool {
        self.log_path.exists()
    }

    /// Append a progress entry to the log file.
    ///
    /// Creates the file with a header if it doesn't exist.
    pub fn append_entry(&self, entry: &ProgressEntry) -> io::Result<()> {
        let file_exists = self.log_path.exists();

        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.log_path)?;

        // Write header if this is a new file
        if !file_exists {
            writeln!(file, "# progress.log")?;
            writeln!(file, "# Auto-generated by Makima - DO NOT EDIT MANUALLY")?;
            writeln!(file)?;
        }

        // Append the entry
        write!(file, "{}", entry.to_markdown())?;
        writeln!(file)?;

        Ok(())
    }

    /// Read all entries from the progress log.
    pub fn read_all_entries(&self) -> io::Result<Vec<ProgressEntry>> {
        if !self.log_path.exists() {
            return Ok(Vec::new());
        }

        let file = fs::File::open(&self.log_path)?;
        let reader = BufReader::new(file);

        let mut entries = Vec::new();
        let mut current_entry = String::new();

        for line in reader.lines() {
            let line = line?;

            // Start of a new entry
            if line.starts_with("## [") {
                // Process previous entry if any
                if !current_entry.is_empty() {
                    if let Some(entry) = ProgressEntry::from_markdown(&current_entry) {
                        entries.push(entry);
                    }
                    current_entry.clear();
                }
            }

            // Skip header lines
            if line.starts_with("# ") || line.is_empty() {
                continue;
            }

            current_entry.push_str(&line);
            current_entry.push('\n');
        }

        // Process last entry
        if !current_entry.is_empty() {
            if let Some(entry) = ProgressEntry::from_markdown(&current_entry) {
                entries.push(entry);
            }
        }

        Ok(entries)
    }

    /// Read the most recent entries from the progress log.
    ///
    /// Returns up to `max` entries, starting from the most recent.
    pub fn read_recent_entries(&self, max: usize) -> io::Result<Vec<ProgressEntry>> {
        let mut entries = self.read_all_entries()?;

        // Return the last `max` entries (most recent)
        if entries.len() > max {
            entries = entries.split_off(entries.len() - max);
        }

        Ok(entries)
    }

    /// Format recent entries for injection into a prompt.
    ///
    /// Returns a formatted string containing the recent progress entries
    /// suitable for including in a Claude prompt.
    pub fn format_for_prompt(&self, max_entries: usize) -> io::Result<String> {
        let entries = self.read_recent_entries(max_entries)?;

        if entries.is_empty() {
            return Ok(String::new());
        }

        let mut output = String::new();
        output.push_str("## Previous Task Progress\n\n");
        output.push_str("The following entries show recent task completions and learnings:\n\n");

        for entry in &entries {
            output.push_str(&entry.to_markdown());
            output.push('\n');
        }

        Ok(output)
    }

    /// Get the total number of entries in the log.
    pub fn entry_count(&self) -> io::Result<usize> {
        Ok(self.read_all_entries()?.len())
    }
}

/// Helper function to create a progress entry and append it to a log.
///
/// This is a convenience function for the common case of appending a single entry.
pub fn append_progress_entry(
    worktree_path: &Path,
    task_id: Uuid,
    task_name: String,
    status: ProgressEntryStatus,
    files_changed: Vec<String>,
    learnings: Vec<String>,
) -> io::Result<()> {
    let log = ProgressLog::new(worktree_path);
    let entry = ProgressEntry::new(task_id, task_name, status, files_changed, learnings);
    log.append_entry(&entry)
}

/// Get the list of files changed in a git worktree.
///
/// This runs `git diff --name-only HEAD~1` to get files changed in the last commit,
/// falling back to `git diff --name-only` for uncommitted changes if no commits exist.
pub async fn get_git_changed_files(worktree_path: &Path) -> Vec<String> {
    // Try to get files from the last commit first
    let output = tokio::process::Command::new("git")
        .args(["diff", "--name-only", "HEAD~1"])
        .current_dir(worktree_path)
        .output()
        .await;

    if let Ok(output) = output {
        if output.status.success() {
            let files: Vec<String> = String::from_utf8_lossy(&output.stdout)
                .lines()
                .filter(|s| !s.is_empty())
                .map(String::from)
                .collect();
            if !files.is_empty() {
                return files;
            }
        }
    }

    // Fall back to uncommitted changes
    let output = tokio::process::Command::new("git")
        .args(["diff", "--name-only", "HEAD"])
        .current_dir(worktree_path)
        .output()
        .await;

    if let Ok(output) = output {
        if output.status.success() {
            return String::from_utf8_lossy(&output.stdout)
                .lines()
                .filter(|s| !s.is_empty())
                .map(String::from)
                .collect();
        }
    }

    // Try to get all tracked files that have been modified
    let output = tokio::process::Command::new("git")
        .args(["status", "--porcelain"])
        .current_dir(worktree_path)
        .output()
        .await;

    if let Ok(output) = output {
        if output.status.success() {
            return String::from_utf8_lossy(&output.stdout)
                .lines()
                .filter(|s| !s.is_empty())
                .filter_map(|line| {
                    // Format: "XY filename" where XY is the status
                    if line.len() > 3 {
                        Some(line[3..].to_string())
                    } else {
                        None
                    }
                })
                .collect();
        }
    }

    Vec::new()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_progress_entry_to_markdown() {
        let entry = ProgressEntry {
            timestamp: DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
                .unwrap()
                .with_timezone(&Utc),
            task_id: Uuid::parse_str("fc09b908-1234-5678-abcd-ef1234567890").unwrap(),
            task_name: "Implement user authentication".to_string(),
            status: ProgressEntryStatus::Done,
            files_changed: vec!["src/auth.rs".to_string(), "src/lib.rs".to_string()],
            learnings: vec![
                "Pattern: Use bcrypt for password hashing".to_string(),
                "Gotcha: Need to handle expired tokens gracefully".to_string(),
            ],
        };

        let markdown = entry.to_markdown();
        assert!(markdown.contains("## [2024-01-15T10:30:00Z]"));
        assert!(markdown.contains("Task [fc09b908]"));
        assert!(markdown.contains("Implement user authentication"));
        assert!(markdown.contains("Status: done"));
        assert!(markdown.contains("src/auth.rs, src/lib.rs"));
        assert!(markdown.contains("Use bcrypt for password hashing"));
        assert!(markdown.contains("---"));
    }

    #[test]
    fn test_progress_entry_roundtrip() {
        let entry = ProgressEntry {
            timestamp: DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
                .unwrap()
                .with_timezone(&Utc),
            task_id: Uuid::parse_str("fc09b908-0000-0000-0000-000000000000").unwrap(),
            task_name: "Test task".to_string(),
            status: ProgressEntryStatus::Failed,
            files_changed: vec!["test.rs".to_string()],
            learnings: vec!["Learning 1".to_string()],
        };

        let markdown = entry.to_markdown();
        let parsed = ProgressEntry::from_markdown(&markdown).unwrap();

        assert_eq!(parsed.timestamp, entry.timestamp);
        assert_eq!(parsed.task_name, entry.task_name);
        assert_eq!(parsed.status, entry.status);
        assert_eq!(parsed.files_changed, entry.files_changed);
        assert_eq!(parsed.learnings, entry.learnings);
    }

    #[test]
    fn test_progress_log_append_and_read() {
        let dir = tempdir().unwrap();
        let log = ProgressLog::new(dir.path());

        // Initially no entries
        assert!(!log.exists());
        assert_eq!(log.read_all_entries().unwrap().len(), 0);

        // Append first entry
        let entry1 = ProgressEntry::new(
            Uuid::new_v4(),
            "Task 1".to_string(),
            ProgressEntryStatus::Done,
            vec!["file1.rs".to_string()],
            vec!["Learning A".to_string()],
        );
        log.append_entry(&entry1).unwrap();

        assert!(log.exists());
        let entries = log.read_all_entries().unwrap();
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].task_name, "Task 1");

        // Append second entry
        let entry2 = ProgressEntry::new(
            Uuid::new_v4(),
            "Task 2".to_string(),
            ProgressEntryStatus::Failed,
            vec!["file2.rs".to_string()],
            vec!["Learning B".to_string()],
        );
        log.append_entry(&entry2).unwrap();

        let entries = log.read_all_entries().unwrap();
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[1].task_name, "Task 2");
    }

    #[test]
    fn test_progress_log_read_recent() {
        let dir = tempdir().unwrap();
        let log = ProgressLog::new(dir.path());

        // Append 5 entries
        for i in 1..=5 {
            let entry = ProgressEntry::new(
                Uuid::new_v4(),
                format!("Task {}", i),
                ProgressEntryStatus::Done,
                vec![],
                vec![],
            );
            log.append_entry(&entry).unwrap();
        }

        // Read only last 3
        let recent = log.read_recent_entries(3).unwrap();
        assert_eq!(recent.len(), 3);
        assert_eq!(recent[0].task_name, "Task 3");
        assert_eq!(recent[1].task_name, "Task 4");
        assert_eq!(recent[2].task_name, "Task 5");
    }

    #[test]
    fn test_progress_log_format_for_prompt() {
        let dir = tempdir().unwrap();
        let log = ProgressLog::new(dir.path());

        let entry = ProgressEntry::new(
            Uuid::new_v4(),
            "Important task".to_string(),
            ProgressEntryStatus::Done,
            vec!["src/main.rs".to_string()],
            vec!["Key insight".to_string()],
        );
        log.append_entry(&entry).unwrap();

        let prompt = log.format_for_prompt(10).unwrap();
        assert!(prompt.contains("## Previous Task Progress"));
        assert!(prompt.contains("Important task"));
        assert!(prompt.contains("Key insight"));
    }

    #[test]
    fn test_progress_entry_status_display() {
        assert_eq!(format!("{}", ProgressEntryStatus::Done), "done");
        assert_eq!(format!("{}", ProgressEntryStatus::Failed), "failed");
    }

    #[test]
    fn test_progress_entry_status_parse() {
        assert_eq!(
            "done".parse::<ProgressEntryStatus>().unwrap(),
            ProgressEntryStatus::Done
        );
        assert_eq!(
            "completed".parse::<ProgressEntryStatus>().unwrap(),
            ProgressEntryStatus::Done
        );
        assert_eq!(
            "failed".parse::<ProgressEntryStatus>().unwrap(),
            ProgressEntryStatus::Failed
        );
        assert_eq!(
            "error".parse::<ProgressEntryStatus>().unwrap(),
            ProgressEntryStatus::Failed
        );
        assert!("unknown".parse::<ProgressEntryStatus>().is_err());
    }

    #[test]
    fn test_empty_files_and_learnings() {
        let entry = ProgressEntry::new(
            Uuid::new_v4(),
            "Empty task".to_string(),
            ProgressEntryStatus::Done,
            vec![],
            vec![],
        );

        let markdown = entry.to_markdown();
        assert!(markdown.contains("Files changed: (none)"));
        assert!(markdown.contains("(none recorded)"));
    }

    #[test]
    fn test_progress_log_file_header() {
        let dir = tempdir().unwrap();
        let log = ProgressLog::new(dir.path());

        let entry = ProgressEntry::new(
            Uuid::new_v4(),
            "Test".to_string(),
            ProgressEntryStatus::Done,
            vec![],
            vec![],
        );
        log.append_entry(&entry).unwrap();

        let content = fs::read_to_string(log.path()).unwrap();
        assert!(content.starts_with("# progress.log\n"));
        assert!(content.contains("Auto-generated by Makima"));
    }

    #[test]
    fn test_append_progress_entry_helper() {
        let dir = tempdir().unwrap();

        append_progress_entry(
            dir.path(),
            Uuid::new_v4(),
            "Helper test".to_string(),
            ProgressEntryStatus::Done,
            vec!["file.rs".to_string()],
            vec!["A learning".to_string()],
        )
        .unwrap();

        let log = ProgressLog::new(dir.path());
        let entries = log.read_all_entries().unwrap();
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].task_name, "Helper test");
    }
}
