// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 SenWeaverCoding
// Licensed under the MIT License.

use anyhow::Result;
use chrono::Local;
use rusqlite::{Connection, params};
use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};

pub const SNAPSHOT_FILENAME: &str = "MEMORY_SNAPSHOT.md";

const SNAPSHOT_HEADER: &str = "# 🧠 SenWeaverCoding Memory Snapshot\n\n\
    > Auto-generated by SenWeaverCoding. Do not edit manually unless you know what you're doing.\n\
    > This file is the \"soul\" of your agent  -  if `brain.db` is lost, start the agent\n\
    > in this workspace and it will auto-hydrate from this file.\n\n";

pub fn export_snapshot(workspace_dir: &Path) -> Result<usize> {
    let db_path = workspace_dir.join("memory").join("brain.db");
    if !db_path.exists() {
        tracing::debug!("snapshot export skipped: brain.db does not exist");
        return Ok(0);
    }

    let conn = Connection::open(&db_path)?;
    conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;")?;

    let mut stmt = conn.prepare(
        "SELECT key, content, category, created_at, updated_at
         FROM memories
         WHERE category = 'core'
         ORDER BY updated_at DESC",
    )?;

    let rows: Vec<(String, String, String, String, String)> = stmt
        .query_map([], |row| {
            Ok((
                row.get(0)?,
                row.get(1)?,
                row.get(2)?,
                row.get(3)?,
                row.get(4)?,
            ))
        })?
        .filter_map(|r| r.ok())
        .collect();

    if rows.is_empty() {
        tracing::debug!("snapshot export: no core memories to export");
        return Ok(0);
    }

    let mut output = String::with_capacity(rows.len() * 200);
    output.push_str(SNAPSHOT_HEADER);

    let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();

    if write!(output, "**Last exported:** {now}\n\n").is_err() {
        tracing::error!("snapshot export: failed to write timestamp header");
        anyhow::bail!("Failed to write snapshot header");
    }
    if write!(output, "**Total core memories:** {}\n\n---\n\n", rows.len()).is_err() {
        tracing::error!("snapshot export: failed to write memory count");
        anyhow::bail!("Failed to write memory count");
    }

    for (key, content, _category, created_at, updated_at) in &rows {
        if write!(output, "### 🔑 `{key}`\n\n").is_err() {
            tracing::error!(key, "snapshot export: failed to write key header");
            anyhow::bail!("Failed to write key header for '{}'", key);
        }
        if write!(output, "{content}\n\n").is_err() {
            tracing::error!(key, "snapshot export: failed to write content");
            anyhow::bail!("Failed to write content for '{}'", key);
        }
        if write!(
            output,
            "*Created: {created_at} | Updated: {updated_at}*\n\n---\n\n"
        )
        .is_err()
        {
            tracing::error!(key, "snapshot export: failed to write metadata");
            anyhow::bail!("Failed to write metadata for '{}'", key);
        }
    }

    let snapshot_path = snapshot_path(workspace_dir);
    if let Err(e) = crate::util::atomic_write(&snapshot_path, output.as_bytes()) {
        tracing::error!(path = %snapshot_path.display(), error = %e, "snapshot export: failed to write file");
        anyhow::bail!("Failed to write snapshot file: {}", e);
    }

    tracing::info!(
        "📸 Memory snapshot exported: {} core memories → {}",
        rows.len(),
        snapshot_path.display()
    );

    Ok(rows.len())
}

