use anyhow::{Context, Result, anyhow};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Deserialize, Default)]
pub struct PyProject {
    pub project: Option<ProjectSection>,
    pub tool: Option<ToolSection>,
}

#[derive(Debug, Deserialize, Default)]
pub struct ProjectSection {
    pub name: Option<String>,
    pub scripts: Option<HashMap<String, String>>,
    #[serde(rename = "requires-python")]
    pub requires_python: Option<String>,
}

#[derive(Debug, Deserialize, Default)]
pub struct ToolSection {
    pub ux: Option<UxConfig>,
}

#[derive(Debug, Deserialize, Default, Clone)]
#[allow(dead_code)] // Used only on macOS
pub struct MacOSConfig {
    /// Path to .icns or .png icon file (PNG will be converted to ICNS)
    pub icon: Option<String>,
    /// Bundle identifier (e.g., com.example.myapp)
    pub bundle_identifier: Option<String>,
    /// Display name in Finder
    pub bundle_name: Option<String>,
    /// App version string
    pub version: Option<String>,
    /// Code signing identity (e.g., "Developer ID Application: Name (XXXXXXXXXX)")
    pub codesign_identity: Option<String>,
    /// Apple ID for notarization
    pub apple_id: Option<String>,
    /// Team ID for notarization
    pub team_id: Option<String>,
}

#[derive(Debug, Deserialize, Default, Clone)]
pub struct UxConfig {
    /// Entry point command name (from [project.scripts])
    pub entry: Option<String>,
    /// uv version to use (default: "latest")
    pub uv_version: Option<String>,
    /// Files/directories to include in bundle
    /// Default: ["*.py", "pyproject.toml", "uv.lock"]
    /// Use "dir/" suffix to include entire directory (e.g., "assets/")
    pub include: Option<Vec<String>>,
    /// Embed Python interpreter and all wheels for fully offline execution
    pub offline: Option<bool>,
    /// Python version to embed (e.g., "3.12"). Auto-detected from requires-python if omitted.
    pub python_version: Option<String>,
    /// macOS-specific configuration
    pub macos: Option<MacOSConfig>,
}

#[derive(Debug)]
pub struct Config {
    #[allow(dead_code)]
    pub project_dir: PathBuf,
    pub project_name: String,
    /// CLI script name (e.g. "bkstg")
    pub entry_point: String,
    /// Full module:function entry (e.g. "bkstg.main:main"), used for offline Python invocation
    pub entry_module: Option<String>,
    pub uv_version: String,
    pub include: Vec<String>,
    /// Whether to create an offline-capable bundle (embed Python + vendor packages)
    pub offline: bool,
    /// Python version to embed in offline mode
    pub python_version: String,
    #[allow(dead_code)] // Used only on macOS
    pub macos: Option<MacOSConfig>,
}

/// Extract a usable Python version string from a `requires-python` specifier.
/// Examples: ">=3.12" → "3.12", ">=3.11,<3.13" → "3.11", "==3.12.*" → "3.12"
fn extract_python_version(spec: &str) -> Option<String> {
    // Find the first digit sequence that looks like a version number
    let digits_re = spec
        .split(|c: char| !c.is_ascii_digit() && c != '.')
        .find(|s| s.contains('.'))?;
    // Take only major.minor
    let parts: Vec<&str> = digits_re.splitn(3, '.').collect();
    if parts.len() >= 2 {
        Some(format!("{}.{}", parts[0], parts[1]))
    } else {
        None
    }
}

impl Config {
    /// Load configuration from project directory
    pub fn load(project_dir: &Path) -> Result<Self> {
        let pyproject_path = project_dir.join("pyproject.toml");

        if !pyproject_path.exists() {
            return Err(anyhow!(
                "pyproject.toml not found in {}",
                project_dir.display()
            ));
        }

        let content = fs::read_to_string(&pyproject_path)
            .with_context(|| format!("Failed to read {}", pyproject_path.display()))?;

        let pyproject: PyProject = toml::from_str(&content)
            .with_context(|| format!("Failed to parse {}", pyproject_path.display()))?;

        // Get project name
        let project_name = pyproject
            .project
            .as_ref()
            .and_then(|p| p.name.clone())
            .ok_or_else(|| anyhow!("[project].name is required in pyproject.toml"))?;

        // Get ux config (optional)
        let ux_config = pyproject
            .tool
            .as_ref()
            .and_then(|t| t.ux.clone())
            .unwrap_or_default();

        // Determine entry point and module
        let (entry_point, entry_module) = Self::resolve_entry_point(&pyproject, &ux_config)?;

        // Get uv version
        let uv_version = ux_config.uv_version.unwrap_or_else(|| "latest".to_string());

        // Get include patterns for extra files/directories
        let include = ux_config.include.unwrap_or_default();

        // Get macOS config
        let macos = ux_config.macos.clone();

        // Offline mode settings
        let offline = ux_config.offline.unwrap_or(false);
        let python_version = ux_config.python_version.clone().unwrap_or_else(|| {
            // Auto-detect from requires-python (e.g. ">=3.12" → "3.12")
            pyproject
                .project
                .as_ref()
                .and_then(|p| p.requires_python.as_deref())
                .and_then(extract_python_version)
                .unwrap_or_else(|| "3.12".to_string())
        });

        Ok(Self {
            project_dir: project_dir.to_path_buf(),
            project_name,
            entry_point,
            entry_module,
            uv_version,
            include,
            offline,
            python_version,
            macos,
        })
    }

    /// Resolve entry point (script name) and its module:function string.
    fn resolve_entry_point(
        pyproject: &PyProject,
        ux_config: &UxConfig,
    ) -> Result<(String, Option<String>)> {
        let scripts = pyproject.project.as_ref().and_then(|p| p.scripts.as_ref());

        // 1. [tool.ux].entry overrides script name
        if let Some(entry) = &ux_config.entry {
            let module = scripts.and_then(|s| s.get(entry.as_str())).cloned();
            return Ok((entry.clone(), module));
        }

        // 2. First script from [project.scripts]
        if let Some(scripts) = scripts
            && let Some((name, module)) = scripts.iter().next()
        {
            return Ok((name.clone(), Some(module.clone())));
        }

        Err(anyhow!(
            "No entry point found. Set [tool.ux].entry or define [project.scripts] in pyproject.toml"
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn test_load_config() {
        let dir = tempdir().unwrap();
        let pyproject_path = dir.path().join("pyproject.toml");

        let mut file = fs::File::create(&pyproject_path).unwrap();
        write!(
            file,
            r#"
[project]
name = "myapp"

[project.scripts]
myapp = "myapp.main:main"

[tool.ux]
uv_version = "0.5.0"
"#
        )
        .unwrap();

        let config = Config::load(dir.path()).unwrap();
        assert_eq!(config.project_name, "myapp");
        assert_eq!(config.entry_point, "myapp");
        assert_eq!(config.uv_version, "0.5.0");
    }

    #[test]
    fn test_explicit_entry() {
        let dir = tempdir().unwrap();
        let pyproject_path = dir.path().join("pyproject.toml");

        let mut file = fs::File::create(&pyproject_path).unwrap();
        write!(
            file,
            r#"
[project]
name = "myapp"

[project.scripts]
myapp = "myapp.main:main"
other = "myapp.other:run"

[tool.ux]
entry = "other"
"#
        )
        .unwrap();

        let config = Config::load(dir.path()).unwrap();
        assert_eq!(config.entry_point, "other");
    }
}
