//! Typed read-only password-store operations.

use std::{collections::BTreeMap, error::Error, fmt, path::Path};

use regex::bytes::{Regex, RegexBuilder};

use crate::{
    command::{EXIT_FAILURE, GrepRequest, Presentation, ShowRequest},
    crypto::{CryptoError, KeyStore, SecretProvider},
    repository::{
        DirectoryPath, EntryPath, Repository, RepositoryError, RepositorySnapshot, ResolvedObject,
        SecretBytes,
    },
};

const EXTENSIONS_DIRECTORY: &str = ".extensions";

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum TreeNodeKind {
    Directory,
    Entry,
}

/// Stable storage-owned identity for a navigation tree object.
///
/// Frontends retain this value across refreshed [`TreeModel`] instances
/// instead of reconstructing identity from rendered labels.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum TreeNodeId {
    Directory(DirectoryPath),
    Entry(EntryPath),
}

impl TreeNodeId {
    pub fn path(&self) -> &Path {
        match self {
            Self::Directory(path) => path.as_path(),
            Self::Entry(path) => path.as_path(),
        }
    }

    pub fn kind(&self) -> TreeNodeKind {
        match self {
            Self::Directory(_) => TreeNodeKind::Directory,
            Self::Entry(_) => TreeNodeKind::Entry,
        }
    }

    pub fn is_directory(&self) -> bool {
        matches!(self, Self::Directory(_))
    }

    pub fn entry(&self) -> Option<&EntryPath> {
        match self {
            Self::Entry(path) => Some(path),
            Self::Directory(_) => None,
        }
    }
}

/// Presentation-safe state calculated by storage services for a tree object.
/// Frontends render these flags and never infer them from names or paths.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct TreeNodeIndicators {
    locked: bool,
    changed: bool,
    conflict: bool,
}

impl TreeNodeIndicators {
    pub const fn new(locked: bool, changed: bool, conflict: bool) -> Self {
        Self {
            locked,
            changed,
            conflict,
        }
    }

    pub const fn is_locked(self) -> bool {
        self.locked
    }

    pub const fn is_changed(self) -> bool {
        self.changed
    }

