//! Progress logging for cross-task learning.
//!
//! This module implements an append-only progress log that persists learnings,
//! patterns, and context across task iterations. Inspired by Ralph's dual-file
//! learning system.
//!
//! The progress log captures:
//! - Task completion events with status
//! - Files changed during the task
//! - Learnings discovered (patterns, gotchas)
//!
//! Format (in progress.log):
//! ```
//! ## [2026-01-24T12:45:00Z] - Task abc123: Implement feature X
//! - Status: done
//! - Files changed:
//!   - src/foo.rs
//!   - src/bar.rs
//! - **Learnings:**
//!   - Pattern: Use XYZ pattern for this type of change
//!   - Gotcha: Don't forget to update the config
//! ---
//! ```

use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Progress log filename.
pub const PROGRESS_LOG_FILENAME: &str = "progress.log";

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

impl TaskCompletionStatus {
    /// Convert to string representation.
    pub fn as_str(&self) -> &'static str {
        match self {
            TaskCompletionStatus::Done => "done",
            TaskCompletionStatus::Failed => "failed",
        }
    }
}

impl std::fmt::Display for TaskCompletionStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// A learning captured during task execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Learning {
    /// Type of learning (e.g., "Pattern", "Gotcha", "Tip").
    pub kind: String,
    /// Description of the learning.
    pub description: String,
}

impl Learning {
    /// Create a new pattern learning.
    pub fn pattern(description: impl Into<String>) -> Self {
        Self {
            kind: "Pattern".to_string(),
            description: description.into(),
        }
    }

    /// Create a new gotcha learning.
    pub fn gotcha(description: impl Into<String>) -> Self {
        Self {
            kind: "Gotcha".to_string(),
            description: description.into(),
        }
    }

    /// Create a new tip learning.
    pub fn tip(description: impl Into<String>) -> Self {
        Self {
            kind: "Tip".to_string(),
            description: description.into(),
        }
    }
}

/// An entry in the progress log.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgressLogEntry {
    /// Timestamp when the entry was created.
    pub timestamp: DateTime<Utc>,
    /// Task identifier.
    pub task_id: String,
    /// Task name/description.
    pub task_name: String,
    /// Completion status.
    pub status: TaskCompletionStatus,
    /// List of files changed during the task.
    pub files_changed: Vec<String>,
    /// Learnings captured during the task.
    pub learnings: Vec<Learning>,
}

impl ProgressLogEntry {
    /// Create a new progress log entry.
    pub fn new(
        task_id: impl Into<String>,
        task_name: impl Into<String>,
        status: TaskCompletionStatus,
    ) -> Self {
        Self {
            timestamp: Utc::now(),
            task_id: task_id.into(),
            task_name: task_name.into(),
            status,
            files_changed: Vec::new(),
            learnings: Vec::new(),
        }
    }

    /// Add files that were changed.
    pub fn with_files_changed(mut self, files: Vec<String>) -> Self {
        self.files_changed = files;
        self
    }

    /// Add learnings.
    pub fn with_learnings(mut self, learnings: Vec<Learning>) -> Self {
        self.learnings = learnings;
        self
    }

    /// Add a single learning.
    pub fn add_learning(&mut self, learning: Learning) {
        self.learnings.push(learning);
    }

    /// Serialize the entry to markdown format.
    pub fn to_markdown(&self) -> String {
        let mut md = String::new();

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

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

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

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

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

        md
    }

