use super::*;
use anyhow::{Context, Result};
use serde::Deserialize;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

#[derive(Debug, Deserialize, Default)]
struct SigningToml {
    package: Option<PackageToml>,
}

#[derive(Debug, Deserialize, Default)]
struct PackageToml {
    android: Option<AndroidPackageToml>,
    ios: Option<ApplePackageToml>,
    macos: Option<MacosPackageToml>,
    windows: Option<WindowsPackageToml>,
}

#[derive(Debug, Deserialize, Default)]
struct AndroidPackageToml {
    keystore: Option<String>,
    upload_keystore: Option<String>,
    keystore_alias: Option<String>,
    package_name: Option<String>,
}

#[derive(Debug, Deserialize, Default)]
struct ApplePackageToml {
    bundle_id: Option<String>,
    team_id: Option<String>,
    entitlements: Option<String>,
    provisioning_profile: Option<String>,
    signing_identity: Option<String>,
}

#[derive(Debug, Deserialize, Default)]
struct MacosPackageToml {
    bundle_id: Option<String>,
    entitlements: Option<String>,
    signing_identity: Option<String>,
    installer_identity: Option<String>,
    notarize: Option<bool>,
}

#[derive(Debug, Deserialize, Default)]
struct WindowsPackageToml {
    identity_name: Option<String>,
    publisher: Option<String>,
    certificate: Option<String>,
    certificate_thumbprint: Option<String>,
}

pub(super) fn status(project_dir: &Path, target: Target, json: bool) -> Result<()> {
    print_report(
        build_status_report("signing.status", project_dir, target),
        json,
    )
}

pub(super) fn sync(project_dir: &Path, target: Target, readonly: bool, json: bool) -> Result<()> {
    let mut report = build_status_report("signing.sync", project_dir, target);
    report.checks.push(ok_check(
        "signing.sync.mode",
        if readonly {
            "readonly"
        } else {
            "write status snapshot"
        },
    ));
    if !readonly {
        let output = project_dir
            .join("release-content/signing")
            .join(format!("{}.status.json", target.as_str()));
        if let Some(parent) = output.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(&output, serde_json::to_vec_pretty(&report)?)
            .with_context(|| format!("failed to write {}", output.display()))?;
        report.checks.push(ok_check(
            "signing.sync.snapshot_written",
            output.display().to_string(),
        ));
    }
    print_report(report, json)
}

pub(super) fn import(
    project_dir: &Path,
    target: Target,
    keystore: Option<PathBuf>,
    alias: Option<String>,
    json: bool,
) -> Result<()> {
    let mut report = base_report("signing.import", None, Some(target));
    report.checks.push(path_check(
        "signing.project_config_exists",
        project_dir.join("fission.toml"),
        "fission.toml exists",
    ));
    match target {
        Target::Android => import_android(project_dir, keystore, alias, &mut report)?,
        Target::Ios | Target::Macos | Target::Windows => report.checks.push(failed_check(
            "signing.import.target_requires_platform_store",
            format!(
                "{} signing import is intentionally read-only for now; use the platform certificate/keychain tooling and record references in fission.toml",
                target.as_str()
            ),
        )),
        _ => report.checks.push(warning_check(
            "signing.import.target",
            format!("{} does not require signing by default", target.as_str()),
        )),
    }
    print_report(report, json)
}

