//! Memory entity — the persistent, model-agnostic knowledge base of a project
//! (LOT A, étage 1: `.md` files as the single source of truth).
//!
//! A [`Memory`] is one Markdown note stored under `.ideai/memory/<slug>.md`. Its
//! frontmatter carries the structured metadata (a kebab-case [`MemorySlug`], a
//! human-readable description, and a [`MemoryType`]); its body is opaque
//! [`MarkdownDoc`] text. Notes cross-reference one another via `[[slug]]` wiki
//! links, scanned by [`Memory::outgoing_links`].
//!
//! The aggregated `.ideai/memory/MEMORY.md` index (one [`MemoryIndexEntry`] line
//! per note) is derived data: [`Memory::index_entry`] produces a note's row. The
//! adapter (`FsMemoryStore`) owns the on-disk YAML frontmatter and index file
//! formats; the domain stays I/O-free and format-neutral.

use serde::{Deserialize, Serialize};

use crate::error::DomainError;
use crate::markdown::MarkdownDoc;

/// A kebab-case identifier for a memory note (`[a-z0-9]` plus `-`), used both as
/// the on-disk file stem (`<slug>.md`) and as the `[[slug]]` link target.
///
/// Invariants enforced by [`MemorySlug::new`]:
/// - non-empty,
/// - only lowercase ASCII letters, ASCII digits, and `-`,
/// - therefore no uppercase, no whitespace, and no `.` (so no `..` traversal).
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MemorySlug(String);

impl MemorySlug {
    /// Builds a validated kebab-case slug.
    ///
    /// # Errors
    /// [`DomainError::InvalidSlug`] if `raw` is empty or contains any character
    /// outside `[a-z0-9-]`.
    pub fn new(raw: impl Into<String>) -> Result<Self, DomainError> {
        let raw = raw.into();
        let invalid = || DomainError::InvalidSlug { value: raw.clone() };
        if raw.is_empty() {
            return Err(invalid());
        }
        if raw
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
        {
            Ok(Self(raw))
        } else {
            Err(invalid())
        }
    }

    /// Returns the slug as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for MemorySlug {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

/// The kind of a memory note, driving how it is surfaced and prioritised.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MemoryType {
    /// A user-authored preference or instruction.
    User,
    /// Feedback captured from a prior interaction.
    Feedback,
    /// A project-level fact or decision.
    Project,
    /// A reference / external knowledge note.
    Reference,
}

/// The structured frontmatter of a memory note.
///
/// Serialised with `type` (not `kind`) as the discriminator field name, matching
/// the on-disk YAML `metadata.type` (the adapter maps the nesting). Invariant:
/// `description` is non-empty (enforced at [`Memory::new`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemoryFrontmatter {
    /// Stable kebab-case identifier (also the file stem).
    pub name: MemorySlug,
    /// Human-readable one-line description (the index hook). Non-empty.
    pub description: String,
    /// The note's kind.
    #[serde(rename = "type")]
    pub r#type: MemoryType,
}

/// A `[[slug]]` wiki link found in a note's body, pointing at another note.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MemoryLink {
    /// The linked note's slug.
    pub target: MemorySlug,
}

/// One row of the aggregated `MEMORY.md` index: the `- [Title](slug.md) — hook`
/// line, decomposed into its parts.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryIndexEntry {
    /// The note's slug.
    pub slug: MemorySlug,
    /// The note's display title (currently the slug; titles are derived later).
    pub title: String,
    /// The one-line hook (the frontmatter description).
    pub hook: String,
    /// The note's kind.
    pub r#type: MemoryType,
}

/// A memory note: validated frontmatter plus an opaque Markdown body.
///
/// Invariants enforced by [`Memory::new`]:
/// - `frontmatter.description` non-empty,
/// - `body` non-empty (an empty note carries no knowledge).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Memory {
    /// Structured metadata.
    pub frontmatter: MemoryFrontmatter,
    /// Markdown body of the note.
    pub body: MarkdownDoc,
}

impl Memory {
    /// Builds a validated memory note.
    ///
    /// # Errors
    /// - [`DomainError::EmptyField`] if `frontmatter.description` is empty,
    /// - [`DomainError::EmptyField`] if `body` is empty.
    pub fn new(frontmatter: MemoryFrontmatter, body: MarkdownDoc) -> Result<Self, DomainError> {
        crate::validation::non_empty(&frontmatter.description, "memory.description")?;
        if body.is_empty() {
            return Err(DomainError::EmptyField {
                field: "memory.body",
            });
        }
        Ok(Self { frontmatter, body })
    }

    /// Returns the note's slug (its identity).
    #[must_use]
    pub fn slug(&self) -> &MemorySlug {
        &self.frontmatter.name
    }

    /// Scans the body for `[[slug]]` wiki links, in order of appearance.
    ///
    /// Tokens whose inner text is not a valid [`MemorySlug`] are skipped (a
    /// malformed link is not a hard error here). Duplicates are preserved — the
    /// caller dedups if it wants a unique link set. No regex: a small linear scan.
    #[must_use]
    pub fn outgoing_links(&self) -> Vec<MemoryLink> {
        let text = self.body.as_str();
        let bytes = text.as_bytes();
        let mut links = Vec::new();
        let mut i = 0;
        while i + 1 < bytes.len() {
            if bytes[i] == b'[' && bytes[i + 1] == b'[' {
                // Find the closing `]]`.
                if let Some(close) = text[i + 2..].find("]]") {
                    let inner = &text[i + 2..i + 2 + close];
                    if let Ok(target) = MemorySlug::new(inner) {
                        links.push(MemoryLink { target });
                    }
                    i = i + 2 + close + 2;
                    continue;
                }
                // No closing token: stop scanning further `[[`.
                break;
            }
            i += 1;
        }
        links
    }

    /// Produces this note's row in the aggregated `MEMORY.md` index.
    #[must_use]
    pub fn index_entry(&self) -> MemoryIndexEntry {
        MemoryIndexEntry {
            slug: self.frontmatter.name.clone(),
            title: self.frontmatter.name.as_str().to_string(),
            hook: self.frontmatter.description.clone(),
            r#type: self.frontmatter.r#type,
        }
    }
}