    /// Parse a progress log entry from markdown text.
    ///
    /// Returns None if the text does not contain a valid entry.
    pub fn from_markdown(text: &str) -> Option<Self> {
        let lines: Vec<&str> = text.lines().collect();
        if lines.is_empty() {
            return None;
        }

        // Parse header line: ## [timestamp] - Task task_id: task_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 and name
        let after_task = header.find(" - Task ")? + 8;
        let remaining = &header[after_task..];
        let colon_pos = remaining.find(": ")?;
        let task_id = remaining[..colon_pos].to_string();
        let task_name = remaining[colon_pos + 2..].to_string();

        // Parse status
        let mut status = TaskCompletionStatus::Done;
        let mut files_changed = Vec::new();
        let mut learnings = Vec::new();
        let mut in_files_section = false;
        let mut in_learnings_section = false;

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

            if trimmed.starts_with("- Status:") {
                let status_str = trimmed.trim_start_matches("- Status:").trim();
                status = if status_str.eq_ignore_ascii_case("done") {
                    TaskCompletionStatus::Done
                } else {
                    TaskCompletionStatus::Failed
                };
                in_files_section = false;
                in_learnings_section = false;
            } else if trimmed == "- Files changed:" {
                in_files_section = true;
                in_learnings_section = false;
            } else if trimmed == "- **Learnings:**" {
                in_files_section = false;
                in_learnings_section = true;
            } else if trimmed.starts_with("- ") && in_files_section {
                let file = trimmed.trim_start_matches("- ").trim();
                if file != "(none)" {
                    files_changed.push(file.to_string());
                }
            } else if trimmed.starts_with("- ") && in_learnings_section {
                let learning_text = trimmed.trim_start_matches("- ").trim();
                if learning_text != "(none)" {
                    // Parse "Kind: description" format
                    if let Some(colon_pos) = learning_text.find(": ") {
                        let kind = learning_text[..colon_pos].to_string();
                        let description = learning_text[colon_pos + 2..].to_string();
                        learnings.push(Learning { kind, description });
                    }
                }
            } else if trimmed == "---" {
                break;
            }
        }

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

/// Append a progress log entry to the progress.log file in the given worktree.
///
/// Creates the file if it doesn't exist.
pub fn append_entry(worktree_path: &Path, entry: &ProgressLogEntry) -> std::io::Result<()> {
    let log_path = worktree_path.join(PROGRESS_LOG_FILENAME);

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

    // Add a newline before the entry if the file is not empty
    let metadata = file.metadata()?;
    if metadata.len() > 0 {
        writeln!(file)?;
    }

    write!(file, "{}", entry.to_markdown())?;
    file.flush()?;

    tracing::debug!(path = %log_path.display(), task_id = %entry.task_id, "Appended progress log entry");

    Ok(())
}

/// Read the most recent N entries from the progress.log file.
///
/// Returns entries in chronological order (oldest first).
/// If the file doesn't exist or has fewer than `limit` entries, returns all available entries.
pub fn read_recent_entries(worktree_path: &Path, limit: usize) -> std::io::Result<Vec<ProgressLogEntry>> {
    let log_path = worktree_path.join(PROGRESS_LOG_FILENAME);

    if !log_path.exists() {
        return Ok(Vec::new());
    }

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

    let mut entries = Vec::new();
    let mut current_entry_lines = Vec::new();

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

        // Check if this is the start of a new entry
        if line.starts_with("## [") && !current_entry_lines.is_empty() {
            // Parse the previous entry
            let entry_text = current_entry_lines.join("\n");
            if let Some(entry) = ProgressLogEntry::from_markdown(&entry_text) {
                entries.push(entry);
            }
            current_entry_lines.clear();
        }

        current_entry_lines.push(line);
    }

    // Parse the last entry if any
    if !current_entry_lines.is_empty() {
        let entry_text = current_entry_lines.join("\n");
        if let Some(entry) = ProgressLogEntry::from_markdown(&entry_text) {
            entries.push(entry);
        }
    }

    // Return the last `limit` entries
    let start = entries.len().saturating_sub(limit);
    Ok(entries[start..].to_vec())
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_entry_creation() {
        let entry = ProgressLogEntry::new("abc123", "Implement feature X", TaskCompletionStatus::Done)
            .with_files_changed(vec!["src/foo.rs".to_string(), "src/bar.rs".to_string()])
            .with_learnings(vec![
                Learning::pattern("Use XYZ pattern for this type of change"),
                Learning::gotcha("Don't forget to update the config"),
            ]);

        assert_eq!(entry.task_id, "abc123");
        assert_eq!(entry.task_name, "Implement feature X");
        assert_eq!(entry.status, TaskCompletionStatus::Done);
        assert_eq!(entry.files_changed.len(), 2);
        assert_eq!(entry.learnings.len(), 2);
    }

