//! In-memory issue registry for the IDE.
//!
//! [`IssueRegistry`] is the single source of truth for all issues visible to
//! the editor.  It holds ephemeral issues (produced by async tasks such as
//! build, lint, or test runners) and persistent issues (loaded from disk and
//! injected via the [`crate::operation::Operation::AddIssue`] operation at
//! startup by a separate Persistent Component).
//!
//! # Design constraints
//!
//! * **No disk I/O** — the registry is pure in-memory.
//! * **Single-threaded** — lives on the main thread as part of
//!   [`crate::app_state::AppState`].  Async tasks that need to add issues send
//!   an [`crate::operation::Operation::AddIssue`] through the existing
//!   `op_tx` channel; the main loop applies it.
//! * **Operation-driven** — all mutations arrive as `Operation` variants;
//!   after each mutation the main loop enqueues the corresponding
//!   `IssueAdded` / `IssueRemoved` / `IssueUpdated` event operation so that
//!   views can react.
//!
//! # Lifecycle
//!
//! ```text
//! IDE start
//!   Persistent Component reads disk, sends:
//!     op_tx.send(vec![Operation::AddIssue { issue: NewIssue { marker: None, … } }])
//!
//! Async task (build, lint, …) — first clear stale results:
//!     op_tx.send(vec![Operation::ClearIssuesByMarker { marker: "build".into() }])
//!   then add fresh ones:
//!     op_tx.send(vec![Operation::AddIssue { issue: NewIssue { marker: Some("build"), … } }])
//!
//! User dismisses an issue:
//!     queue.push_back(Operation::DismissIssue { id })
//!
//! IDE restart: new empty IssueRegistry — no ephemeral issues.
//! ```

use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::editor::position::Position;

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// Unique, monotonically-increasing identifier assigned by the registry.
/// Never reused within a single registry lifetime.
pub type IssueId = u64;

/// Severity level.  Ordered `Info < Warning < Error`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
    Info,
    Warning,
    Error,
    /// A TODO / FIXME / HACK comment found in task output.
    /// Ordered above `Error` so it sorts separately; not treated as a build
    /// failure.  Ephemeral only — never persisted to `.oo/issues.yaml`.
    Todo,
}

/// A single diagnostic issue.
#[derive(Debug, Clone)]
pub struct Issue {
    /// Unique identifier assigned by the registry.
    pub id: IssueId,
    /// When `Some`, the issue is *ephemeral* and belongs to this marker group.
    /// `None` means the issue is *persistent* and immune to marker clears.
    pub marker: Option<String>,
    /// Human-readable source tag, e.g. `"lsp"`, `"build"`, `"lint"`.
    pub source: String,
    /// File the issue belongs to, if applicable.
    pub path: Option<PathBuf>,
    /// Byte-column range within the file, if known.
    pub range: Option<(Position, Position)>,
    pub message: String,
    pub severity: Severity,
    pub dismissed: bool,
    pub resolved: bool,
    pub created_at: SystemTime,
}

/// Input data for the `AddIssue` operation.  The registry assigns `id`,
/// `dismissed`, `resolved`, and `created_at`.
#[derive(Debug, Clone)]
pub struct NewIssue {
    /// `Some(marker)` → ephemeral (removable via `ClearIssuesByMarker`).
    /// `None`         → persistent (never removed by marker clears).
    pub marker: Option<String>,
    pub source: String,
    pub path: Option<PathBuf>,
    pub range: Option<(Position, Position)>,
    pub message: String,
    pub severity: Severity,
}

// ---------------------------------------------------------------------------
// Registry
// ---------------------------------------------------------------------------

/// Central in-memory store for all IDE issues.
///
/// All mutations are performed through the `pub(crate)` methods called by
/// `apply_operation` in `app.rs`.  After each successful mutation a
/// corresponding `IssueAdded / IssueRemoved / IssueUpdated` operation is
/// enqueued by the caller so views can react.
pub struct IssueRegistry {
    next_id: IssueId,
    issues: HashMap<IssueId, Issue>,
    /// marker → set of associated ephemeral issue IDs.
    markers: HashMap<String, HashSet<IssueId>>,
    /// file path → set of issue IDs for fast per-file lookup.
    by_file: HashMap<PathBuf, HashSet<IssueId>>,
}

impl std::fmt::Debug for IssueRegistry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("IssueRegistry")
            .field("issue_count", &self.issues.len())
            .field("marker_count", &self.markers.len())
            .finish_non_exhaustive()
    }
}

impl Default for IssueRegistry {
    fn default() -> Self {
        Self::new()
    }
}

impl IssueRegistry {
    /// Create an empty registry.
    pub fn new() -> Self {
        Self {
            next_id: 1,
            issues: HashMap::new(),
            markers: HashMap::new(),
            by_file: HashMap::new(),
        }
    }

