//! Command tree (DESIGN.md §16.5). v0: new / build / launch / doctor; the remaining
//! porcelain (sign / pack / lint / script) lands with M6–M8.

use std::path::PathBuf;

use clap::{Parser, Subcommand};

use crate::meta;
use crate::ops;
use crate::targets;

#[derive(Parser)]
#[command(
    name = "day",
    version = env!("DAY_VERSION_LONG"),
    about = "Day — cross-platform apps in Rust with native toolkits"
)]
struct Cli {
    /// Project directory (default: nearest ancestor with day.yaml)
    #[arg(long, global = true)]
    project: Option<PathBuf>,
    /// Output format: plain (default) or json (NDJSON result events)
    #[arg(long, global = true, default_value = "plain")]
    format: String,
    #[command(subcommand)]
    command: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Print the version, build profile (`*` = debug), and the git ref it was built from
    Version,
    /// Scaffold a new Day project — an app, a piece, or a part (interactive when run bare)
    New {
        #[command(subcommand)]
        what: Option<NewKind>,
    },
    /// Build the app for one or more targets
    Build {
        #[arg(short = 'p', long = "platform", required = true)]
        platforms: Vec<String>,
        #[arg(long, default_value = "debug")]
        profile: String,
    },
    /// Build + launch on one or more targets (in parallel)
    Launch {
        #[arg(short = 'p', long = "platform", required = true)]
        platforms: Vec<String>,
        #[arg(long, default_value = "debug")]
        profile: String,
        /// BCP-47 locale override passed to the app
        #[arg(long)]
        locale: Option<String>,
        /// Extra environment K=V passed to the app (repeatable)
        #[arg(long = "env")]
        envs: Vec<String>,
        /// Exit after launch instead of staying attached to logs
        #[arg(long)]
        detach: bool,
        /// dayscript file(s) to execute after launch (repeatable; implies detach)
        #[arg(long = "script")]
        scripts: Vec<PathBuf>,
    },
    /// Build + sign + produce installable artifacts (.dmg / .ipa / .apk+.aab / .flatpak / .msix+setup.exe / .hap)
    Pack {
        #[arg(short = 'p', long = "platform", required = true)]
        platforms: Vec<String>,
        /// Pack defaults to release (distribution artifacts); pass debug for a dev-install pack.
        #[arg(long, default_value = "release")]
        profile: String,
        /// Comma-separated format subset (e.g. `--formats apk` to skip the aab)
        #[arg(long)]
        formats: Option<String>,
        /// Skip signing entirely (artifacts are marked unsigned)
        #[arg(long)]
        no_sign: bool,
        /// Sign but skip notarization (macOS)
        #[arg(long)]
        no_notarize: bool,
        /// Submit for notarization without waiting (check later: day sign --notarize-status <id>)
        #[arg(long)]
        no_wait: bool,
    },
    /// Signing utilities: --check validates day.yaml signing config (never prints secrets)
    Sign {
        /// Validate signing config resolvability (env vars set, files present)
        #[arg(long)]
        check: bool,
        /// Poll an async notarization submission by id
        #[arg(long = "notarize-status")]
        notarize_status: Option<String>,
    },
    /// Check the development environment, grouped by toolkit
    Doctor {
        /// Focus a toolkit (repeatable): its checks become errors + print setup help.
        /// One of: appkit, uikit, gtk, qt, winui, android, harmonyos.
        #[arg(long = "toolkit")]
        toolkits: Vec<String>,
    },
    /// Check the project for common errors (fluent coverage, ids)
    Lint {
        /// Exit non-zero (10) when findings exist
        #[arg(long)]
        strict: bool,
    },
    /// HarmonyOS / OpenHarmony helpers (emulator, …)
    Ohos {
        #[command(subcommand)]
        cmd: OhosCmd,
    },
    /// PLUMBING: invoked by the Xcode script phase (reads Xcode's env)
    #[command(name = "xcode-backend", hide = true)]
    XcodeBackend {
        #[arg(default_value = "build")]
        action: String,
    },
    /// PLUMBING: invoked by the gradle scaffold (reads DAY_* env)
    #[command(name = "gradle-backend", hide = true)]
    GradleBackend {
        #[arg(default_value = "build")]
        action: String,
    },
}