fn import_android(
    project_dir: &Path,
    keystore: Option<PathBuf>,
    alias: Option<String>,
    report: &mut LifecycleReport,
) -> Result<()> {
    let keystore = keystore.context("signing import --target android requires --keystore")?;
    let alias = alias.context("signing import --target android requires --alias")?;
    report.checks.push(path_check(
        "signing.android.keystore_exists",
        keystore.clone(),
        "Android upload keystore exists",
    ));
    if !keystore.exists() {
        return Ok(());
    }
    let relative = project_relative_or_absolute(project_dir, &keystore);
    let path = project_dir.join("fission.toml");
    let data = fs::read_to_string(&path).unwrap_or_default();
    let mut root = if data.trim().is_empty() {
        toml_edit::DocumentMut::new()
    } else {
        parse_toml_edit_document(&data, &path)?
    };
    set_toml_edit_path(
        &mut root,
        "package.android.keystore",
        toml_edit::value(relative.clone()),
    )?;
    set_toml_edit_path(
        &mut root,
        "package.android.keystore_alias",
        toml_edit::value(alias.clone()),
    )?;
    write_toml_edit_document(&path, &root)?;
    report.checks.push(ok_check(
        "signing.android.config_written",
        format!("package.android.keystore = {relative}, alias = {alias}"),
    ));
    report.checks.push(warning_check(
        "signing.android.password_not_imported",
        "keystore passwords were not stored in fission.toml; use ANDROID_KEYSTORE_PASSWORD and ANDROID_KEY_PASSWORD in CI or an OS-backed secret store".to_string(),
    ));
    Ok(())
}

fn build_status_report(area: &str, project_dir: &Path, target: Target) -> LifecycleReport {
    let mut report = base_report(area, None, Some(target));
    let config_path = project_dir.join("fission.toml");
    report.checks.push(path_check(
        "signing.project_config_exists",
        config_path.clone(),
        "fission.toml exists",
    ));
    let config = signing_config(&config_path).unwrap_or_default();
    match target {
        Target::Android => android_checks(
            project_dir,
            config.package.and_then(|p| p.android),
            &mut report,
        ),
        Target::Ios => {
            apple_ios_checks(project_dir, config.package.and_then(|p| p.ios), &mut report)
        }
        Target::Macos => macos_checks(
            project_dir,
            config.package.and_then(|p| p.macos),
            &mut report,
        ),
        Target::Windows => windows_checks(
            project_dir,
            config.package.and_then(|p| p.windows),
            &mut report,
        ),
        _ => report.checks.push(warning_check(
            "signing.target",
            format!("{} does not require signing by default", target.as_str()),
        )),
    }
    finalize_status(&mut report);
    report
}

fn android_checks(
    project_dir: &Path,
    cfg: Option<AndroidPackageToml>,
    report: &mut LifecycleReport,
) {
    let keystore = env::var("ANDROID_KEYSTORE")
        .ok()
        .or_else(|| cfg.as_ref().and_then(|cfg| cfg.keystore.clone()))
        .or_else(|| cfg.as_ref().and_then(|cfg| cfg.upload_keystore.clone()));
    report.checks.push(required_text(
        "signing.android.package_name",
        cfg.as_ref().and_then(|cfg| cfg.package_name.as_deref()),
        "Android package name is configured",
        "Set package.android.package_name in fission.toml.",
    ));
    report.checks.push(required_text(
        "signing.android.alias",
        cfg.as_ref().and_then(|cfg| cfg.keystore_alias.as_deref()),
        "Android keystore alias is configured",
        "Run `fission signing import --target android --keystore <file> --alias <alias>`.",
    ));
    match keystore {
        Some(path) => report.checks.push(path_check(
            "signing.android.keystore_exists",
            project_dir.join(path),
            "Android upload keystore exists",
        )),
        None => report.checks.push(required_text(
            "signing.android.keystore",
            None,
            "Android upload keystore is configured",
            "Set ANDROID_KEYSTORE or package.android.keystore.",
        )),
    }
    report.checks.push(env_or_warning(
        "signing.android.keystore_password",
        &["ANDROID_KEYSTORE_PASSWORD", "ANDROID_KEY_PASSWORD"],
        "Android signing password source is configured",
        "Set ANDROID_KEYSTORE_PASSWORD and ANDROID_KEY_PASSWORD in CI or an OS-backed secret store; do not write passwords to fission.toml.",
    ));
    report.checks.push(tool_check(
        "signing.android.keytool_available",
        "keytool",
        "Install a JDK so Fission can inspect Android keystores.",
    ));
    report.checks.push(tool_check(
        "signing.android.apksigner_available",
        "apksigner",
        "Install Android build-tools and ensure apksigner is on PATH.",
    ));
}