    // -----------------------------------------------------------------------
    // Mutations — pub(crate) so only app.rs / apply_operation can call them.
    // Callers are responsible for pushing the matching event operation.
    // -----------------------------------------------------------------------

    /// Add a new issue and return its [`IssueId`].
    ///
    /// Typically called by `apply_operation(Operation::AddIssue { .. })`,
    /// which also enqueues `Operation::IssueAdded { id }` so views can react.
    /// For direct use (e.g., integration tests), the `IssueAdded` event is not
    /// emitted automatically — callers must notify views if needed.
    pub fn add_issue(&mut self, new: NewIssue) -> IssueId {
        let id = self.next_id;
        self.next_id += 1;

        let issue = Issue {
            id,
            marker: new.marker.clone(),
            source: new.source,
            path: new.path,
            range: new.range,
            message: new.message,
            severity: new.severity,
            dismissed: false,
            resolved: false,
            created_at: SystemTime::now(),
        };

        if let Some(path) = &issue.path {
            self.by_file.entry(path.clone()).or_default().insert(id);
        }
        if let Some(m) = &new.marker {
            self.markers.entry(m.clone()).or_default().insert(id);
        }

        self.issues.insert(id, issue);
        id
    }

    /// Remove an issue by ID.  Returns `true` if it existed.
    ///
    /// Called by `apply_operation(Operation::RemoveIssue { .. })`.
    /// The caller enqueues `Operation::IssueRemoved { id }` on success.
    pub(crate) fn remove_issue(&mut self, id: IssueId) -> bool {
        let Some(issue) = self.issues.remove(&id) else { return false };
        self.remove_from_indexes(id, &issue);
        true
    }

    /// Mark an issue as resolved.  Returns `true` if it existed and was not
    /// already resolved.
    ///
    /// Called by `apply_operation(Operation::ResolveIssue { .. })`.
    /// The caller enqueues `Operation::IssueUpdated { id }` on success.
    pub(crate) fn resolve_issue(&mut self, id: IssueId) -> bool {
        let Some(issue) = self.issues.get_mut(&id) else { return false };
        if issue.resolved { return false; }
        issue.resolved = true;
        true
    }

    /// Mark an issue as dismissed.  Returns `true` if it existed and was not
    /// already dismissed.
    ///
    /// Called by `apply_operation(Operation::DismissIssue { .. })`.
    /// The caller enqueues `Operation::IssueUpdated { id }` on success.
    pub(crate) fn dismiss_issue(&mut self, id: IssueId) -> bool {
        let Some(issue) = self.issues.get_mut(&id) else { return false };
        if issue.dismissed { return false; }
        issue.dismissed = true;
        true
    }

    /// Remove all ephemeral issues belonging to `marker`.  Returns the IDs
    /// that were removed so the caller can enqueue `IssueRemoved` events.
    ///
    /// Persistent issues (`marker = None`) are never affected.
    ///
    /// Called by `apply_operation(Operation::ClearIssuesByMarker { .. })`.
    pub fn clear_by_marker(&mut self, marker: &str) -> Vec<IssueId> {
        let Some(ids) = self.markers.remove(marker) else { return Vec::new() };
        let removed: Vec<IssueId> = ids.into_iter().collect();
        for &id in &removed {
            if let Some(issue) = self.issues.remove(&id)
                && let Some(path) = &issue.path {
                    let empty = self
                        .by_file
                        .get_mut(path)
                        .map(|s| { s.remove(&id); s.is_empty() })
                        .unwrap_or(false);
                    if empty { self.by_file.remove(path); }
                }
        }
        removed
    }

    // -----------------------------------------------------------------------
    // Queries — public; used by views, editor, and extensions.
    // -----------------------------------------------------------------------

    /// All issues, sorted by ascending ID (insertion order).
    pub fn list_all(&self) -> Vec<&Issue> {
        let mut v: Vec<&Issue> = self.issues.values().collect();
        v.sort_by_key(|i| i.id);
        v
    }

    /// Issues for a specific file, sorted by ascending ID.
    pub fn list_by_file(&self, path: &Path) -> Vec<&Issue> {
        let Some(ids) = self.by_file.get(path) else { return Vec::new() };
        let mut v: Vec<&Issue> = ids.iter().filter_map(|id| self.issues.get(id)).collect();
        v.sort_by_key(|i| i.id);
        v
    }

    /// Issues with a specific severity, sorted by ascending ID.
    pub fn list_by_severity(&self, severity: Severity) -> Vec<&Issue> {
        let mut v: Vec<&Issue> = self.issues.values().filter(|i| i.severity == severity).collect();
        v.sort_by_key(|i| i.id);
        v
    }