/// `day new <piece|part|app>` — scaffold an extension crate or app; `day new` (bare) walks an
/// interactive dialog. Every value-carrying flag has an equivalent question in the dialog (the dialog
/// is the fallback branch of the flags — see `new.rs`), so a value not passed on the command line is
/// asked for when a terminal is present, and defaulted (or reported as required) when it is not. The
/// meta flags `--local` (CI) and `--no-input` have no dialog fallback by design.
///
/// Scaffolds default to REMOTE (git) day dependencies so they are self-contained; the hidden
/// `--local <path>` (or `DAY_LOCAL` env) redirects to a local day checkout for CI smoke-tests of a
/// freshly-scaffolded project against the day tree under test.
#[derive(Subcommand)]
enum NewKind {
    /// Scaffold a Day PIECE crate (a reusable widget). No `--toolkits` ⇒ a COMPOSITE piece.
    Piece {
        /// Crate name (prompted if omitted in an interactive terminal).
        name: Option<String>,
        /// Comma-separated toolkits for a NATIVE piece (appkit,gtk,qt,uikit,widget,winui).
        /// Omit for a COMPOSITE piece (pure composition; works on every backend with no per-backend code).
        #[arg(long)]
        toolkits: Option<String>,
        /// Force a COMPOSITE piece even if `--toolkits` is given.
        #[arg(long)]
        composite: bool,
        /// Package id (reverse-DNS); default `dev.example.<name>`. Also the piece KIND + Java package.
        #[arg(long)]
        id: Option<String>,
        /// Scaffold `day` deps from the git remote instead of the default (crates.io, pinned to this
        /// CLI's version) — useful before a matching release is published.
        #[arg(long)]
        git: bool,
        /// Use `path` deps rooted at a local day checkout (CI / framework development).
        #[arg(long, hide = true)]
        local: Option<PathBuf>,
        /// Never prompt; use flags + defaults only (also implied when stdin is not a terminal).
        #[arg(long)]
        no_input: bool,
    },
    /// Scaffold a Day PART crate (a headless, UI-less capability).
    Part {
        /// Crate name (prompted if omitted in an interactive terminal).
        name: Option<String>,
        /// Comma-separated platforms (macos,ios,android,linux,windows); default: all.
        #[arg(long)]
        platforms: Option<String>,
        /// Package id (reverse-DNS); default `dev.example.<name>`. Also the Java package.
        #[arg(long)]
        id: Option<String>,
        /// Scaffold `day` deps from the git remote instead of the default (crates.io, pinned to this
        /// CLI's version) — useful before a matching release is published.
        #[arg(long)]
        git: bool,
        /// Use `path` deps rooted at a local day checkout (CI / framework development).
        #[arg(long, hide = true)]
        local: Option<PathBuf>,
        /// Never prompt; use flags + defaults only (also implied when stdin is not a terminal).
        #[arg(long)]
        no_input: bool,
    },
    /// Scaffold a new Day app (the canonical app command).
    App {
        /// App name (prompted if omitted in an interactive terminal).
        name: Option<String>,
        /// A target to support (repeatable): e.g. `--toolkit ios-uikit --toolkit macos-appkit`.
        /// Values may also be comma-separated. Omit to choose interactively.
        #[arg(long = "toolkit")]
        toolkits: Vec<String>,
        /// Application id / bundle id (reverse-DNS); default `dev.example.<name>`.
        #[arg(long)]
        appid: Option<String>,
        /// Alias for --appid (Android application id / Apple bundle id).
        #[arg(long)]
        bundleid: Option<String>,
        /// Back-compat alias for --appid.
        #[arg(long, hide = true)]
        id: Option<String>,
        /// Back-compat: comma-separated target list (prefer repeated --toolkit).
        #[arg(long, hide = true)]
        targets: Option<String>,
        /// Scaffold `day` deps from the git remote instead of the default (crates.io, pinned to this
        /// CLI's version) — useful before a matching release is published.
        #[arg(long)]
        git: bool,
        /// Use `path` deps rooted at a local day checkout (CI / framework development).
        #[arg(long, hide = true)]
        local: Option<PathBuf>,
        /// Never prompt; use flags + defaults only (also implied when stdin is not a terminal).
        #[arg(long)]
        no_input: bool,
    },
}

#[derive(clap::Subcommand)]
pub enum OhosCmd {
    /// Manage the OpenHarmony emulator
    Emulator {
        #[command(subcommand)]
        cmd: EmulatorCmd,
    },
}

