use std::fmt;
use std::path::{Path, PathBuf};

use lora_wal::{SyncMode, WalConfig};

/// Hard ceiling for one portable database container/root.
///
/// The current WAL backend still stores segment files under this root, but
/// callers should treat the resolved `.loradb` path as the database artifact.
/// The archive backend will use the same limit when it starts writing framed
/// compressed chunks directly into portable files.
pub const DEFAULT_DATABASE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;

/// Options for opening a named filesystem-backed database.
#[derive(Debug, Clone)]
pub struct DatabaseOpenOptions {
    pub database_dir: PathBuf,
    pub sync_mode: SyncMode,
    pub segment_target_bytes: u64,
    pub max_database_bytes: u64,
}

impl Default for DatabaseOpenOptions {
    fn default() -> Self {
        Self {
            database_dir: PathBuf::from("."),
            sync_mode: SyncMode::GroupSync { interval_ms: 1_000 },
            segment_target_bytes: 8 * 1024 * 1024,
            max_database_bytes: DEFAULT_DATABASE_MAX_BYTES,
        }
    }
}

impl DatabaseOpenOptions {
    pub fn with_database_dir(mut self, database_dir: impl Into<PathBuf>) -> Self {
        self.database_dir = database_dir.into();
        self
    }

    pub fn wal_config_for(&self, name: &DatabaseName) -> WalConfig {
        WalConfig::Enabled {
            dir: self.database_path_for(name),
            sync_mode: self.sync_mode,
            segment_target_bytes: self.segment_target_bytes,
        }
    }

    pub fn database_path_for(&self, name: &DatabaseName) -> PathBuf {
        self.database_dir.join(name.relative_path())
    }
}

/// Validated logical database name/path.
///
/// The input is serialized as a safe relative path under
/// [`DatabaseOpenOptions::database_dir`]. For example:
///
/// - `app` -> `app.loradb`
/// - `app.loradb` -> `app.loradb`
/// - `./tenant-a/app` -> `tenant-a/app.loradb`
///
/// Absolute paths, parent-directory traversal, empty components, and characters
/// outside ASCII letters/digits plus `+`, `_`, `-` are rejected. The final path
/// component may additionally end in `.loradb`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DatabaseName {
    raw: String,
    relative_path: PathBuf,
}

impl DatabaseName {
    pub fn parse(value: impl AsRef<str>) -> Result<Self, DatabaseNameError> {
        let value = value.as_ref();
        let parts = normalized_relative_parts(value)?;

        let mut path = PathBuf::new();
        for (idx, part) in parts.iter().enumerate() {
            if *part == "." {
                continue;
            }
            if *part == ".." {
                return Err(DatabaseNameError::Reserved(value.to_string()));
            }
            let is_basename = idx == parts.len() - 1;
            let serialized = serialize_component(part, is_basename)
                .ok_or_else(|| DatabaseNameError::InvalidCharacters(value.to_string()))?;
            path.push(serialized);
        }
        if path.as_os_str().is_empty() {
            return Err(DatabaseNameError::Reserved(value.to_string()));
        }
        Ok(Self {
            raw: value.to_string(),
            relative_path: path,
        })
    }

    pub fn as_str(&self) -> &str {
        &self.raw
    }

    pub fn relative_path(&self) -> &Path {
        &self.relative_path
    }
}

fn normalized_relative_parts(value: &str) -> Result<Vec<&str>, DatabaseNameError> {
    if value.is_empty() {
        return Err(DatabaseNameError::Empty);
    }

    if value.starts_with('/') || value.starts_with('\\') || looks_like_windows_absolute(value) {
        return Err(DatabaseNameError::AbsolutePath(value.to_string()));
    }

    let mut parts: Vec<&str> = value.split(['/', '\\']).collect();
    if parts.iter().any(|part| part.is_empty()) {
        return Err(DatabaseNameError::InvalidCharacters(value.to_string()));
    }
    let start = parts
        .iter()
        .position(|part| *part != ".")
        .unwrap_or(parts.len());
    let end = parts
        .iter()
        .rposition(|part| *part != ".")
        .map_or(start, |idx| idx + 1);
    if start == end {
        return Err(DatabaseNameError::Reserved(value.to_string()));
    }

    parts.truncate(end);
    parts.drain(..start);
    Ok(parts)
}

impl fmt::Display for DatabaseName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.raw.fmt(f)
    }
}

impl TryFrom<&str> for DatabaseName {
    type Error = DatabaseNameError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::parse(value)
    }
}

impl std::str::FromStr for DatabaseName {
    type Err = DatabaseNameError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Self::parse(value)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DatabaseNameError {
    Empty,
    Reserved(String),
    AbsolutePath(String),
    InvalidCharacters(String),
}

impl fmt::Display for DatabaseNameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => write!(f, "database name must not be empty"),
            Self::Reserved(name) => write!(f, "database name `{name}` is reserved"),
            Self::AbsolutePath(name) => write!(
                f,
                "invalid database name `{name}`: use a relative path under `database_dir`"
            ),
            Self::InvalidCharacters(name) => write!(
                f,
                "invalid database name `{name}`: use relative path components containing only letters, digits, `+`, `_`, `-`, with an optional `.loradb` suffix on the basename"
            ),
        }
    }
}

impl std::error::Error for DatabaseNameError {}

pub fn resolve_database_path(
    database_name: &str,
    database_dir: impl AsRef<Path>,
) -> Result<PathBuf, DatabaseNameError> {
    let name = DatabaseName::parse(database_name)?;
    Ok(database_dir.as_ref().join(name.relative_path()))
}

fn serialize_component(component: &str, is_basename: bool) -> Option<String> {
    if component.is_empty() {
        return None;
    }

    if is_basename {
        if let Some(stem) = component.strip_suffix(".loradb") {
            return (!stem.is_empty() && is_portable_component(stem))
                .then(|| component.to_string());
        }
        return is_portable_component(component).then(|| format!("{component}.loradb"));
    }

    is_portable_component(component).then(|| component.to_string())
}

fn is_portable_component(value: &str) -> bool {
    value
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'_' | b'-'))
}

fn looks_like_windows_absolute(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() >= 3
        && bytes[0].is_ascii_alphabetic()
        && bytes[1] == b':'
        && matches!(bytes[2], b'/' | b'\\')
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_with_standard_from_str_trait() {
        let name: DatabaseName = "tenant/app".parse().unwrap();
        assert_eq!(name.as_str(), "tenant/app");
        assert_eq!(name.relative_path(), Path::new("tenant/app.loradb"));
    }
}
