use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    compile_mic_check_helper();
    compile_system_audio_helper();
    compile_calendar_helper();
    stage_minutes_cli_sidecar();
    stage_assistant_skill_bundle();
    tauri_build::build()
}

/// Below this size the staged sidecar cannot be a real CLI binary (the real
/// one is tens of MB; the placeholder is ~52 bytes).
const MIN_REAL_CLI_SIDECAR_BYTES: u64 = 1_000_000;

/// Ensure `bin/minutes-<target>` exists so Tauri's `externalBin` resolution
/// succeeds during plain `cargo check` / `cargo clippy` runs that don't go
/// through `scripts/build.sh`.
///
/// If the release CLI has been built (`target/release/minutes`), copy it. If
/// not, write a non-empty placeholder. The placeholder is enough to satisfy
/// Tauri's existence check at compile time for check/clippy runs.
///
/// Distributable bundles must never contain the placeholder. Release builds
/// set `MINUTES_REQUIRE_REAL_CLI_SIDECAR=1`, which makes this step hard-fail
/// instead of writing a placeholder, and re-stage over a stale placeholder
/// left by an earlier check/clippy run (issue #324: the release workflow
/// called `cargo tauri build` without building the CLI first, and every
/// CI-built macOS bundle since v0.16.2 shipped the stub).
fn stage_minutes_cli_sidecar() {
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    if target_os != "macos" {
        return;
    }

    let manifest_dir = PathBuf::from(
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set"),
    );
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".into());
    let bin_dir = manifest_dir.join("bin");
    let staged = bin_dir.join(format!("minutes-{}", target));

    println!("cargo:rerun-if-changed={}", staged.display());
    println!("cargo:rerun-if-env-changed=MINUTES_REQUIRE_REAL_CLI_SIDECAR");

    let require_real = std::env::var("MINUTES_REQUIRE_REAL_CLI_SIDECAR")
        .map(|v| v == "1" || v == "true")
        .unwrap_or(false);

    if staged.exists() {
        let staged_len = fs::metadata(&staged).map(|m| m.len()).unwrap_or(0);
        if !require_real || staged_len >= MIN_REAL_CLI_SIDECAR_BYTES {
            return;
        }
        // Required mode found a stale placeholder from an earlier
        // check/clippy run: drop it and stage the real binary below.
        fs::remove_file(&staged).expect("failed to remove stale placeholder sidecar");
    }

    fs::create_dir_all(&bin_dir).expect("failed to create sidecar bin dir");

    // Prefer the built release CLI when present (developer ran
    // `cargo build --release -p minutes-cli` first).
    let repo_root = manifest_dir.join("../..");
    let candidate_paths = [
        repo_root
            .join("target")
            .join(&target)
            .join("release/minutes"),
        repo_root.join("target/release/minutes"),
    ];
    for candidate in candidate_paths.iter() {
        if candidate.exists() {
            fs::copy(candidate, &staged).expect("failed to copy CLI sidecar");
            return;
        }
    }

    if require_real {
        panic!(
            "MINUTES_REQUIRE_REAL_CLI_SIDECAR is set but no release CLI binary was found \
             (looked at {:?}). Run `cargo build --release -p minutes-cli` before \
             `cargo tauri build` so the bundle ships a working CLI instead of a placeholder.",
            candidate_paths
        );
    }

    // Placeholder for check/clippy only. Distributable builds set
    // MINUTES_REQUIRE_REAL_CLI_SIDECAR and hard-fail above instead.
    println!(
        "cargo:warning=No minutes CLI release binary found; writing placeholder sidecar at {}. Run `cargo build --release -p minutes-cli` (or `scripts/build.sh`) before `cargo tauri build`.",
        staged.display()
    );
    fs::write(
        &staged,
        b"#!/bin/sh\necho 'minutes CLI placeholder' >&2\nexit 1\n",
    )
    .expect("failed to write placeholder sidecar");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perm = fs::metadata(&staged)
            .expect("metadata for placeholder sidecar")
            .permissions();
        perm.set_mode(0o755);
        fs::set_permissions(&staged, perm).expect("chmod placeholder sidecar");
    }
}