    /// Look up a single issue by ID.
    pub fn get(&self, id: IssueId) -> Option<&Issue> {
        self.issues.get(&id)
    }

    /// Total number of issues currently held.
    pub fn len(&self) -> usize {
        self.issues.len()
    }

    /// `true` when the registry contains no issues.
    pub fn is_empty(&self) -> bool {
        self.issues.is_empty()
    }

    /// Cloned snapshot of all **persistent** issues (`marker: None`), sorted
    /// by ascending ID.
    ///
    /// Used by the Persistent Component to build a save snapshot after any
    /// mutation that affects a persistent issue.
    pub fn list_persistent(&self) -> Vec<Issue> {
        let mut v: Vec<Issue> = self
            .issues
            .values()
            .filter(|i| i.marker.is_none())
            .cloned()
            .collect();
        v.sort_by_key(|i| i.id);
        v
    }

    // -----------------------------------------------------------------------
    // Private helpers
    // -----------------------------------------------------------------------

    fn remove_from_indexes(&mut self, id: IssueId, issue: &Issue) {
        if let Some(path) = &issue.path {
            let empty = self
                .by_file
                .get_mut(path)
                .map(|s| { s.remove(&id); s.is_empty() })
                .unwrap_or(false);
            if empty { self.by_file.remove(path); }
        }
        if let Some(marker) = &issue.marker {
            let empty = self
                .markers
                .get_mut(marker.as_str())
                .map(|s| { s.remove(&id); s.is_empty() })
                .unwrap_or(false);
            if empty { self.markers.remove(marker.as_str()); }
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;

    fn make(msg: &str, sev: Severity) -> NewIssue {
        NewIssue { marker: None, source: "test".into(), path: None, range: None, message: msg.into(), severity: sev }
    }

    fn make_marked(msg: &str, marker: &str) -> NewIssue {
        NewIssue { marker: Some(marker.into()), source: "test".into(), path: None, range: None, message: msg.into(), severity: Severity::Warning }
    }

    fn make_file(msg: &str, path: &str) -> NewIssue {
        NewIssue { marker: None, source: "test".into(), path: Some(PathBuf::from(path)), range: None, message: msg.into(), severity: Severity::Warning }
    }

    // --- Basic operations ---------------------------------------------------

    #[test]
    fn new_registry_is_empty() {
        let r = IssueRegistry::new();
        assert!(r.is_empty());
        assert_eq!(r.len(), 0);
        assert!(r.list_all().is_empty());
    }

    #[test]
    fn add_returns_sequential_ids() {
        let mut r = IssueRegistry::new();
        let a = r.add_issue(make("a", Severity::Info));
        let b = r.add_issue(make("b", Severity::Warning));
        let c = r.add_issue(make("c", Severity::Error));
        assert!(a < b && b < c);
    }

    #[test]
    fn add_and_get() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make("oops", Severity::Error));
        let i = r.get(id).unwrap();
        assert_eq!(i.message, "oops");
        assert_eq!(i.severity, Severity::Error);
        assert!(!i.dismissed && !i.resolved);
        assert!(i.marker.is_none());
    }

