use std::{
    collections::{HashMap, HashSet},
    fs,
    path::{Path, PathBuf},
    sync::{Arc, RwLock},
};

use serde::{Deserialize, Serialize};

use crate::editor::{
    Position,
    buffer::{Buffer, Marker, SerializableSnapshot},
    fold::FoldState,
    selection::Selection,
};
use crate::extension::{ExtensionConfig, ExtensionManager};
use crate::prelude::*;
use crate::schema::SchemaRegistry;
use crate::views::terminal::TerminalStateStore;

pub(crate) mod detect;

const MAX_HISTORY: usize = 1000;

#[derive(Debug, Default, Serialize, Deserialize)]
struct HistoryStore {
    /// File paths relative to the project root, most-recent first.
    files: Vec<PathBuf>,
}

/// Light-weight persisted project state file (.oo/cache/project_state.yaml)
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub(crate) struct PersistedHistoryEntry {
    pub id: String,
    #[serde(default)]
    pub args: std::collections::HashMap<String, String>,
    #[serde(default)]
    pub count: Option<u32>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
struct ProjectState {
    pub last_opened_file: Option<PathBuf>,
    #[serde(default)]
    pub command_history: Vec<PersistedHistoryEntry>,
}

/// Serialisable record for one file's fold state.
#[derive(Debug, Default, Serialize, Deserialize)]
struct FileEditorState {
    folds: FoldState,
    #[serde(default)]
    lines: Vec<String>,
    #[serde(default)]
    cursor: Position,
    #[serde(default)]
    selection: Option<Selection>,
    #[serde(default)]
    scroll: usize,
    #[serde(default)]
    dirty: bool,
    #[serde(default)]
    markers: Vec<Marker>,
    #[serde(default)]
    undo_stack: Vec<SerializableSnapshot>,
    #[serde(default)]
    redo_stack: Vec<SerializableSnapshot>,
}

/// The full `.oo/editor_state.yaml` file.
/// Maps relative-path strings → per-file state.
#[derive(Debug, Default, Serialize, Deserialize)]
struct EditorStateFile {
    files: HashMap<String, FileEditorState>,
}

pub struct Project {
    /// Path to the root project directory
    pub project_path: PathBuf,
    /// Path to the project settings (aka `.oo`)
    pub(crate) project_settings_path: PathBuf,
    /// Path to the per-project persisted state file (.oo/cache/project_state.yaml)
    project_state_path: PathBuf,
    /// Last opened file for this project (cached in IDE cache)
    pub(crate) last_opened_file: Option<PathBuf>,
    /// Channel sender for debounced async saves; created by start_state_save_daemon().
    project_state_save_tx: Option<tokio::sync::mpsc::UnboundedSender<Option<PathBuf>>>,
    history: HistoryStore,
    history_path: PathBuf,
    /// Persisted command-palette history loaded from project_state.yaml
    pub(crate) command_history_persisted: Vec<PersistedHistoryEntry>,

    /// Per-file editor state (folds etc.) — loaded from and saved to disk.
    editor_state: EditorStateFile,
    editor_state_path: PathBuf,

    /// Terminal state — tab titles/commands saved to .oo/terminal_state.yaml.
    terminal_state: TerminalStateStore,
    terminal_state_path: PathBuf,

    /// Languages detected in the project (e.g. ["rust", "python"]). Filled at
    /// startup by `init_extensions`.
    pub(crate) languages: Vec<String>,

    /// Schema registry shared with extensions; packaged schemas are registered
    /// when the extension is enabled.
    pub(crate) schema_registry: Arc<RwLock<SchemaRegistry>>,

    /// Loaded WASM extensions. Initialized empty; call `init_extensions` once
    /// the async op channel is available.
    pub(crate) extension_manager: ExtensionManager,
}

impl Project {
    pub fn new(project_path: PathBuf) -> Result<Self> {
        // Canonicalize so that strip_prefix works correctly when comparing against
        // absolute paths (e.g. from terminal link detection or LSP responses).
        let project_path = project_path.canonicalize().unwrap_or(project_path);
        let project_settings_path = project_path.join(".oo");
        let cache_dir = project_settings_path.join("cache");

        let history_path = cache_dir.join("file_history.yaml");
        let editor_state_path = cache_dir.join("editor_state.yaml");
        let terminal_state_path = cache_dir.join("terminal_state.yaml");
        // Project state path in the cache dir
        let project_state_path = cache_dir.join("project_state.yaml");

        // Ensure cache dir exists and migrate any legacy files from .oo to .oo/cache
        if let Err(e) = std::fs::create_dir_all(&cache_dir) {
            eprintln!("warning: could not create cache dir {}: {}", cache_dir.display(), e);
        }
        let legacy_history = project_settings_path.join("file_history.yaml");
        if legacy_history.exists() && !history_path.exists() {
            let _ = std::fs::copy(&legacy_history, &history_path);
        }
        let legacy_editor = project_settings_path.join("editor_state.yaml");
        if legacy_editor.exists() && !editor_state_path.exists() {
            let _ = std::fs::copy(&legacy_editor, &editor_state_path);
        }
        let legacy_terminal = project_path.join(".oo/terminal_state.yaml");
        if legacy_terminal.exists() && !terminal_state_path.exists() {
            let _ = std::fs::copy(&legacy_terminal, &terminal_state_path);
        }
        let legacy_project_state = project_settings_path.join("project_state.yaml");
        if legacy_project_state.exists() && !project_state_path.exists() {
            let _ = std::fs::copy(&legacy_project_state, &project_state_path);
        }

        let history = Self::load_history(&history_path);
        let editor_state = Self::load_editor_state(&editor_state_path);
        let terminal_state = Self::load_terminal_state(&terminal_state_path);

        // Start debounced async save daemon if runtime available
        let mut project_state_save_tx: Option<tokio::sync::mpsc::UnboundedSender<Option<PathBuf>>> =
            None;
        if tokio::runtime::Handle::try_current().is_ok() {
            let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Option<PathBuf>>();
            let ps_path = project_state_path.clone();
            tokio::spawn(async move {
                use tokio::time::{Duration, timeout};
                while let Some(mut pending) = rx.recv().await {
                    while let Ok(Some(next)) = timeout(Duration::from_millis(500), rx.recv()).await
                    {
                        pending = next;
                    }
                    let mut ps = ProjectState::default();
                    // Merge with existing state (preserve command history) if present.
                    if let Ok(s) = std::fs::read_to_string(&ps_path)
                        && let Ok(existing) = serde_saphyr::from_str::<ProjectState>(&s) {
                            ps = existing;
                        }
                    ps.last_opened_file = pending.clone();
                    let path = ps_path.clone();
                    tokio::task::spawn_blocking(move || {
                        if let Some(parent) = path.parent() {
                            let _ = std::fs::create_dir_all(parent);
                        }
                        if let Ok(y) = serde_saphyr::to_string(&ps) {
                            let _ = std::fs::write(&path, y);
                        }
                    });
                }
            });
            project_state_save_tx = Some(tx);
        }

        Ok(Self {
            project_path,
            project_settings_path,
            project_state_path,
            last_opened_file: None,
            project_state_save_tx,
            history,
            history_path,
            command_history_persisted: Vec::new(),
            editor_state,
            editor_state_path,
            terminal_state,
            terminal_state_path,
            languages: Vec::new(),
            schema_registry: SchemaRegistry::shared(),
            extension_manager: ExtensionManager::empty(),
        })
    }

    /// Load extensions from the configured extensions directory.
    /// Must be called after the async op channel (`op_tx`) is available.
    /// Returns extension default configs (name, yaml) for merging into settings.
    pub(crate) fn init_extensions(
        &mut self,
        op_tx: tokio::sync::mpsc::UnboundedSender<Vec<crate::operation::Operation>>,
        settings: &crate::settings::Settings,
        ext_config: &ExtensionConfig,
    ) -> Vec<(String, String)> {
        let ext_dir = self.extensions_dir(settings);
        let langs = detect::detect_project_languages(&self.project_path);
        self.languages = langs.clone();
        let (manager, defaults) = ExtensionManager::load_all(&ext_dir, op_tx, langs, ext_config);
        self.extension_manager = manager;
        self.extension_manager.detect_project();
        defaults
    }

    fn extensions_dir(&self, settings: &crate::settings::Settings) -> PathBuf {
        let configured = settings.get::<String>("extensions.extensions_dir");
        if !configured.is_empty() {
            return PathBuf::from(configured);
        }
        // Platform default: ~/.local/share/oo/extensions/ or {FOLDERID_RoamingAppData}\oo\data
        directories::ProjectDirs::from("com", "cyloncore", "oo")
            .map(|d| d.data_dir().join("extensions"))
            .unwrap_or_else(|| PathBuf::from("extensions"))
    }

    pub(crate) fn has_git_repo(&self) -> bool {
        self.project_path.join(".git").exists()
    }

    pub(crate) fn history_entries(&self) -> &[PathBuf] {
        &self.history.files
    }

    /// Return absolute paths for all stashed buffers that have unsaved modifications.
    pub(crate) fn dirty_paths(&self) -> HashSet<PathBuf> {
        self.editor_state
            .files
            .iter()
            .filter(|(_, state)| state.dirty)
            .map(|(rel, _)| {
                self.project_path
                    .join(rel.replace('/', std::path::MAIN_SEPARATOR_STR))
            })
            .collect()
    }

    /// Record that `path` was opened. Deduplicates.
    ///
    /// Project-relative files are stored as relative paths and persisted to
    /// disk immediately. External files (outside the project root) are stored
    /// as absolute paths in the in-memory list only — they appear in the
    /// Recent Files pane for the current session but are not written to disk
    /// and are purged on exit.
    ///
    /// Always succeeds (best-effort; errors are logged).
    pub(crate) fn history_push(&mut self, path: &Path) {
        match path.strip_prefix(&self.project_path) {
            Ok(rel) => {
                let rel = rel.to_path_buf();
                // Deduplicate: remove any previous entry for this file (either form).
                self.history.files.retain(|f| f != &rel && f != path);
                self.history.files.insert(0, rel);
                self.history.files.truncate(MAX_HISTORY);
                if let Err(e) = self.save_history() {
                    log::error!("Failed to save history: {}", e);
                }
            }
            Err(_) => {
                // External file: record absolute path in-memory only.
                self.history.files.retain(|f| f != path);
                self.history.files.insert(0, path.to_path_buf());
                self.history.files.truncate(MAX_HISTORY);
                // Intentionally no disk save: external entries are session-only.
            }
        }
    }

    /// Record the last opened file for this project and schedule a debounced
    /// async save to the project state cache. This is best-effort and errors
    /// are logged.
    pub(crate) fn set_last_opened_file(&mut self, path: Option<PathBuf>) {
        self.last_opened_file = path.clone();
        if let Some(tx) = &self.project_state_save_tx {
            let _ = tx.send(path);
        } else if let Err(e) = self.save_state() {
            log::error!("Failed to save project state synchronously: {}", e);
        }
    }

    /// Synchronously write the small project state file (.oo/project_state.yaml).
    pub(crate) fn save_state(&self) -> Result<()> {
        if let Some(parent) = self.project_state_path.parent() {
            fs::create_dir_all(parent)?;
        }
        let ps = ProjectState {
            last_opened_file: self.last_opened_file.clone(),
            command_history: self.command_history_persisted.clone(),
        };
        let yaml = serde_saphyr::to_string(&ps)?;
        fs::write(&self.project_state_path, yaml)?;
        Ok(())
    }

    /// Persist command-palette history asynchronously. Accepts the registry history
    /// (VecDeque) and writes a lightweight serialisable form to `.oo/project_state.yaml`.
    pub(crate) fn persist_command_history_async(&self, history: &std::collections::VecDeque<crate::commands::HistoryEntry>) {
        let path = self.project_state_path.clone();
        let entries: Vec<PersistedHistoryEntry> = history.iter().map(|e| {
            let args = e.args.iter().map(|(k, v)| {
                let s = match v {
                    crate::commands::ArgValue::String(s) => s.clone(),
                    crate::commands::ArgValue::Bool(b) => b.to_string(),
                    crate::commands::ArgValue::Int(i) => i.to_string(),
                    crate::commands::ArgValue::FilePath(p) => p.to_string_lossy().into_owned(),
                };
                (k.clone(), s)
            }).collect::<std::collections::HashMap<String, String>>();
            PersistedHistoryEntry { id: e.id.to_string(), args, count: Some(e.count) }
        }).collect();

        tokio::task::spawn_blocking(move || {
            let mut ps = ProjectState::default();
            if let Ok(s) = std::fs::read_to_string(&path)
                && let Ok(existing) = serde_saphyr::from_str::<ProjectState>(&s) {
                    ps = existing;
                }
            ps.command_history = entries;
            if let Some(parent) = path.parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            if let Ok(y) = serde_saphyr::to_string(&ps) {
                let _ = std::fs::write(&path, y);
            }
        });
    }

    /// Load the small project state file into memory. Best-effort: missing or
    /// malformed files are ignored.
    pub fn restore_state(&mut self) {
        if let Ok(s) = fs::read_to_string(&self.project_state_path)
            && let Ok(ps) = serde_saphyr::from_str::<ProjectState>(&s) {
                self.last_opened_file = ps.last_opened_file;
                self.command_history_persisted = ps.command_history;
            }
    }

    /// Return the persisted command-palette history as a vector of (command_id, args) pairs.
    pub fn get_persisted_command_history(&self) -> Vec<(String, std::collections::HashMap<String, String>)> {
        self.command_history_persisted
            .iter()
            .map(|e| (e.id.clone(), e.args.clone()))
            .collect()
    }

    /// Return a `Buffer` for `path`.
    ///
    /// If a suspended buffer is cached, it is restored (cursor, scroll, dirty
    /// content preserved).  Otherwise the file is read from disk.
    pub(crate) fn take_buffer(&mut self, path: PathBuf) -> Result<Buffer> {
        let rel = self.rel_str(&path);
        if let Some(state) = self.editor_state.files.remove(&rel) {
            let lines = if state.dirty {
                state.lines
            } else {
                let text = fs::read_to_string(&path)?;
                if text.is_empty() {
                    vec![String::new()]
                } else {
                    let mut ls: Vec<String> = text.lines().map(|l| l.to_string()).collect();
                    if text.ends_with('\n') {
                        ls.push(String::new());
                    }
                    ls
                }
            };
            return Ok(Buffer::restore(
                path,
                lines,
                state.selection,
                state.scroll,
                state.dirty,
                state.markers,
                state.undo_stack,
                state.redo_stack,
            ));
        }
        Buffer::open(&path)
    }
    /// Stash a `Buffer` into the in-memory cache when leaving the editor.
    /// Also persists fold state to disk immediately.
    pub(crate) fn stash_buffer(&mut self, buf: Buffer, folds: FoldState) {
        let abs = match buf.path.as_ref() {
            Some(p) => p.clone(),
            None => return, // scratch buffer — nothing to stash
        };
        let rel = self.rel_str(&abs);
        let markers = buf.markers.clone();
        let cursor = buf.cursor();
        let selection = buf.selection().or({
            Some(Selection {
                anchor: cursor,
                active: cursor,
            })
        });
        let undo_stack = buf.undo_stack();
        let redo_stack = buf.redo_stack();
        let scroll = buf.scroll;
        let dirty = buf.is_dirty();
        let lines = buf.into_lines();
        let state = FileEditorState {
            folds,
            lines,
            cursor,
            selection,
            scroll,
            dirty,
            markers,
            undo_stack,
            redo_stack,
        };
        self.editor_state.files.insert(rel, state);
        let _ = self.save_editor_state();
    }

    /// Retrieve the persisted fold state for `path`, if any.
    pub(crate) fn get_fold_state(&self, path: &Path) -> FoldState {
        let rel = self.rel_str(path);
        self.editor_state
            .files
            .get(&rel)
            .map(|s| s.folds.clone())
            .unwrap_or_default()
    }

    /// Path relative to project root, forward-slash separated.
    fn rel_str(&self, path: &Path) -> String {
        path.strip_prefix(&self.project_path)
            .unwrap_or(path)
            .to_string_lossy()
            .replace('\\', "/")
    }

    fn load_history(path: &Path) -> HistoryStore {
        fs::read_to_string(path)
            .ok()
            .and_then(|s| serde_saphyr::from_str(&s).ok())
            .unwrap_or_default()
    }

    fn save_history(&self) -> Result<()> {
        if let Some(parent) = self.history_path.parent() {
            fs::create_dir_all(parent)?;
        }
        // Only persist project-relative entries. External files (absolute paths)
        // are session-only and must not be written to disk.
        let persistent = HistoryStore {
            files: self.history.files.iter().filter(|p| p.is_relative()).cloned().collect(),
        };
        let yaml = serde_saphyr::to_string(&persistent)?;
        fs::write(&self.history_path, yaml)?;
        Ok(())
    }

    fn load_editor_state(path: &Path) -> EditorStateFile {
        fs::read_to_string(path)
            .ok()
            .and_then(|s| serde_saphyr::from_str(&s).ok())
            .unwrap_or_default()
    }

    fn save_editor_state(&self) -> Result<()> {
        if let Some(parent) = self.editor_state_path.parent() {
            fs::create_dir_all(parent)?;
        }
        let yaml = serde_saphyr::to_string(&self.editor_state)?;
        fs::write(&self.editor_state_path, yaml)?;
        Ok(())
    }

    pub(crate) fn get_terminal_state(&self) -> &TerminalStateStore {
        &self.terminal_state
    }

    pub(crate) fn save_terminal_state(&mut self, state: &TerminalStateStore) {
        self.terminal_state.tabs = state.tabs.clone();
        self.terminal_state.active = state.active;
        self.terminal_state.next_id = state.next_id;
        if let Err(e) = self.persist_terminal_state() {
            log::error!("Failed to save terminal state: {}", e);
        }
    }

    fn load_terminal_state(path: &Path) -> TerminalStateStore {
        fs::read_to_string(path)
            .ok()
            .and_then(|s| serde_saphyr::from_str(&s).ok())
            .unwrap_or_default()
    }

    fn persist_terminal_state(&self) -> Result<()> {
        if let Some(parent) = self.terminal_state_path.parent() {
            fs::create_dir_all(parent)?;
        }
        let yaml = serde_saphyr::to_string(&self.terminal_state)?;
        fs::write(&self.terminal_state_path, yaml)?;
        Ok(())
    }
}