fn compile_mic_check_helper() {
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    if target_os != "macos" {
        return;
    }

    let manifest_dir = PathBuf::from(
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set"),
    );
    let source = manifest_dir.join("src/mic_check.swift");
    let bin_dir = manifest_dir.join("bin");
    let binary = bin_dir.join("mic_check");
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".into());
    let target_binary = bin_dir.join(format!("mic_check-{}", target));

    println!("cargo:rerun-if-changed={}", source.display());
    std::fs::create_dir_all(&bin_dir).expect("failed to create helper bin dir");

    let output = Command::new("swiftc")
        .arg(&source)
        .arg("-o")
        .arg(&binary)
        .output()
        .expect("failed to run swiftc for mic_check");

    if !output.status.success() {
        panic!(
            "failed to compile mic_check.swift: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    std::fs::copy(&binary, &target_binary)
        .expect("failed to copy target-specific mic_check helper");
}

fn compile_system_audio_helper() {
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    if target_os != "macos" {
        return;
    }

    let manifest_dir = PathBuf::from(
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set"),
    );
    let source = manifest_dir.join("src/system_audio_record.swift");
    let bin_dir = manifest_dir.join("bin");
    let binary = bin_dir.join("system_audio_record");
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".into());
    let target_binary = bin_dir.join(format!("system_audio_record-{}", target));

    println!("cargo:rerun-if-changed={}", source.display());
    std::fs::create_dir_all(&bin_dir).expect("failed to create helper bin dir");

    let output = Command::new("swiftc")
        .args(["-parse-as-library"])
        .arg(&source)
        .arg("-o")
        .arg(&binary)
        .output()
        .expect("failed to run swiftc for system_audio_record");

    if !output.status.success() {
        panic!(
            "failed to compile system_audio_record.swift: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    std::fs::copy(&binary, &target_binary)
        .expect("failed to copy target-specific system_audio_record helper");
}

fn compile_calendar_helper() {
    // The Swift EventKit helper (`calendar-events`) is the fast, non-intrusive
    // path for reading Apple Calendar. Without it, callers fall through to
    // AppleScript — which, since PR #164, returns empty unless Calendar.app
    // is already running. Bundling the helper inside the signed .app means
    // release DMG users get real calendar data regardless of Calendar.app state.
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    if target_os != "macos" {
        return;
    }

    let manifest_dir = PathBuf::from(
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set"),
    );
    let repo_root = manifest_dir.join("../..");
    let source = repo_root.join("scripts/calendar-events.swift");
    let info_plist = repo_root.join("scripts/calendar-helper-Info.plist");
    // Output goes to `bin/` (not `resources/`) so Tauri treats it as an
    // externalBin. That way it lands at `Contents/MacOS/calendar-events`
    // in the packaged .app and gets signed + notarized in lockstep with
    // the main binary. Declaring it under `resources` in earlier
    // releases placed it at `Contents/Resources/resources/calendar-events`
    // unsigned, which failed notarization (see #183-followup).
    let bin_dir = manifest_dir.join("bin");
    let binary = bin_dir.join("calendar-events");
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".into());
    let target_binary = bin_dir.join(format!("calendar-events-{}", target));

    println!("cargo:rerun-if-changed={}", source.display());
    println!("cargo:rerun-if-changed={}", info_plist.display());

    fs::create_dir_all(&bin_dir).expect("failed to create helper bin dir");

    // Mirrors `scripts/build.sh` / `scripts/install-dev-app.sh`: `-sectcreate
    // __TEXT __info_plist` embeds the plist so macOS can display the
    // NSCalendarsFullAccessUsageDescription string on the EventKit prompt.
    let output = Command::new("swiftc")
        .arg("-O")
        .args(["-Xlinker", "-sectcreate"])
        .args(["-Xlinker", "__TEXT"])
        .args(["-Xlinker", "__info_plist"])
        .arg("-Xlinker")
        .arg(&info_plist)
        .arg(&source)
        .arg("-o")
        .arg(&binary)
        .output()
        .expect("failed to run swiftc for calendar-events");

    if !output.status.success() {
        panic!(
            "failed to compile calendar-events.swift: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    // Tauri's externalBin convention looks for `<base>-<target-triple>`,
    // so copy the fresh build to the target-suffixed path alongside the
    // base filename. Matches `compile_system_audio_helper` above.
    fs::copy(&binary, &target_binary)
        .expect("failed to copy target-specific calendar-events helper");
}

fn stage_assistant_skill_bundle() {
    let manifest_dir = PathBuf::from(
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set"),
    );
    let repo_root = manifest_dir.join("../..");
    let resources_root = manifest_dir
        .join("resources")
        .join("assistant-skill-bundle");

    let sources = [
        ("agents-skills", repo_root.join(".agents").join("skills")),
        (
            "opencode-skills",
            repo_root.join(".opencode").join("skills"),
        ),
        (
            "opencode-commands",
            repo_root.join(".opencode").join("commands"),
        ),
    ];

    for (_, source) in &sources {
        println!("cargo:rerun-if-changed={}", source.display());
    }

    if resources_root.exists() {
        fs::remove_dir_all(&resources_root).expect("failed to clear staged assistant skill bundle");
    }
    fs::create_dir_all(&resources_root).expect("failed to create assistant skill bundle root");

    for (relative_name, source) in &sources {
        copy_dir_recursive(source, &resources_root.join(relative_name))
            .unwrap_or_else(|error| panic!("failed to stage {}: {}", source.display(), error));
    }
}

fn copy_dir_recursive(source: &Path, target: &Path) -> std::io::Result<()> {
    fs::create_dir_all(target)?;
    for entry in fs::read_dir(source)? {
        let entry = entry?;
        let from = entry.path();
        let to = target.join(entry.file_name());
        let metadata = entry.metadata()?;
        if metadata.is_dir() {
            copy_dir_recursive(&from, &to)?;
        } else {
            if let Some(parent) = to.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::copy(&from, &to)?;
        }
    }
    Ok(())
}