    #[test]
    fn remove_existing_returns_true() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make("x", Severity::Info));
        assert!(r.remove_issue(id));
        assert!(r.get(id).is_none());
        assert!(r.is_empty());
    }

    #[test]
    fn remove_missing_returns_false() {
        let mut r = IssueRegistry::new();
        assert!(!r.remove_issue(99));
    }

    #[test]
    fn resolve_sets_flag() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make("x", Severity::Warning));
        assert!(r.resolve_issue(id));
        assert!(r.get(id).unwrap().resolved);
    }

    #[test]
    fn resolve_idempotent() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make("x", Severity::Info));
        assert!(r.resolve_issue(id));
        assert!(!r.resolve_issue(id));
    }

    #[test]
    fn resolve_missing_returns_false() {
        let mut r = IssueRegistry::new();
        assert!(!r.resolve_issue(42));
    }

    #[test]
    fn dismiss_sets_flag() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make("x", Severity::Info));
        assert!(r.dismiss_issue(id));
        assert!(r.get(id).unwrap().dismissed);
    }

    #[test]
    fn dismiss_idempotent() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make("x", Severity::Info));
        assert!(r.dismiss_issue(id));
        assert!(!r.dismiss_issue(id));
    }

    #[test]
    fn dismiss_missing_returns_false() {
        let mut r = IssueRegistry::new();
        assert!(!r.dismiss_issue(99));
    }

    // --- Markers and ephemeral clears ----------------------------------------

    #[test]
    fn marker_stored_on_issue() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make_marked("e", "build"));
        assert_eq!(r.get(id).unwrap().marker.as_deref(), Some("build"));
    }

    #[test]
    fn no_marker_for_persistent() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make("p", Severity::Warning));
        assert!(r.get(id).unwrap().marker.is_none());
    }

    #[test]
    fn clear_by_marker_removes_correct_issues() {
        let mut r = IssueRegistry::new();
        let a = r.add_issue(make_marked("a", "build"));
        let b = r.add_issue(make_marked("b", "build"));
        let c = r.add_issue(make_marked("c", "lint"));
        let d = r.add_issue(make("d", Severity::Info)); // persistent

        let removed = r.clear_by_marker("build");
        assert_eq!(removed.len(), 2);
        assert!(removed.contains(&a) && removed.contains(&b));
        assert!(r.get(c).is_some(), "different marker must survive");
        assert!(r.get(d).is_some(), "persistent must survive");
    }

    #[test]
    fn clear_by_marker_unknown_returns_empty() {
        let mut r = IssueRegistry::new();
        assert!(r.clear_by_marker("nope").is_empty());
    }

    #[test]
    fn clear_by_marker_cleans_internal_index() {
        let mut r = IssueRegistry::new();
        r.add_issue(make_marked("x", "m"));
        r.clear_by_marker("m");
        assert!(r.clear_by_marker("m").is_empty()); // no panic, no double-count
    }

    #[test]
    fn new_registry_has_no_ephemeral_issues() {
        assert!(IssueRegistry::new().is_empty());
    }

    // --- File index ---------------------------------------------------------

    #[test]
    fn list_by_file_filters() {
        let mut r = IssueRegistry::new();
        r.add_issue(make_file("a", "src/main.rs"));
        r.add_issue(make_file("b", "src/main.rs"));
        r.add_issue(make_file("c", "src/lib.rs"));
        let main = r.list_by_file(Path::new("src/main.rs"));
        assert_eq!(main.len(), 2);
        assert!(main.iter().all(|i| i.path.as_deref() == Some(Path::new("src/main.rs"))));
    }

    #[test]
    fn list_by_file_empty_for_unknown() {
        assert!(IssueRegistry::new().list_by_file(Path::new("x.rs")).is_empty());
    }

    #[test]
    fn file_index_cleaned_on_remove() {
        let mut r = IssueRegistry::new();
        let id = r.add_issue(make_file("x", "foo.rs"));
        r.remove_issue(id);
        assert!(r.list_by_file(Path::new("foo.rs")).is_empty());
    }

    #[test]
    fn file_index_cleaned_on_clear_by_marker() {
        let mut r = IssueRegistry::new();
        let mut ni = make_file("e", "foo.rs");
        ni.marker = Some("build".into());
        r.add_issue(ni);
        r.clear_by_marker("build");
        assert!(r.list_by_file(Path::new("foo.rs")).is_empty());
    }

    // --- Severity filter ----------------------------------------------------

    #[test]
    fn list_by_severity() {
        let mut r = IssueRegistry::new();
        r.add_issue(make("e1", Severity::Error));
        r.add_issue(make("w1", Severity::Warning));
        r.add_issue(make("e2", Severity::Error));
        assert_eq!(r.list_by_severity(Severity::Error).len(), 2);
        assert_eq!(r.list_by_severity(Severity::Info).len(), 0);
    }

    // --- list_all ordering --------------------------------------------------

    #[test]
    fn list_all_insertion_order() {
        let mut r = IssueRegistry::new();
        let id1 = r.add_issue(make("1", Severity::Info));
        let id2 = r.add_issue(make("2", Severity::Info));
        let id3 = r.add_issue(make("3", Severity::Info));
        let all = r.list_all();
        assert_eq!([all[0].id, all[1].id, all[2].id], [id1, id2, id3]);
    }

    // --- list_persistent ----------------------------------------------------

    #[test]
    fn list_persistent_excludes_ephemeral() {
        let mut r = IssueRegistry::new();
        let pid = r.add_issue(make("persistent", Severity::Warning));
        let _eid = r.add_issue(make_marked("ephemeral", "build"));
        let persistent = r.list_persistent();
        assert_eq!(persistent.len(), 1);
        assert_eq!(persistent[0].id, pid);
        assert!(persistent[0].marker.is_none());
    }

    #[test]
    fn list_persistent_empty_when_all_ephemeral() {
        let mut r = IssueRegistry::new();
        r.add_issue(make_marked("e1", "lint"));
        r.add_issue(make_marked("e2", "build"));
        assert!(r.list_persistent().is_empty());
    }

    #[test]
    fn list_persistent_sorted_by_id() {
        let mut r = IssueRegistry::new();
        let a = r.add_issue(make("a", Severity::Info));
        let b = r.add_issue(make("b", Severity::Error));
        let c = r.add_issue(make("c", Severity::Warning));
        let p = r.list_persistent();
        assert_eq!([p[0].id, p[1].id, p[2].id], [a, b, c]);
    }
}