fn apple_ios_checks(
    project_dir: &Path,
    cfg: Option<ApplePackageToml>,
    report: &mut LifecycleReport,
) {
    report.checks.push(host_os_check_local(
        "signing.apple.host_is_macos",
        "Apple signing and provisioning checks require macOS.",
    ));
    report.checks.push(required_text(
        "signing.ios.bundle_id",
        cfg.as_ref().and_then(|cfg| cfg.bundle_id.as_deref()),
        "iOS bundle identifier is configured",
        "Set package.ios.bundle_id.",
    ));
    report.checks.push(required_text(
        "signing.ios.team_id",
        cfg.as_ref().and_then(|cfg| cfg.team_id.as_deref()),
        "Apple team id is configured",
        "Set package.ios.team_id.",
    ));
    check_optional_path(
        project_dir,
        &mut report.checks,
        "signing.ios.entitlements",
        cfg.as_ref().and_then(|cfg| cfg.entitlements.as_deref()),
        "iOS entitlements file exists",
    );
    check_optional_path(
        project_dir,
        &mut report.checks,
        "signing.ios.provisioning_profile",
        cfg.as_ref()
            .and_then(|cfg| cfg.provisioning_profile.as_deref()),
        "iOS provisioning profile exists",
    );
    report.checks.push(tool_check(
        "signing.apple.xcrun_available",
        "xcrun",
        "Install Xcode command line tools.",
    ));
    report.checks.push(tool_check(
        "signing.apple.security_available",
        "security",
        "Run on macOS with the security tool available.",
    ));
    report.checks.push(apple_identity_check(
        cfg.as_ref().and_then(|cfg| cfg.signing_identity.as_deref()),
    ));
}

fn macos_checks(project_dir: &Path, cfg: Option<MacosPackageToml>, report: &mut LifecycleReport) {
    report.checks.push(host_os_check_local(
        "signing.apple.host_is_macos",
        "macOS signing and notarization checks require macOS.",
    ));
    report.checks.push(required_text(
        "signing.macos.bundle_id",
        cfg.as_ref().and_then(|cfg| cfg.bundle_id.as_deref()),
        "macOS bundle identifier is configured",
        "Set package.macos.bundle_id.",
    ));
    check_optional_path(
        project_dir,
        &mut report.checks,
        "signing.macos.entitlements",
        cfg.as_ref().and_then(|cfg| cfg.entitlements.as_deref()),
        "macOS entitlements file exists",
    );
    report.checks.push(required_text(
        "signing.macos.identity",
        cfg.as_ref().and_then(|cfg| cfg.signing_identity.as_deref()),
        "Developer ID Application signing identity is configured",
        "Set package.macos.signing_identity.",
    ));
    report.checks.push(tool_check(
        "signing.apple.codesign_available",
        "codesign",
        "Run on macOS with Xcode command line tools installed.",
    ));
    report.checks.push(apple_identity_check(
        cfg.as_ref().and_then(|cfg| cfg.signing_identity.as_deref()),
    ));
    if cfg.as_ref().and_then(|cfg| cfg.notarize).unwrap_or(false) {
        report.checks.push(required_text(
            "signing.macos.installer_identity",
            cfg.as_ref()
                .and_then(|cfg| cfg.installer_identity.as_deref()),
            "Developer ID Installer identity is configured for pkg signing",
            "Set package.macos.installer_identity when package.macos.notarize = true.",
        ));
        for (id, name) in [
            (
                "signing.apple.notary_key_path",
                "APP_STORE_CONNECT_API_KEY_PATH",
            ),
            ("signing.apple.notary_key_id", "APP_STORE_CONNECT_KEY_ID"),
            (
                "signing.apple.notary_issuer_id",
                "APP_STORE_CONNECT_ISSUER_ID",
            ),
        ] {
            report.checks.push(env_or_missing(
                id,
                &[name],
                &format!("{name} is configured for notarization"),
                &format!("Set {name} in the release environment."),
            ));
        }
    }
}