    #[test]
    fn test_entry_to_markdown() {
        let mut entry = ProgressLogEntry::new("abc123", "Implement feature X", TaskCompletionStatus::Done);
        entry.files_changed = vec!["src/foo.rs".to_string(), "src/bar.rs".to_string()];
        entry.learnings = vec![
            Learning::pattern("Use XYZ pattern"),
            Learning::gotcha("Watch out for edge cases"),
        ];

        let md = entry.to_markdown();

        assert!(md.contains("## ["));
        assert!(md.contains("] - Task abc123: Implement feature X"));
        assert!(md.contains("- Status: done"));
        assert!(md.contains("- Files changed:"));
        assert!(md.contains("  - src/foo.rs"));
        assert!(md.contains("  - src/bar.rs"));
        assert!(md.contains("- **Learnings:**"));
        assert!(md.contains("  - Pattern: Use XYZ pattern"));
        assert!(md.contains("  - Gotcha: Watch out for edge cases"));
        assert!(md.contains("---"));
    }

    #[test]
    fn test_entry_roundtrip() {
        let original = ProgressLogEntry::new("task-001", "Test task", TaskCompletionStatus::Failed)
            .with_files_changed(vec!["file1.rs".to_string()])
            .with_learnings(vec![Learning::tip("Remember to test")]);

        let md = original.to_markdown();
        let parsed = ProgressLogEntry::from_markdown(&md).unwrap();

        assert_eq!(parsed.task_id, original.task_id);
        assert_eq!(parsed.task_name, original.task_name);
        assert_eq!(parsed.status, original.status);
        assert_eq!(parsed.files_changed, original.files_changed);
        assert_eq!(parsed.learnings.len(), original.learnings.len());
        assert_eq!(parsed.learnings[0].kind, original.learnings[0].kind);
        assert_eq!(parsed.learnings[0].description, original.learnings[0].description);
    }

    #[test]
    fn test_empty_entry() {
        let entry = ProgressLogEntry::new("empty-task", "Empty task", TaskCompletionStatus::Done);

        let md = entry.to_markdown();

        assert!(md.contains("- Files changed:"));
        assert!(md.contains("  - (none)"));
        assert!(md.contains("- **Learnings:**"));
        assert!(md.contains("  - (none)"));
    }

    #[test]
    fn test_status_display() {
        assert_eq!(TaskCompletionStatus::Done.as_str(), "done");
        assert_eq!(TaskCompletionStatus::Failed.as_str(), "failed");
        assert_eq!(format!("{}", TaskCompletionStatus::Done), "done");
    }

    #[test]
    fn test_learning_constructors() {
        let pattern = Learning::pattern("test pattern");
        assert_eq!(pattern.kind, "Pattern");
        assert_eq!(pattern.description, "test pattern");

        let gotcha = Learning::gotcha("test gotcha");
        assert_eq!(gotcha.kind, "Gotcha");

        let tip = Learning::tip("test tip");
        assert_eq!(tip.kind, "Tip");
    }

    #[test]
    fn test_append_and_read_entries() {
        let temp_dir = std::env::temp_dir().join(format!("progress_log_test_{}", std::process::id()));
        std::fs::create_dir_all(&temp_dir).unwrap();

        // Append first entry
        let entry1 = ProgressLogEntry::new("task-1", "First task", TaskCompletionStatus::Done)
            .with_files_changed(vec!["file1.rs".to_string()]);
        append_entry(&temp_dir, &entry1).unwrap();

        // Append second entry
        let entry2 = ProgressLogEntry::new("task-2", "Second task", TaskCompletionStatus::Failed)
            .with_learnings(vec![Learning::gotcha("Something went wrong")]);
        append_entry(&temp_dir, &entry2).unwrap();

        // Read all entries
        let entries = read_recent_entries(&temp_dir, 10).unwrap();
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0].task_id, "task-1");
        assert_eq!(entries[1].task_id, "task-2");

        // Read only last entry
        let entries = read_recent_entries(&temp_dir, 1).unwrap();
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].task_id, "task-2");

        // Cleanup
        std::fs::remove_dir_all(&temp_dir).ok();
    }

    #[test]
    fn test_read_nonexistent_file() {
        let temp_dir = std::env::temp_dir().join("nonexistent_progress_log_test");
        let entries = read_recent_entries(&temp_dir, 10).unwrap();
        assert!(entries.is_empty());
    }
}
