use std::{
    fs, io,
    path::{Path, PathBuf},
};

use crate::diag::Diagnostic;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ProjectTemplate {
    Binary,
    Library,
    Workspace,
}

impl ProjectTemplate {
    pub fn parse(value: &str) -> Option<Self> {
        match value {
            "binary" => Some(Self::Binary),
            "library" => Some(Self::Library),
            "workspace" => Some(Self::Workspace),
            _ => None,
        }
    }
}

pub fn create_project(
    target: &str,
    explicit_name: Option<&str>,
    template: ProjectTemplate,
) -> Result<(), Diagnostic> {
    if target.trim().is_empty() {
        return Err(Diagnostic::new(
            target,
            "UsageError",
            "project directory must not be empty",
        ));
    }

    let root = Path::new(target);
    if root.is_file() {
        return Err(Diagnostic::new(
            target,
            "ProjectScaffoldBlocked",
            format!(
                "cannot create project `{}` because a file exists there",
                target
            ),
        ));
    }
    if root.is_dir() && has_entries(root)? {
        return Err(Diagnostic::new(
            target,
            "ProjectScaffoldBlocked",
            format!(
                "cannot create project `{}` because the directory is not empty",
                target
            ),
        )
        .hint("choose an empty directory or a new project path"));
    }

    let raw_name = explicit_name
        .map(str::to_string)
        .unwrap_or_else(|| basename(root).unwrap_or_else(|| "slovo-project".to_string()));
    let name = sanitize_project_name(&raw_name);
    if name.is_empty() {
        return Err(Diagnostic::new(
            target,
            "InvalidProjectName",
            "project name must contain at least one ASCII letter or digit",
        ));
    }

    match template {
        ProjectTemplate::Binary => create_binary_project(root, target, &name),
        ProjectTemplate::Library => create_library_project(root, target, &name),
        ProjectTemplate::Workspace => create_workspace_project(root, target),
    }
}

fn create_binary_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> {
    let src = root.join("src");
    create_dir_all_checked(&src, target)?;
    write_checked(
        &root.join("slovo.toml"),
        &format!(
            "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
            name
        ),
        target,
    )?;
    write_checked(
        &src.join("main.slo"),
        "(module main)\n\n(fn main () -> i32\n  0)\n\n(test \"main returns zero\"\n  (= (main) 0))\n",
        target,
    )
}

fn create_library_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> {
    let src = root.join("src");
    create_dir_all_checked(&src, target)?;
    write_checked(
        &root.join("slovo.toml"),
        &format!(
            "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"lib\"\n",
            name
        ),
        target,
    )?;
    write_checked(
        &src.join("lib.slo"),
        "(module lib (export answer double))\n\n(fn answer () -> i32\n  42)\n\n(fn double ((value i32)) -> i32\n  (+ value value))\n\n(test \"answer is stable\"\n  (= (answer) 42))\n\n(test \"double works\"\n  (= (double 21) 42))\n",
        target,
    )
}

fn create_workspace_project(root: &Path, target: &str) -> Result<(), Diagnostic> {
    let app_src = root.join("packages/app/src");
    let lib_src = root.join("packages/libutil/src");
    create_dir_all_checked(&app_src, target)?;
    create_dir_all_checked(&lib_src, target)?;
    write_checked(
        &root.join("slovo.toml"),
        "[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\ndefault_package = \"app\"\n",
        target,
    )?;
    write_checked(
        &root.join("packages/libutil/slovo.toml"),
        "[package]\nname = \"libutil\"\nversion = \"0.1.0\"\n",
        target,
    )?;
    write_checked(
        &root.join("packages/libutil/src/libutil.slo"),
        "(module libutil (export answer label))\n\n(fn answer () -> i32\n  42)\n\n(fn label () -> string\n  \"libutil\")\n\n(test \"answer is stable\"\n  (= (answer) 42))\n",
        target,
    )?;
    write_checked(
        &root.join("packages/app/slovo.toml"),
        "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nlibutil = { path = \"../libutil\" }\n",
        target,
    )?;
    write_checked(
        &root.join("packages/app/src/main.slo"),
        "(module main)\n\n(import libutil.libutil (answer label))\n\n(fn main () -> i32\n  (if (= (answer) 42)\n    0\n    1))\n\n(test \"app uses libutil\"\n  (if (= (label) \"libutil\")\n    (= (answer) 42)\n    false))\n",
        target,
    )
}

fn has_entries(path: &Path) -> Result<bool, Diagnostic> {
    let mut entries = fs::read_dir(path).map_err(|err| io_diagnostic(path, err))?;
    match entries.next() {
        Some(Ok(_)) => Ok(true),
        Some(Err(err)) => Err(io_diagnostic(path, err)),
        None => Ok(false),
    }
}

fn basename(path: &Path) -> Option<String> {
    path.file_name()
        .and_then(|name| name.to_str())
        .map(str::to_string)
}

fn sanitize_project_name(value: &str) -> String {
    let mut out = String::new();
    let mut previous_dash = false;
    for ch in value.chars().flat_map(char::to_lowercase) {
        let mapped = if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
            Some(ch)
        } else if ch == '-' || ch == '_' || ch == '.' || ch.is_whitespace() {
            Some('-')
        } else {
            None
        };
        let Some(ch) = mapped else {
            continue;
        };
        if ch == '-' {
            if !out.is_empty() && !previous_dash {
                out.push('-');
                previous_dash = true;
            }
        } else {
            out.push(ch);
            previous_dash = false;
        }
    }
    while out.ends_with('-') {
        out.pop();
    }
    if out
        .chars()
        .next()
        .is_some_and(|first| first.is_ascii_digit())
    {
        out.insert_str(0, "project-");
    }
    out
}

fn create_dir_all_checked(path: &PathBuf, target: &str) -> Result<(), Diagnostic> {
    fs::create_dir_all(path).map_err(|err| {
        Diagnostic::new(
            target,
            "ProjectScaffoldBlocked",
            format!("cannot create directory `{}`: {}", path.display(), err),
        )
    })
}

fn write_checked(path: &Path, text: &str, target: &str) -> Result<(), Diagnostic> {
    fs::write(path, text).map_err(|err| {
        Diagnostic::new(
            target,
            "ProjectScaffoldBlocked",
            format!("cannot write `{}`: {}", path.display(), err),
        )
    })
}

fn io_diagnostic(path: &Path, err: io::Error) -> Diagnostic {
    Diagnostic::new(
        path.display().to_string(),
        "ProjectScaffoldBlocked",
        format!("cannot inspect `{}`: {}", path.display(), err),
    )
}