fn windows_checks(
    project_dir: &Path,
    cfg: Option<WindowsPackageToml>,
    report: &mut LifecycleReport,
) {
    report.checks.push(required_text(
        "signing.windows.identity_name",
        cfg.as_ref().and_then(|cfg| cfg.identity_name.as_deref()),
        "Windows package identity name is configured",
        "Set package.windows.identity_name.",
    ));
    report.checks.push(required_text(
        "signing.windows.publisher",
        cfg.as_ref().and_then(|cfg| cfg.publisher.as_deref()),
        "Windows publisher identity is configured",
        "Set package.windows.publisher to the certificate subject.",
    ));
    if let Some(certificate) = cfg.as_ref().and_then(|cfg| cfg.certificate.as_deref()) {
        report.checks.push(path_check(
            "signing.windows.certificate_exists",
            project_dir.join(certificate),
            "Windows signing certificate file exists",
        ));
    } else {
        report.checks.push(required_text(
            "signing.windows.certificate_reference",
            cfg.as_ref()
                .and_then(|cfg| cfg.certificate_thumbprint.as_deref()),
            "Windows signing certificate reference is configured",
            "Set package.windows.certificate or package.windows.certificate_thumbprint.",
        ));
    }
    report.checks.push(env_or_warning(
        "signing.windows.certificate_password",
        &["WINDOWS_CERTIFICATE_PASSWORD"],
        "Windows certificate password source is configured",
        "Set WINDOWS_CERTIFICATE_PASSWORD in CI or use an OS certificate store; do not write passwords to fission.toml.",
    ));
    report.checks.push(tool_check(
        "signing.windows.signtool_available",
        "signtool",
        "Install Windows SDK signing tools and ensure signtool is on PATH.",
    ));
}

fn signing_config(path: &Path) -> Result<SigningToml> {
    if !path.exists() {
        return Ok(SigningToml::default());
    }
    let data = fs::read_to_string(path)?;
    toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))
}

fn check_optional_path(
    project_dir: &Path,
    checks: &mut Vec<LifecycleCheck>,
    id: &str,
    path: Option<&str>,
    summary: &str,
) {
    if let Some(path) = path.filter(|path| !path.trim().is_empty()) {
        checks.push(path_check(id, project_dir.join(path), summary));
    } else {
        checks.push(required_text(
            id,
            None,
            summary,
            "Configure this path in fission.toml if the app requires the capability.",
        ));
    }
}

fn apple_identity_check(expected: Option<&str>) -> LifecycleCheck {
    if !cfg!(target_os = "macos") {
        return LifecycleCheck {
            id: "signing.apple.identity_available".to_string(),
            status: "warning".to_string(),
            summary: "Apple code signing identity is available".to_string(),
            details: Some("identity lookup requires macOS".to_string()),
            remediation: vec![
                "Run this check on a macOS release machine or remote builder.".to_string(),
            ],
        };
    }
    let output = Command::new("security")
        .args(["find-identity", "-v", "-p", "codesigning"])
        .output();
    match output {
        Ok(output) if output.status.success() => {
            let stdout = String::from_utf8_lossy(&output.stdout);
            let found = expected.is_some_and(|needle| stdout.contains(needle));
            LifecycleCheck {
                id: "signing.apple.identity_available".to_string(),
                status: if expected.is_none() || found { "passed" } else { "missing" }.to_string(),
                summary: "Apple code signing identity is available".to_string(),
                details: expected.map(|expected| format!("expected identity: {expected}")),
                remediation: vec!["Install the certificate in the login keychain or update the configured signing identity.".to_string()],
            }
        }
        Ok(output) => LifecycleCheck {
            id: "signing.apple.identity_available".to_string(),
            status: "failed".to_string(),
            summary: "Apple code signing identity lookup succeeds".to_string(),
            details: Some(String::from_utf8_lossy(&output.stderr).to_string()),
            remediation: vec![
                "Unlock the keychain and ensure Xcode command line tools are installed."
                    .to_string(),
            ],
        },
        Err(error) => failed_check("signing.apple.identity_available", error.to_string()),
    }
}