    pub const fn has_conflict(self) -> bool {
        self.conflict
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeNode {
    name: String,
    id: TreeNodeId,
    indicators: TreeNodeIndicators,
    children: Vec<TreeNode>,
}

impl TreeNode {
    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn path(&self) -> &str {
        self.id
            .path()
            .to_str()
            .expect("tree construction rejects non-UTF-8 paths")
    }

    pub fn kind(&self) -> TreeNodeKind {
        self.id.kind()
    }

    pub fn id(&self) -> &TreeNodeId {
        &self.id
    }

    pub fn indicators(&self) -> TreeNodeIndicators {
        self.indicators
    }

    pub fn children(&self) -> &[TreeNode] {
        &self.children
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeModel {
    title: String,
    root: DirectoryPath,
    children: Vec<TreeNode>,
}

impl TreeModel {
    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn root(&self) -> &DirectoryPath {
        &self.root
    }

    pub fn children(&self) -> &[TreeNode] {
        &self.children
    }

    /// Render the stable, uncolored tree used by the CLI adapter.
    pub fn render_plain(&self) -> String {
        let mut rendered = String::new();
        rendered.push_str(&self.title);
        rendered.push('\n');
        render_children(&mut rendered, &self.children, "");
        rendered
    }
}

pub enum ShowResult {
    Entry(SecretBytes),
    Directory(TreeModel),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PresentationChannel {
    Clipboard,
    QrCode,
}

pub struct PresentationSecret {
    entry: EntryPath,
    line: usize,
    channel: PresentationChannel,
    contents: SecretBytes,
}

impl PresentationSecret {
    pub fn entry(&self) -> &EntryPath {
        &self.entry
    }

    pub fn line(&self) -> usize {
        self.line
    }

    pub fn channel(&self) -> PresentationChannel {
        self.channel
    }

    pub fn contents(&self) -> &SecretBytes {
        &self.contents
    }
}

impl fmt::Debug for PresentationSecret {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("PresentationSecret")
            .field("entry", &self.entry)
            .field("line", &self.line)
            .field("channel", &self.channel)
            .field("contents", &self.contents)
            .finish()
    }
}

pub enum ShowOutput {
    Display(ShowResult),
    Present(PresentationSecret),
}

impl fmt::Debug for ShowOutput {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Display(result) => formatter.debug_tuple("Display").field(result).finish(),
            Self::Present(secret) => formatter.debug_tuple("Present").field(secret).finish(),
        }
    }
}

impl fmt::Debug for ShowResult {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Entry(secret) => formatter.debug_tuple("Entry").field(secret).finish(),
            Self::Directory(tree) => formatter.debug_tuple("Directory").field(tree).finish(),
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NameMatch {
    id: TreeNodeId,
}

impl NameMatch {
    pub fn path(&self) -> &str {
        self.id
            .path()
            .to_str()
            .expect("search construction rejects non-UTF-8 paths")
    }

    pub fn kind(&self) -> TreeNodeKind {
        self.id.kind()
    }

    pub fn id(&self) -> &TreeNodeId {
        &self.id
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FindResults {
    terms: Vec<String>,
    matches: Vec<NameMatch>,
    tree: TreeModel,
}

impl FindResults {
    pub fn terms(&self) -> &[String] {
        &self.terms
    }

    pub fn matches(&self) -> &[NameMatch] {
        &self.matches
    }

    pub fn tree(&self) -> &TreeModel {
        &self.tree
    }

    pub fn render_plain(&self) -> String {
        format!(
            "Search Terms: {}\n{}",
            self.terms.join(","),
            self.tree.render_plain()
        )
    }
}

pub struct GrepLine {
    number: usize,
    contents: SecretBytes,
}

impl GrepLine {
    pub fn number(&self) -> usize {
        self.number
    }

    pub fn contents(&self) -> &SecretBytes {
        &self.contents
    }
}

impl fmt::Debug for GrepLine {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("GrepLine")
            .field("number", &self.number)
            .field("contents", &self.contents)
            .finish()
    }
}

pub struct GrepEntry {
    path: EntryPath,
    lines: Vec<GrepLine>,
}

impl GrepEntry {
    pub fn path(&self) -> &EntryPath {
        &self.path
    }

    pub fn lines(&self) -> &[GrepLine] {
        &self.lines
    }
}

impl fmt::Debug for GrepEntry {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("GrepEntry")
            .field("path", &self.path)
            .field("line_count", &self.lines.len())
            .finish()
    }
}

#[derive(Default)]
pub struct GrepResults {
    entries: Vec<GrepEntry>,
    line_numbers: bool,
}

impl GrepResults {
    pub fn entries(&self) -> &[GrepEntry] {
        &self.entries
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn includes_line_numbers(&self) -> bool {
        self.line_numbers
    }

    /// Render matched bytes without requiring the CLI to reconstruct entry semantics.
    pub fn render_plain(&self) -> SecretBytes {
        let mut rendered = Vec::new();
        for entry in &self.entries {
            rendered.extend_from_slice(entry.path.to_string().as_bytes());
            rendered.extend_from_slice(b":\n");
            for line in &entry.lines {
                if self.line_numbers {
                    rendered.extend_from_slice(line.number.to_string().as_bytes());
                    rendered.push(b':');
                }
                rendered.extend_from_slice(line.contents.expose());
                rendered.push(b'\n');
            }
        }
        SecretBytes::new(rendered)
    }
}

impl fmt::Debug for GrepResults {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("GrepResults")
            .field("entry_count", &self.entries.len())
            .finish()
    }
}

pub struct VaultReader<'a> {
    repository: &'a Repository,
    keys: &'a KeyStore,
}

impl<'a> VaultReader<'a> {
    pub fn new(repository: &'a Repository, keys: &'a KeyStore) -> Self {
        Self { repository, keys }
    }

    pub fn list(&self, directory: &DirectoryPath) -> Result<TreeModel, ReadError> {
        list_tree(self.repository, directory)
    }

    /// Implement explicit or implicit show dispatch. No path means the root tree; a directory
    /// produces a tree and an entry is decrypted in full.
    pub fn show(
        &self,
        input: Option<&str>,
        provider: &mut impl SecretProvider,
    ) -> Result<ShowResult, ReadError> {
        let snapshot = self.repository.snapshot()?;
        let Some(input) = input else {
            return Ok(ShowResult::Directory(build_tree(
                &snapshot,
                &DirectoryPath::root(),
                None,
            )?));
        };
        match snapshot.resolve(input)? {
            ResolvedObject::Entry(entry) => {
                let ciphertext = self.repository.read_entry(entry.path())?;
                Ok(ShowResult::Entry(self.keys.decrypt(&ciphertext, provider)?))
            }
            ResolvedObject::Directory(directory) => Ok(ShowResult::Directory(build_tree(
                &snapshot,
                directory.path(),
                None,
            )?)),
        }
    }

    /// Execute the typed show request without inspecting a rendered command or display string.
    pub fn execute_show(
        &self,
        request: &ShowRequest,
        provider: &mut impl SecretProvider,
    ) -> Result<ShowOutput, ReadError> {
        let Some(input) = request.entry.as_deref() else {
            return self.show(None, provider).map(ShowOutput::Display);
        };
        let snapshot = self.repository.snapshot()?;
        match snapshot.resolve(input)? {
            ResolvedObject::Directory(_) => {
                self.show(Some(input), provider).map(ShowOutput::Display)
            }
            ResolvedObject::Entry(entry) => match request.presentation {
                Presentation::Terminal => self.show(Some(input), provider).map(ShowOutput::Display),
                Presentation::Clipboard { line } => Ok(ShowOutput::Present(PresentationSecret {
                    entry: entry.path().clone(),
                    line: line.get(),
                    channel: PresentationChannel::Clipboard,
                    contents: self.select_line(entry.path(), line.get(), provider)?,
                })),
                Presentation::QrCode { line } => Ok(ShowOutput::Present(PresentationSecret {
                    entry: entry.path().clone(),
                    line: line.get(),
                    channel: PresentationChannel::QrCode,
                    contents: self.select_line(entry.path(), line.get(), provider)?,
                })),
            },
        }
    }

    pub fn select_line(
        &self,
        entry: &EntryPath,
        line: usize,
        provider: &mut impl SecretProvider,
    ) -> Result<SecretBytes, ReadError> {
        if line == 0 {
            return Err(ReadError::LineNotFound {
                entry: entry.clone(),
                line,
            });
        }
        let ciphertext = self.repository.read_entry(entry)?;
        let plaintext = self.keys.decrypt(&ciphertext, provider)?;
        let selected = plaintext
            .expose()
            .split(|byte| *byte == b'\n')
            .nth(line - 1)
            .filter(|selected| !selected.is_empty())
            .ok_or_else(|| ReadError::LineNotFound {
                entry: entry.clone(),
                line,
            })?;
        Ok(SecretBytes::new(selected.to_vec()))
    }

    pub fn find(&self, terms: &[String]) -> Result<FindResults, ReadError> {
        if terms.is_empty() {
            return Err(ReadError::MissingSearchTerms);
        }
        let folded = terms
            .iter()
            .map(|term| term.to_lowercase())
            .collect::<Vec<_>>();
        let snapshot = self.repository.snapshot()?;
        let mut matches = Vec::new();
        for directory in snapshot.directories() {
            if directory.path().as_path().as_os_str().is_empty()
                || hidden_path(directory.path().as_path())
            {
                continue;
            }
            let name = display_name(directory.path().as_path())?;
            if folded.iter().any(|term| name.to_lowercase().contains(term)) {
                matches.push(NameMatch {
                    id: TreeNodeId::Directory(directory.path().clone()),
                });
            }
        }
        for entry in snapshot.entries() {
            if hidden_path(entry.path().as_path()) {
                continue;
            }
            let name = display_name(entry.path().as_path())?;
            if folded.iter().any(|term| name.to_lowercase().contains(term)) {
                matches.push(NameMatch {
                    id: TreeNodeId::Entry(entry.path().clone()),
                });
            }
        }
        matches.sort_by(|left, right| left.path().cmp(right.path()));
        let included = matches.iter().map(NameMatch::path).collect::<Vec<_>>();
        let tree = build_tree(&snapshot, &DirectoryPath::root(), Some(included.as_slice()))?;
        Ok(FindResults {
            terms: terms.to_vec(),
            matches,
            tree,
        })
    }

    pub fn grep(
        &self,
        request: &GrepRequest,
        provider: &mut impl SecretProvider,
    ) -> Result<GrepResults, ReadError> {
        let regex = build_regex(request)?;
        let snapshot = self.repository.snapshot()?;
        let mut results = Vec::new();
        for record in snapshot.entries() {
            if hidden_path(record.path().as_path()) {
                continue;
            }
            let ciphertext = self.repository.read_entry(record.path())?;
            let plaintext = self.keys.decrypt(&ciphertext, provider)?;
            let mut lines = Vec::new();
            let mut plaintext_lines = plaintext
                .expose()
                .split(|byte| *byte == b'\n')
                .collect::<Vec<_>>();
            if plaintext.expose().ends_with(b"\n") {
                plaintext_lines.pop();
            }
            for (index, line) in plaintext_lines.into_iter().enumerate() {
                let matched = regex.is_match(line);
                if matched != request.invert_match {
                    lines.push(GrepLine {
                        number: index + 1,
                        contents: SecretBytes::new(line.to_vec()),
                    });
                }
            }
            if !lines.is_empty() {
                results.push(GrepEntry {
                    path: record.path().clone(),
                    lines,
                });
            }
        }
        Ok(GrepResults {
            entries: results,
            line_numbers: request.line_number,
        })
    }
}

/// Build a locked navigation model without loading key material or decrypting entries.
pub(crate) fn list_tree(
    repository: &Repository,
    directory: &DirectoryPath,
) -> Result<TreeModel, ReadError> {
    let snapshot = repository.snapshot()?;
    build_tree(&snapshot, directory, None)
}

fn build_regex(request: &GrepRequest) -> Result<Regex, ReadError> {
    let pattern = if request.fixed_strings {
        regex::escape(&request.pattern)
    } else {
        request.pattern.clone()
    };
    RegexBuilder::new(&pattern)
        .case_insensitive(request.ignore_case)
        .build()
        .map_err(|_| ReadError::InvalidRegex)
}

#[derive(Default)]
struct MutableNode {
    directory: bool,
    children: BTreeMap<String, MutableNode>,
}

fn build_tree(
    snapshot: &RepositorySnapshot,
    root: &DirectoryPath,
    included: Option<&[&str]>,
) -> Result<TreeModel, ReadError> {
    if snapshot
        .directories()
        .all(|directory| directory.path() != root)
    {
        return Err(ReadError::Repository(RepositoryError::NotFound {
            path: root.as_path().to_owned(),
        }));
    }
    let mut mutable = MutableNode {
        directory: true,
        children: BTreeMap::new(),
    };
    for directory in snapshot.directories() {
        let path = directory.path().as_path();
        if path == root.as_path() || !path.starts_with(root.as_path()) || hidden_path(path) {
            continue;
        }
        if included.is_some_and(|included| !is_included_path(path, included, true)) {
            continue;
        }
        insert_path(
            &mut mutable,
            path.strip_prefix(root.as_path()).expect("prefix"),
            true,
        )?;
    }
    for entry in snapshot.entries() {
        let path = entry.path().as_path();
        if !path.starts_with(root.as_path()) || hidden_path(path) {
            continue;
        }
        if included.is_some_and(|included| !is_included_path(path, included, false)) {
            continue;
        }
        insert_path(
            &mut mutable,
            path.strip_prefix(root.as_path()).expect("prefix"),
            false,
        )?;
    }
    let title = if root.as_path().as_os_str().is_empty() {
        "Password Store".to_owned()
    } else {
        path_text(root.as_path())?
    };
    Ok(TreeModel {
        title,
        root: root.clone(),
        children: finalize_children(mutable, root.as_path())?,
    })
}

fn insert_path(root: &mut MutableNode, path: &Path, directory: bool) -> Result<(), ReadError> {
    let components = path
        .components()
        .map(|component| {
            component
                .as_os_str()
                .to_str()
                .map(str::to_owned)
                .ok_or(ReadError::NonUtf8Path)
        })
        .collect::<Result<Vec<_>, _>>()?;
    let mut node = root;
    for (index, component) in components.iter().enumerate() {
        node = node.children.entry(component.clone()).or_default();
        if index + 1 < components.len() || directory {
            node.directory = true;
        }
    }
    Ok(())
}

fn finalize_children(node: MutableNode, parent: &Path) -> Result<Vec<TreeNode>, ReadError> {
    node.children
        .into_iter()
        .map(|(name, child)| {
            let path = parent.join(&name);
            let kind = if child.directory {
                TreeNodeKind::Directory
            } else {
                TreeNodeKind::Entry
            };
            let children = finalize_children(child, &path)?;
            Ok(TreeNode {
                name,
                id: match kind {
                    TreeNodeKind::Directory => TreeNodeId::Directory(DirectoryPath::parse(&path)?),
                    TreeNodeKind::Entry => TreeNodeId::Entry(EntryPath::parse(&path)?),
                },
                indicators: TreeNodeIndicators::default(),
                children,
            })
        })
        .collect()
}

fn render_children(rendered: &mut String, children: &[TreeNode], prefix: &str) {
    for (index, child) in children.iter().enumerate() {
        let last = index + 1 == children.len();
        rendered.push_str(prefix);
        rendered.push_str(if last { "└── " } else { "├── " });
        rendered.push_str(child.name());
        rendered.push('\n');
        let continuation = format!("{prefix}{}", if last { "    " } else { "│   " });
        render_children(rendered, child.children(), &continuation);
    }
}

fn is_included_path(path: &Path, included: &[&str], directory: bool) -> bool {
    let text = path.to_string_lossy();
    included.iter().any(|candidate| {
        let candidate = Path::new(candidate);
        candidate == path
            || (directory && candidate.starts_with(path))
            || path.starts_with(candidate)
    }) || included.iter().any(|candidate| *candidate == text)
}

pub(crate) fn hidden_path(path: &Path) -> bool {
    path.components()
        .any(|component| component.as_os_str() == EXTENSIONS_DIRECTORY)
}

fn display_name(path: &Path) -> Result<&str, ReadError> {
    path.file_name()
        .and_then(|name| name.to_str())
        .ok_or(ReadError::NonUtf8Path)
}

fn path_text(path: &Path) -> Result<String, ReadError> {
    path.to_str()
        .map(str::to_owned)
        .ok_or(ReadError::NonUtf8Path)
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReadError {
    Repository(RepositoryError),
    Crypto(CryptoError),
    MissingSearchTerms,
    InvalidRegex,
    NonUtf8Path,
    LineNotFound { entry: EntryPath, line: usize },
}

impl fmt::Display for ReadError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Repository(error) => error.fmt(formatter),
            Self::Crypto(error) => error.fmt(formatter),
            Self::MissingSearchTerms => formatter.write_str("at least one search term is required"),
            Self::InvalidRegex => formatter.write_str("decrypted grep pattern is invalid"),
            Self::NonUtf8Path => formatter.write_str("password-store path is not valid UTF-8"),
            Self::LineNotFound { entry, line } => {
                write!(formatter, "entry {entry} has no nonempty line {line}")
            }
        }
    }
}

impl Error for ReadError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Repository(error) => Some(error),
            Self::Crypto(error) => Some(error),
            _ => None,
        }
    }
}

impl From<RepositoryError> for ReadError {
    fn from(error: RepositoryError) -> Self {
        Self::Repository(error)
    }
}

impl From<CryptoError> for ReadError {
    fn from(error: CryptoError) -> Self {
        Self::Crypto(error)
    }
}

impl ReadError {
    pub fn exit_code(&self) -> u8 {
        EXIT_FAILURE
    }
}