#[derive(clap::Subcommand)]
pub enum EmulatorCmd {
    /// Launch the Oniro/OpenHarmony QEMU emulator as a native window (no VNC/password)
    Launch {
        /// No window (hdc-only) — for CI / headless hosts.
        #[arg(long)]
        headless: bool,
    },
}

pub fn run() -> i32 {
    let cli = Cli::parse();
    match cli.command {
        Cmd::Version => {
            println!("day {}", env!("DAY_VERSION_LONG"));
            0
        }
        Cmd::Doctor { toolkits } => crate::doctor::run(&toolkits),
        Cmd::Pack {
            platforms,
            profile,
            formats,
            no_sign,
            no_notarize,
            no_wait,
        } => with_project(cli.project.as_deref(), |project| {
            let opts = crate::pack::PackOptions {
                profile,
                formats: formats
                    .as_deref()
                    .map(|s| s.split(',').map(|f| f.trim().to_string()).collect()),
                no_sign,
                no_notarize,
                no_wait,
            };
            let mut outcomes = Vec::new();
            for p in &platforms {
                let Some(target) = targets::find(p) else {
                    eprintln!("error: unknown target {p:?}");
                    return 2;
                };
                match crate::pack::run(project, target, &opts) {
                    Ok(o) => outcomes.push(o),
                    Err(e) => {
                        eprintln!("error: {}", e.message());
                        return e.exit_code();
                    }
                }
            }
            if cli.format == "json" {
                print_pack_json(&outcomes);
            }
            0
        }),
        Cmd::Sign {
            check,
            notarize_status,
        } => with_project(cli.project.as_deref(), |project| {
            if let Some(id) = &notarize_status {
                return crate::sign::notarize_status(project, id);
            }
            if check {
                return crate::sign::check(project);
            }
            eprintln!("error: day sign needs --check or --notarize-status <id>");
            2
        }),
        Cmd::Lint { strict } => with_project(cli.project.as_deref(), |project| {
            crate::lint::run(project, strict)
        }),
        Cmd::Ohos {
            cmd:
                OhosCmd::Emulator {
                    cmd: EmulatorCmd::Launch { headless },
                },
        } => match crate::ohos::emulator_launch(headless) {
            Ok(()) => 0,
            Err(e) => {
                eprintln!("error: {e}");
                5
            }
        },
        Cmd::XcodeBackend { .. } => crate::mobile::xcode_backend_build(),
        Cmd::GradleBackend { .. } => crate::mobile::gradle_backend_build(),
        Cmd::New { what } => match what {
            None => crate::new::interactive(),
            Some(NewKind::Piece {
                name,
                toolkits,
                composite,
                id,
                git,
                local,
                no_input,
            }) => crate::new::piece(
                name.as_deref(),
                toolkits.as_deref(),
                composite,
                id.as_deref(),
                local.as_deref(),
                git,
                no_input,
            ),
            Some(NewKind::Part {
                name,
                platforms,
                id,
                git,
                local,
                no_input,
            }) => crate::new::part(
                name.as_deref(),
                platforms.as_deref(),
                id.as_deref(),
                local.as_deref(),
                git,
                no_input,
            ),
            Some(NewKind::App {
                name,
                toolkits,
                appid,
                bundleid,
                id,
                targets,
                git,
                local,
                no_input,
            }) => crate::new::app(
                name.as_deref(),
                &toolkits,
                appid.as_deref(),
                bundleid.as_deref(),
                id.as_deref(),
                targets.as_deref(),
                local.as_deref(),
                git,
                no_input,
            ),
        },
        Cmd::Build { platforms, profile } => with_project(cli.project.as_deref(), |project| {
            let mut results = Vec::new();
            for p in &platforms {
                let target = match targets::find(p) {
                    Some(t) => t,
                    None => {
                        eprintln!("error: unknown target {p:?}");
                        return 2;
                    }
                };
                match ops::build(project, target, &profile) {
                    Ok(o) => {
                        ops::status(
                            "Built",
                            &format!(
                                "{} → {} ({:.1}s)",
                                o.target,
                                o.artifact.display(),
                                o.seconds
                            ),
                        );
                        results.push(o);
                    }
                    Err(e) => {
                        eprintln!("error: {e}");
                        return 4;
                    }
                }
            }
            if cli.format == "json" {
                print_result_json("build", &results);
            }
            0
        }),
        Cmd::Launch {
            platforms,
            profile,
            locale,
            envs,
            detach,
            scripts,
        } => with_project(cli.project.as_deref(), |project| {
            let script_mode = !scripts.is_empty();
            let mut spec = ops::LaunchSpec {
                locale: locale.clone(),
                envs: envs
                    .iter()
                    .filter_map(|kv| kv.split_once('=').map(|(k, v)| (k.into(), v.into())))
                    .collect(),
                attached: !detach && !script_mode,
            };
            // Ctrl-C during an attached run must take the launched apps and their log
            // watchers (simctl / adb logcat) down too — not leave them orphaned.
            if spec.attached {
                crate::signals::install();
            }
            let token = crate::script::make_token();
            let mut handles = Vec::new();
            let mut script_failures = 0usize;
            for (ti, p) in platforms.iter().enumerate() {
                let port = crate::script::pick_port(ti);
                if script_mode {
                    spec.envs
                        .retain(|(k, _)| k != "DAYSCRIPT_PORT" && k != "DAYSCRIPT_TOKEN");
                    spec.envs.push(("DAYSCRIPT_PORT".into(), port.to_string()));
                    spec.envs.push(("DAYSCRIPT_TOKEN".into(), token.clone()));
                }
                let target = match targets::find(p) {
                    Some(t) => t,
                    None => {
                        eprintln!("error: unknown target {p:?}");
                        return 2;
                    }
                };
                let outcome = match ops::build(project, target, &profile) {
                    Ok(o) => o,
                    Err(e) => {
                        eprintln!("error: {e}");
                        return 4;
                    }
                };
                match ops::launch(project, target, &outcome, &spec) {
                    Ok(h) => handles.push(h),
                    Err(e) => {
                        eprintln!("error: {e}");
                        return 1;
                    }
                }
                if script_mode {
                    match crate::script::run_scripts(
                        project,
                        target,
                        port,
                        &token,
                        &scripts,
                        locale.as_deref(),
                    ) {
                        Ok(run) => {
                            script_failures += run.steps_failed;
                            ops::status(
                                "Script",
                                &format!(
                                    "{}: {}/{} steps passed · {} screenshot(s)",
                                    target.name,
                                    run.steps_total - run.steps_failed,
                                    run.steps_total,
                                    run.screenshots.len()
                                ),
                            );
                        }
                        Err(e) => {
                            eprintln!("error: {e}");
                            return 5;
                        }
                    }
                }
            }
            if script_mode {
                return if script_failures > 0 { 5 } else { 0 };
            }
            if spec.attached {
                let mut code = 0;
                for h in handles {
                    code = code.max(h.join().unwrap_or(1));
                }
                // A target that exited on its own leaves its siblings' log watchers (and
                // any child that outlives its stream) running — reap them before we go.
                crate::signals::kill_all();
                code
            } else {
                0
            }
        }),
    }
}

