//! The runtime config the device reads from the card (`typoena.conf`), plus the
//! derive ladder that pre-fills it from this Mac. Mirrors the `_write-conf`
//! recipe in `firmware/justfile`.

use std::process::Command;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Field {
    WifiSsid,
    WifiPass,
    RemoteUrl,
    GhUser,
    Pat,
    AuthorName,
    AuthorEmail,
}

impl Field {
    pub const ALL: [Field; 7] = [
        Field::WifiSsid,
        Field::WifiPass,
        Field::RemoteUrl,
        Field::GhUser,
        Field::Pat,
        Field::AuthorName,
        Field::AuthorEmail,
    ];

    pub fn label(self) -> &'static str {
        match self {
            Field::WifiSsid => "Wi-Fi SSID",
            Field::WifiPass => "Wi-Fi password",
            Field::RemoteUrl => "Git remote URL",
            Field::GhUser => "GitHub username",
            Field::Pat => "GitHub PAT",
            Field::AuthorName => "Commit author name",
            Field::AuthorEmail => "Commit author email",
        }
    }

    pub fn secret(self) -> bool {
        matches!(self, Field::WifiPass | Field::Pat)
    }

    /// The `typoena.conf` key this field writes.
    pub fn conf_key(self) -> &'static str {
        match self {
            Field::WifiSsid => "TW_WIFI_SSID",
            Field::WifiPass => "TW_WIFI_PASS",
            Field::RemoteUrl => "TW_REMOTE_URL",
            Field::GhUser => "TW_GH_USER",
            Field::Pat => "TW_PAT",
            Field::AuthorName => "TW_AUTHOR_NAME",
            Field::AuthorEmail => "TW_AUTHOR_EMAIL",
        }
    }

    /// Required to ship a working device (a blank one bricks). Matches the
    /// four-var guard in `firmware/build.rs` minus the runtime-defaulted author.
    pub fn required(self) -> bool {
        matches!(self, Field::WifiSsid | Field::RemoteUrl | Field::Pat)
    }
}

#[derive(Default)]
pub struct Config {
    pub wifi_ssid: String,
    pub wifi_pass: String,
    pub remote_url: String,
    pub gh_user: String,
    pub pat: String,
    pub author_name: String,
    pub author_email: String,
}

impl Config {
    /// Pre-fill from this Mac (the derive ladder). The PAT is never derived —
    /// a broad token on plaintext removable media defeats the scoped-token model.
    pub fn derived() -> Self {
        Config {
            wifi_ssid: active_wifi_ssid().unwrap_or_default(),
            wifi_pass: String::new(),
            remote_url: String::new(),
            gh_user: gh_login().unwrap_or_default(),
            pat: String::new(),
            author_name: git_config("user.name").unwrap_or_default(),
            author_email: git_config("user.email").unwrap_or_default(),
        }
    }

    pub fn get(&self, f: Field) -> &str {
        match f {
            Field::WifiSsid => &self.wifi_ssid,
            Field::WifiPass => &self.wifi_pass,
            Field::RemoteUrl => &self.remote_url,
            Field::GhUser => &self.gh_user,
            Field::Pat => &self.pat,
            Field::AuthorName => &self.author_name,
            Field::AuthorEmail => &self.author_email,
        }
    }

    pub fn get_mut(&mut self, f: Field) -> &mut String {
        match f {
            Field::WifiSsid => &mut self.wifi_ssid,
            Field::WifiPass => &mut self.wifi_pass,
            Field::RemoteUrl => &mut self.remote_url,
            Field::GhUser => &mut self.gh_user,
            Field::Pat => &mut self.pat,
            Field::AuthorName => &mut self.author_name,
            Field::AuthorEmail => &mut self.author_email,
        }
    }

    /// Required fields still blank (the SD step refuses to write a dead conf).
    pub fn missing_required(&self) -> Vec<Field> {
        Field::ALL
            .iter()
            .copied()
            .filter(|f| f.required() && self.get(*f).trim().is_empty())
            .collect()
    }

    /// Render the `typoena.conf` body — written to the card by the SD step.
    pub fn to_conf(&self) -> String {
        let mut s = String::new();
        s.push_str("# Typoena runtime config — generated by the installer.\n");
        s.push_str("# Plaintext secrets on removable media: keep the card safe; scope TW_PAT\n");
        s.push_str("# to contents:write on just the notes repo.\n");
        for f in Field::ALL {
            s.push_str(f.conf_key());
            s.push('=');
            s.push_str(self.get(f));
            s.push('\n');
        }
        s
    }
}

fn git_config(key: &str) -> Option<String> {
    non_empty(Command::new("git").args(["config", key]).output().ok()?)
}

fn gh_login() -> Option<String> {
    non_empty(
        Command::new("gh")
            .args(["api", "user", "--jq", ".login"])
            .output()
            .ok()?,
    )
}

fn active_wifi_ssid() -> Option<String> {
    // The Wi-Fi device on a Mac is usually en0; ask networksetup for its SSID.
    // Didn't work, but this command did for me:
    // networksetup -listpreferredwirelessnetworks en0 | grep -v '^Preferred networks on' | head -1 | xargs
    let out = Command::new("networksetup")
        .args(["-getairportnetwork", "en0"])
        .output()
        .ok()?;
    if !out.status.success() {
        return None;
    }
    String::from_utf8_lossy(&out.stdout)
        .lines()
        .find_map(|l| l.strip_prefix("Current Wi-Fi Network: "))
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

/// Look up a saved Wi-Fi password in the System keychain. Pops a macOS auth
/// dialog, so call only on explicit user action. Mirrors the justfile's
/// `security find-generic-password -wa <ssid>`.
pub fn keychain_wifi_password(ssid: &str) -> Option<String> {
    if ssid.trim().is_empty() {
        return None;
    }
    non_empty(
        Command::new("security")
            .args(["find-generic-password", "-wa", ssid])
            .output()
            .ok()?,
    )
}

fn non_empty(out: std::process::Output) -> Option<String> {
    if !out.status.success() {
        return None;
    }
    let v = String::from_utf8_lossy(&out.stdout).trim().to_string();
    (!v.is_empty()).then_some(v)
}
