//! Per-connection long-term memory for the chat agent (F1).
//!
//! Stored as a markdown file at `<app_data_dir>/memory/<connection_id>.md`.
//! The agent appends notes via the `remember` tool; the user can view and edit
//! the file in the Memory sidebar tab. The same content is injected into the
//! LEARNED NOTES section of the system prompt every turn.

use crate::error::{TuskError, TuskResult};
use chrono::Utc;
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Manager};

/// Soft cap on memory file size. Overflow drops oldest `## ts` blocks until fits.
pub const MEMORY_BYTE_CAP: usize = 16 * 1024;

pub(crate) fn get_memory_path(
    app: &AppHandle,
    connection_id: &str,
) -> TuskResult<PathBuf> {
    let dir = app
        .path()
        .app_data_dir()
        .map_err(|e| TuskError::Config(e.to_string()))?
        .join("memory");
    fs::create_dir_all(&dir)?;
    let safe = sanitize_connection_id(connection_id);
    Ok(dir.join(format!("{}.md", safe)))
}

fn sanitize_connection_id(id: &str) -> String {
    id.chars()
        .map(|c| match c {
            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => c,
            _ => '_',
        })
        .collect()
}

pub(crate) fn read_memory_core(
    app: &AppHandle,
    connection_id: &str,
) -> TuskResult<String> {
    let path = get_memory_path(app, connection_id)?;
    if !path.exists() {
        return Ok(String::new());
    }
    Ok(fs::read_to_string(&path)?)
}

pub(crate) fn write_memory_core(
    app: &AppHandle,
    connection_id: &str,
    content: &str,
) -> TuskResult<()> {
    let path = get_memory_path(app, connection_id)?;
    let trimmed = enforce_size_cap(content, MEMORY_BYTE_CAP);
    fs::write(&path, trimmed)?;
    Ok(())
}

pub(crate) fn append_memory_core(
    app: &AppHandle,
    connection_id: &str,
    note: &str,
) -> TuskResult<()> {
    let trimmed_note = note.trim();
    if trimmed_note.is_empty() {
        return Err(TuskError::Custom("remember: note must not be empty".into()));
    }

    let existing = read_memory_core(app, connection_id)?;
    let mut buf = String::new();
    if existing.is_empty() {
        buf.push_str("# Memory\n\n");
    } else {
        buf.push_str(&existing);
        if !buf.ends_with('\n') {
            buf.push('\n');
        }
        if !buf.ends_with("\n\n") {
            buf.push('\n');
        }
    }
    let ts = Utc::now().format("%Y-%m-%dT%H:%M:%SZ");
    buf.push_str(&format!("## {}\n{}\n", ts, trimmed_note));

    let final_content = enforce_size_cap(&buf, MEMORY_BYTE_CAP);
    let path = get_memory_path(app, connection_id)?;
    fs::write(&path, final_content)?;
    Ok(())
}

/// Trim the file from the *oldest* note (top) until it fits within `cap` bytes.
/// Always preserves the trailing notes (the most recent observations). Keeps
/// the leading `# Memory\n\n` header if present.
pub(crate) fn enforce_size_cap(content: &str, cap: usize) -> String {
    if content.len() <= cap {
        return content.to_string();
    }

    let header = if content.starts_with("# Memory") {
        match content.find("\n## ") {
            Some(pos) => &content[..pos + 1],
            None => "# Memory\n\n",
        }
    } else {
        ""
    };

    // Split into note blocks by "\n## " marker.
    // First block (after header) might lack the leading "## " — handle uniformly.
    let body_start = header.len();
    let body = &content[body_start..];

    let mut blocks: Vec<&str> = Vec::new();
    let mut idx = 0;
    while idx < body.len() {
        // Find the next "\n## " starting at idx; if not found, the rest is one block.
        let rel = body[idx..].find("\n## ");
        match rel {
            Some(r) => {
                blocks.push(&body[idx..idx + r + 1]); // include trailing newline before next block
                idx = idx + r + 1; // start of "## "
            }
            None => {
                blocks.push(&body[idx..]);
                break;
            }
        }
    }

    // Drop blocks from the front until total fits.
    let mut current_size: usize = header.len() + blocks.iter().map(|b| b.len()).sum::<usize>();
    let mut start = 0usize;
    while current_size > cap && start < blocks.len() {
        current_size -= blocks[start].len();
        start += 1;
    }

    let mut out = String::with_capacity(current_size);
    out.push_str(header);
    for b in &blocks[start..] {
        out.push_str(b);
    }
    out
}

/// Best-effort delete of a connection's memory file. Returns Ok(()) when the
/// file doesn't exist; only surfaces an error for actual filesystem failures.
/// Used by delete_connection to keep memory/ from filling up with orphan files.
pub(crate) fn delete_memory_core(
    app: &AppHandle,
    connection_id: &str,
) -> TuskResult<()> {
    let path = get_memory_path(app, connection_id)?;
    if !path.exists() {
        return Ok(());
    }
    fs::remove_file(&path)?;
    Ok(())
}

#[tauri::command]
pub async fn get_memory(app: AppHandle, connection_id: String) -> TuskResult<String> {
    read_memory_core(&app, &connection_id)
}

#[tauri::command]
pub async fn save_memory(
    app: AppHandle,
    connection_id: String,
    content: String,
) -> TuskResult<()> {
    write_memory_core(&app, &connection_id, &content)
}

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

    #[test]
    fn cap_passthrough_under_limit() {
        let small = "# Memory\n\n## 2026-01-01T00:00:00Z\nshort note\n";
        assert_eq!(enforce_size_cap(small, MEMORY_BYTE_CAP), small);
    }

    #[test]
    fn cap_drops_oldest_blocks() {
        // 3 blocks of ~6KB each -> 18KB total > 16KB cap
        let block_body = "x".repeat(6000);
        let content = format!(
            "# Memory\n\n## 2026-01-01T00:00:00Z\n{body}\n## 2026-02-01T00:00:00Z\n{body}\n## 2026-03-01T00:00:00Z\n{body}\n",
            body = block_body
        );
        assert!(content.len() > MEMORY_BYTE_CAP);
        let trimmed = enforce_size_cap(&content, MEMORY_BYTE_CAP);
        assert!(trimmed.len() <= MEMORY_BYTE_CAP);
        // Most recent block must survive.
        assert!(trimmed.contains("2026-03-01T00:00:00Z"));
        // Oldest must be dropped.
        assert!(!trimmed.contains("2026-01-01T00:00:00Z"));
        // Header preserved.
        assert!(trimmed.starts_with("# Memory"));
    }

    #[test]
    fn cap_keeps_only_latest_when_single_block_huge() {
        let block_body = "y".repeat(20_000);
        let content = format!(
            "# Memory\n\n## 2026-01-01T00:00:00Z\n{}\n",
            block_body
        );
        let trimmed = enforce_size_cap(&content, MEMORY_BYTE_CAP);
        // Even after dropping that single block we keep at least the header,
        // so the result is just the header (or close to it).
        assert!(trimmed.starts_with("# Memory"));
        assert!(trimmed.len() <= MEMORY_BYTE_CAP);
    }

    #[test]
    fn sanitize_strips_path_chars() {
        assert_eq!(sanitize_connection_id("abc/../etc"), "abc____etc");
        assert_eq!(
            sanitize_connection_id("cf9feefd-59ab-4a7c"),
            "cf9feefd-59ab-4a7c"
        );
    }
}