fn with_project(start: Option<&std::path::Path>, f: impl FnOnce(&meta::Project) -> i32) -> i32 {
    match meta::find_project(start) {
        Ok(p) => f(&p),
        Err(e) => {
            eprintln!("error: {e}");
            2
        }
    }
}

fn print_pack_json(outcomes: &[crate::pack::PackOutcome]) {
    let targets: Vec<serde_json::Value> = outcomes
        .iter()
        .map(|o| {
            let artifacts: Vec<serde_json::Value> = o
                .artifacts
                .iter()
                .map(|a| {
                    serde_json::json!({
                        "path": a.path, "kind": a.kind,
                        "sha256": a.sha256, "signed": a.tier.as_str(),
                    })
                })
                .collect();
            serde_json::json!({
                "target": o.target, "ok": true, "code": 0,
                "artifacts": artifacts, "seconds": o.seconds,
            })
        })
        .collect();
    println!(
        "{}",
        serde_json::json!({"event": "result", "command": "pack", "ok": true, "targets": targets})
    );
}

fn print_result_json(command: &str, results: &[ops::BuildOutcome]) {
    let targets: Vec<serde_json::Value> = results
        .iter()
        .map(|o| {
            serde_json::json!({
                "target": o.target, "ok": true, "code": 0,
                "artifacts": [{"path": o.artifact}], "seconds": o.seconds,
            })
        })
        .collect();
    println!(
        "{}",
        serde_json::json!({"event": "result", "command": command, "ok": true, "targets": targets})
    );
}