fn required_text(
    id: &str,
    value: Option<&str>,
    summary: &str,
    remediation: &str,
) -> LifecycleCheck {
    LifecycleCheck {
        id: id.to_string(),
        status: if value.is_some_and(|value| !value.trim().is_empty()) {
            "passed"
        } else {
            "missing"
        }
        .to_string(),
        summary: summary.to_string(),
        details: value.map(str::to_string),
        remediation: vec![remediation.to_string()],
    }
}

fn env_or_missing(id: &str, vars: &[&str], summary: &str, remediation: &str) -> LifecycleCheck {
    let found = vars.iter().find(|name| env::var_os(name).is_some());
    LifecycleCheck {
        id: id.to_string(),
        status: if found.is_some() { "passed" } else { "missing" }.to_string(),
        summary: summary.to_string(),
        details: found.map(|name| (*name).to_string()),
        remediation: vec![remediation.to_string()],
    }
}

fn env_or_warning(id: &str, vars: &[&str], summary: &str, remediation: &str) -> LifecycleCheck {
    let found = vars.iter().find(|name| env::var_os(name).is_some());
    LifecycleCheck {
        id: id.to_string(),
        status: if found.is_some() { "passed" } else { "warning" }.to_string(),
        summary: summary.to_string(),
        details: found.map(|name| (*name).to_string()),
        remediation: vec![remediation.to_string()],
    }
}

fn host_os_check_local(id: &str, remediation: &str) -> LifecycleCheck {
    LifecycleCheck {
        id: id.to_string(),
        status: if cfg!(target_os = "macos") {
            "passed"
        } else {
            "missing"
        }
        .to_string(),
        summary: "host operating system supports this signing flow".to_string(),
        details: Some(env::consts::OS.to_string()),
        remediation: vec![remediation.to_string()],
    }
}

fn tool_check(id: &str, program: &str, remediation: &str) -> LifecycleCheck {
    LifecycleCheck {
        id: id.to_string(),
        status: if command_exists(program) {
            "passed"
        } else {
            "missing"
        }
        .to_string(),
        summary: format!("{program} is available on PATH"),
        details: env::var_os("PATH").map(|_| program.to_string()),
        remediation: vec![remediation.to_string()],
    }
}

fn command_exists(program: &str) -> bool {
    let Some(paths) = env::var_os("PATH") else {
        return false;
    };
    env::split_paths(&paths).any(|dir| {
        let candidate = dir.join(program);
        if candidate.is_file() {
            return true;
        }
        if cfg!(windows) {
            ["exe", "cmd", "bat"]
                .iter()
                .any(|ext| dir.join(format!("{program}.{ext}")).is_file())
        } else {
            false
        }
    })
}

fn project_relative_or_absolute(project_dir: &Path, path: &Path) -> String {
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        env::current_dir()
            .unwrap_or_else(|_| project_dir.to_path_buf())
            .join(path)
    };
    absolute
        .strip_prefix(project_dir)
        .unwrap_or(&absolute)
        .to_string_lossy()
        .trim_start_matches(std::path::MAIN_SEPARATOR)
        .to_string()
}

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

    #[test]
    fn android_import_writes_non_secret_references() {
        let dir = std::env::temp_dir().join(format!(
            "fission-signing-import-{}-{}",
            std::process::id(),
            now_unix_seconds()
        ));
        fs::create_dir_all(&dir).unwrap();
        fs::write(dir.join("upload.jks"), "not a real keystore").unwrap();
        fs::write(
            &dir.join("fission.toml"),
            "[package.android]\npackage_name = \"com.example.todo\"\n",
        )
        .unwrap();
        let mut report = base_report("test", None, Some(Target::Android));
        import_android(
            &dir,
            Some(dir.join("upload.jks")),
            Some("upload".to_string()),
            &mut report,
        )
        .unwrap();
        let text = fs::read_to_string(dir.join("fission.toml")).unwrap();
        assert!(text.contains("keystore = \"upload.jks\""));
        assert!(text.contains("keystore_alias = \"upload\""));
        let _ = fs::remove_dir_all(&dir);
    }
}
