use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::future::Future;

/// Storage interface behind the memories tools.
///
/// Implementations should return paths relative to the memory store and enforce
/// their own storage-specific access rules. The local implementation uses the
/// filesystem today; a later implementation can satisfy the same contract from a
/// remote backend.
pub trait MemoriesBackend: Clone + Send + Sync + 'static {
    fn add_ad_hoc_note(
        &self,
        request: AddAdHocMemoryNoteRequest,
    ) -> impl Future<Output = Result<AddAdHocMemoryNoteResponse, MemoriesBackendError>> + Send;

    fn list(
        &self,
        request: ListMemoriesRequest,
    ) -> impl Future<Output = Result<ListMemoriesResponse, MemoriesBackendError>> + Send;

    fn read(
        &self,
        request: ReadMemoryRequest,
    ) -> impl Future<Output = Result<ReadMemoryResponse, MemoriesBackendError>> + Send;

    fn search(
        &self,
        request: SearchMemoriesRequest,
    ) -> impl Future<Output = Result<SearchMemoriesResponse, MemoriesBackendError>> + Send;
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddAdHocMemoryNoteRequest {
    pub filename: String,
    pub note: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct AddAdHocMemoryNoteResponse {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListMemoriesRequest {
    pub path: Option<String>,
    pub cursor: Option<String>,
    pub max_results: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ListMemoriesResponse {
    pub path: Option<String>,
    pub entries: Vec<MemoryEntry>,
    pub next_cursor: Option<String>,
    pub truncated: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadMemoryRequest {
    pub path: String,
    pub line_offset: usize,
    pub max_lines: Option<usize>,
    pub max_tokens: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ReadMemoryResponse {
    pub path: String,
    pub start_line_number: usize,
    pub content: String,
    pub truncated: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchMemoriesRequest {
    pub queries: Vec<String>,
    pub match_mode: SearchMatchMode,
    pub path: Option<String>,
    pub cursor: Option<String>,
    pub context_lines: usize,
    pub case_sensitive: bool,
    pub normalized: bool,
    pub max_results: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct SearchMemoriesResponse {
    pub queries: Vec<String>,
    pub match_mode: SearchMatchMode,
    pub path: Option<String>,
    pub matches: Vec<MemorySearchMatch>,
    pub next_cursor: Option<String>,
    pub truncated: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SearchMatchMode {
    Any,
    AllOnSameLine,
    AllWithinLines {
        #[schemars(range(min = 1))]
        line_count: usize,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct MemoryEntry {
    pub path: String,
    pub entry_type: MemoryEntryType,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MemoryEntryType {
    File,
    Directory,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct MemorySearchMatch {
    pub path: String,
    pub match_line_number: usize,
    pub content_start_line_number: usize,
    pub content: String,
    pub matched_queries: Vec<String>,
}

#[derive(Debug, thiserror::Error)]
pub enum MemoriesBackendError {
    #[error("filename '{filename}' {reason}")]
    InvalidFilename { filename: String, reason: String },
    #[error("ad-hoc note must not be empty")]
    EmptyAdHocNote,
    #[error("ad-hoc note '{filename}' already exists")]
    AdHocNoteAlreadyExists { filename: String },
    #[error("path '{path}' {reason}")]
    InvalidPath { path: String, reason: String },
    #[error("cursor '{cursor}' {reason}")]
    InvalidCursor { cursor: String, reason: String },
    #[error("path '{path}' was not found")]
    NotFound { path: String },
    #[error("line_offset must be a 1-indexed line number")]
    InvalidLineOffset,
    #[error("max_lines must be a positive integer")]
    InvalidMaxLines,
    #[error("line_offset exceeds file length")]
    LineOffsetExceedsFileLength,
    #[error("path '{path}' is not a file")]
    NotFile { path: String },
    #[error("queries must not be empty or contain empty strings")]
    EmptyQuery,
    #[error("all_within_lines.line_count must be a positive integer")]
    InvalidMatchWindow,
    #[error("I/O error while reading memories: {0}")]
    Io(#[from] std::io::Error),
}

impl MemoriesBackendError {
    pub fn invalid_filename(filename: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::InvalidFilename {
            filename: filename.into(),
            reason: reason.into(),
        }
    }

    pub fn invalid_path(path: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::InvalidPath {
            path: path.into(),
            reason: reason.into(),
        }
    }

    pub fn invalid_cursor(cursor: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::InvalidCursor {
            cursor: cursor.into(),
            reason: reason.into(),
        }
    }
}