pub fn hydrate_from_snapshot(workspace_dir: &Path) -> Result<usize> {
    let snapshot = snapshot_path(workspace_dir);
    if !snapshot.exists() {
        return Ok(0);
    }

    let content = fs::read_to_string(&snapshot)?;
    let entries = parse_snapshot(&content);

    if entries.is_empty() {
        return Ok(0);
    }

    let db_dir = workspace_dir.join("memory");
    fs::create_dir_all(&db_dir)?;

    let db_path = db_dir.join("brain.db");
    let conn = Connection::open(&db_path)?;
    conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;")?;

    conn.execute_batch(
        "CREATE TABLE IF NOT EXISTS memories (
            id            TEXT PRIMARY KEY,
            key           TEXT NOT NULL UNIQUE,
            content       TEXT NOT NULL,
            session_id    TEXT,
            category      TEXT NOT NULL DEFAULT 'core',
            embedding     BLOB,
            namespace     TEXT NOT NULL DEFAULT 'default',
            importance    REAL,
            superseded_by TEXT,
            created_at    TEXT NOT NULL,
            updated_at    TEXT NOT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_mem_key ON memories(key);
        CREATE INDEX IF NOT EXISTS idx_mem_cat ON memories(category);
        CREATE INDEX IF NOT EXISTS idx_mem_updated ON memories(updated_at);
        CREATE INDEX IF NOT EXISTS idx_mem_ns ON memories(namespace);

        CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
            USING fts5(key, content, content='memories', content_rowid='rowid');

        CREATE TABLE IF NOT EXISTS embedding_cache (
            content_hash TEXT PRIMARY KEY,
            embedding    BLOB NOT NULL,
            created_at   TEXT NOT NULL,
            accessed_at  TEXT
        );
        CREATE INDEX IF NOT EXISTS idx_cache_accessed ON embedding_cache(accessed_at);",
    )?;

    let now = Local::now().to_rfc3339();
    let mut hydrated = 0;

    for (key, content) in &entries {
        let id = uuid::Uuid::new_v4().to_string();
        let result = conn.execute(
            "INSERT OR IGNORE INTO memories (id, key, content, category, created_at, updated_at)
             VALUES (?1, ?2, ?3, 'core', ?4, ?5)",
            params![id, key, content, now, now],
        );

        match result {
            Ok(changed) if changed > 0 => {
                let rowid = conn.last_insert_rowid();
                let _ = conn.execute(
                    "INSERT INTO memories_fts(rowid, key, content) VALUES (?1, ?2, ?3)",
                    params![rowid, key, content],
                );
                hydrated += 1;
            }
            Ok(_) => {
                tracing::debug!("hydrate: key '{key}' already exists, skipping");
            }
            Err(e) => {
                tracing::warn!("hydrate: failed to insert key '{key}': {e}");
            }
        }
    }

    tracing::info!(
        "🧬 Memory hydration complete: {} entries restored from {}",
        hydrated,
        snapshot.display()
    );

    Ok(hydrated)
}

pub fn should_hydrate(workspace_dir: &Path) -> bool {
    let db_path = workspace_dir.join("memory").join("brain.db");
    let snapshot = snapshot_path(workspace_dir);

    let db_missing_or_empty = if db_path.exists() {

        fs::metadata(&db_path)
            .map(|m| m.len() < 4096)
            .unwrap_or(true)
    } else {
        true
    };

    db_missing_or_empty && snapshot.exists()
}

fn snapshot_path(workspace_dir: &Path) -> PathBuf {
    workspace_dir.join(SNAPSHOT_FILENAME)
}

fn parse_snapshot(input: &str) -> Vec<(String, String)> {
    let mut entries = Vec::new();
    let mut current_key: Option<String> = None;
    let mut current_content = String::new();

    for line in input.lines() {
        let trimmed = line.trim();

        if trimmed.starts_with("### 🔑 `") && trimmed.ends_with('`') {

            if let Some(key) = current_key.take() {
                let content = current_content.trim().to_string();
                if !content.is_empty() {
                    entries.push((key, content));
                }
            }

            let key = trimmed
                .strip_prefix("### 🔑 `")
                .and_then(|s| s.strip_suffix('`'))
                .unwrap_or("")
                .to_string();

            if !key.is_empty() {
                current_key = Some(key);
                current_content = String::new();
            }
        } else if current_key.is_some() {

            if trimmed.starts_with("*Created:") || trimmed == "---" {
                continue;
            }

            if !current_content.is_empty() || !trimmed.is_empty() {
                if !current_content.is_empty() {
                    current_content.push('\n');
                }
                current_content.push_str(line);
            }
        }
    }

    if let Some(key) = current_key {
        let content = current_content.trim().to_string();
        if !content.is_empty() {
            entries.push((key, content));
        }
    }

    entries
}
