use std::collections::BTreeMap;
use std::fmt;
use std::path::{Component, Path, PathBuf};

pub const RUNTIME_RECEIPTS_DIR_CONFIG_KEY: &str = "runtime.receipts.dir";
pub const RUNX_RECEIPT_DIR_ENV: &str = "RUNX_RECEIPT_DIR";
pub const RUNX_PROJECT_DIR_ENV: &str = "RUNX_PROJECT_DIR";
pub const RUNX_CWD_ENV: &str = "RUNX_CWD";
pub const INIT_CWD_ENV: &str = "INIT_CWD";

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RuntimeReceiptConfig {
    pub dir: Option<PathBuf>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReceiptPathSource {
    ExplicitInput,
    RuntimeConfig,
    Environment,
    ProjectDefault,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReceiptStoreLabel(String);

impl ReceiptStoreLabel {
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReceiptStorePublicProjection {
    label: ReceiptStoreLabel,
}

impl ReceiptStorePublicProjection {
    #[must_use]
    pub fn label(&self) -> &ReceiptStoreLabel {
        &self.label
    }

    #[must_use]
    pub fn summary(&self) -> String {
        format!("receipt store: {}", self.label)
    }
}

impl fmt::Display for ReceiptStorePublicProjection {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "receipt store: {}", self.label)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedReceiptPath {
    pub path: PathBuf,
    pub source: ReceiptPathSource,
    pub label: ReceiptStoreLabel,
    pub project_runx_dir: PathBuf,
    pub workspace_base: PathBuf,
}

impl ResolvedReceiptPath {
    #[must_use]
    pub fn public_projection(&self) -> ReceiptStorePublicProjection {
        ReceiptStorePublicProjection {
            label: self.label.clone(),
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub struct ReceiptPathInputs<'a> {
    pub explicit_dir: Option<&'a Path>,
    pub runtime_config: Option<&'a RuntimeReceiptConfig>,
    pub env: &'a BTreeMap<String, String>,
    pub cwd: &'a Path,
}

#[must_use]
pub fn resolve_receipt_path(inputs: ReceiptPathInputs<'_>) -> ResolvedReceiptPath {
    let workspace_base = resolve_workspace_base(inputs.env, inputs.cwd);
    let project_runx_dir = resolve_project_runx_dir(inputs.env, &workspace_base);
    let (path, source) = match inputs.explicit_dir {
        Some(path) => (
            resolve_from_workspace_base(path, &workspace_base),
            ReceiptPathSource::ExplicitInput,
        ),
        None => match inputs
            .runtime_config
            .and_then(|config| config.dir.as_deref())
        {
            Some(path) => (
                resolve_from_workspace_base(path, &workspace_base),
                ReceiptPathSource::RuntimeConfig,
            ),
            None => match env_path(inputs.env, RUNX_RECEIPT_DIR_ENV) {
                Some(path) => (
                    resolve_from_workspace_base(path, &workspace_base),
                    ReceiptPathSource::Environment,
                ),
                None => (
                    project_runx_dir.join("receipts"),
                    ReceiptPathSource::ProjectDefault,
                ),
            },
        },
    };
    let path = lexical_normalize(&path);
    let label = safe_receipt_store_label(&path, &workspace_base, &project_runx_dir);
    ResolvedReceiptPath {
        path,
        source,
        label,
        project_runx_dir,
        workspace_base,
    }
}

#[must_use]
pub fn resolve_workspace_base(env: &BTreeMap<String, String>, cwd: &Path) -> PathBuf {
    env_path(env, RUNX_CWD_ENV)
        .or_else(|| env_path(env, INIT_CWD_ENV))
        .map_or_else(|| absolute_cwd(cwd), |path| resolve_from_cwd(path, cwd))
}

#[must_use]
pub fn resolve_project_runx_dir(env: &BTreeMap<String, String>, workspace_base: &Path) -> PathBuf {
    env_path(env, RUNX_PROJECT_DIR_ENV).map_or_else(
        || lexical_normalize(&workspace_base.join(".runx")),
        |path| resolve_from_workspace_base(path, workspace_base),
    )
}

#[must_use]
pub fn safe_receipt_store_label(
    receipt_dir: &Path,
    workspace_base: &Path,
    project_runx_dir: &Path,
) -> ReceiptStoreLabel {
    let receipt_dir = lexical_normalize(receipt_dir);
    let workspace_base = lexical_normalize(workspace_base);
    let project_runx_dir = lexical_normalize(project_runx_dir);

    if let Ok(relative_to_project) = receipt_dir.strip_prefix(&project_runx_dir) {
        if let Ok(relative_to_workspace) = receipt_dir.strip_prefix(&workspace_base) {
            return ReceiptStoreLabel(path_label(relative_to_workspace));
        }
        return ReceiptStoreLabel(format!("runx-project:{}", path_label(relative_to_project)));
    }

    ReceiptStoreLabel(format!(
        "external-receipt-store:{}",
        stable_path_hash(&receipt_dir)
    ))
}

#[must_use]
pub fn safe_receipt_store_projection(
    receipt_dir: &Path,
    workspace_base: &Path,
    project_runx_dir: &Path,
) -> ReceiptStorePublicProjection {
    ReceiptStorePublicProjection {
        label: safe_receipt_store_label(receipt_dir, workspace_base, project_runx_dir),
    }
}

fn env_path<'a>(env: &'a BTreeMap<String, String>, key: &str) -> Option<&'a Path> {
    env.get(key)
        .filter(|value| !value.trim().is_empty())
        .map(Path::new)
}

fn resolve_from_workspace_base(path: &Path, workspace_base: &Path) -> PathBuf {
    if path.is_absolute() {
        lexical_normalize(path)
    } else {
        lexical_normalize(&workspace_base.join(path))
    }
}

fn resolve_from_cwd(path: &Path, cwd: &Path) -> PathBuf {
    if path.is_absolute() {
        lexical_normalize(path)
    } else {
        lexical_normalize(&absolute_cwd(cwd).join(path))
    }
}

fn absolute_cwd(cwd: &Path) -> PathBuf {
    if cwd.is_absolute() {
        lexical_normalize(cwd)
    } else {
        let base = match std::env::current_dir() {
            Ok(path) => path,
            Err(_) => PathBuf::from("."),
        };
        lexical_normalize(&base.join(cwd))
    }
}

fn lexical_normalize(path: &Path) -> PathBuf {
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
            Component::RootDir => normalized.push(component.as_os_str()),
            Component::CurDir => {}
            Component::ParentDir => {
                if !normalized.pop() {
                    normalized.push("..");
                }
            }
            Component::Normal(segment) => normalized.push(segment),
        }
    }
    normalized
}

fn path_label(path: &Path) -> String {
    let label = path
        .components()
        .filter_map(|component| match component {
            Component::Normal(segment) => Some(segment.to_string_lossy().into_owned()),
            Component::CurDir => Some(".".to_owned()),
            Component::ParentDir => Some("..".to_owned()),
            Component::Prefix(_) | Component::RootDir => None,
        })
        .collect::<Vec<_>>()
        .join("/");
    if label.is_empty() {
        ".".to_owned()
    } else {
        label
    }
}

fn stable_path_hash(path: &Path) -> String {
    let mut hash = 0xcbf29ce484222325u64;
    for byte in path.to_string_lossy().as_bytes() {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(0x100000001b3);
    }
    format!("{hash:016x}")
}
