// rust-style-allow: large-file - skill CLI parsing keeps shared state and
// option finalization in one module until the native parser surface stabilizes.
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};

use runx_contracts::JsonValue;
use runx_runtime::orchestrator::LocalCredentialDescriptor;
use runx_runtime::{resolve_path_from_user_input, resolve_runx_home_dir};
use serde::Deserialize;

use super::inputs::{parse_direct_input_arg, parse_input_arg, parse_json_input_arg};
use super::{SkillAction, SkillPlan};

pub fn parse_skill_plan(args: &[OsString]) -> Result<SkillPlan, String> {
    let mut state = SkillParseState::default();
    let mut index = 1;

    while index < args.len() {
        index = parse_skill_arg(args, index, &mut state)?;
        index += 1;
    }

    let env = env::vars().collect();
    let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    let local_credential = finalize_local_credential(&state, &env, &cwd)?;

    let Some(skill_path) = state.skill_path.as_ref() else {
        return Err("runx skill requires a skill package path".to_owned());
    };
    reject_resolver_flags_for_skill_management_action(skill_path, &state)?;
    let skill_path = skill_path.clone();
    let action = if state.inspect {
        SkillAction::Inspect
    } else {
        SkillAction::Run
    };

    Ok(SkillPlan {
        action,
        skill_path,
        runner: state.runner,
        receipt_dir: state.receipt_dir,
        run_id: state.run_id,
        answers: state.answers,
        registry: state.registry,
        expected_digest: state.expected_digest,
        json: state.json,
        inputs: state.inputs,
        local_credential,
    })
}

#[derive(Default)]
struct SkillParseState {
    skill_path: Option<PathBuf>,
    runner: Option<String>,
    receipt_dir: Option<PathBuf>,
    run_id: Option<String>,
    answers: Option<PathBuf>,
    registry: Option<String>,
    expected_digest: Option<String>,
    json: bool,
    inspect: bool,
    force_run: bool,
    inputs: BTreeMap<String, JsonValue>,
    credential: Option<CredentialBinding>,
    credential_profile: Option<String>,
    secret_env: Option<(String, String)>,
}

struct CredentialBinding {
    provider: String,
    auth_mode: String,
    material_ref: String,
    scopes: Vec<String>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CredentialProfilesFile {
    profiles: BTreeMap<String, CredentialProfile>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CredentialProfile {
    credential: String,
    secret_env: String,
}

fn parse_credential_binding(value: &str) -> Result<CredentialBinding, String> {
    let (provider, rest) = value.split_once(':').ok_or_else(credential_usage_error)?;
    let provider = non_empty_credential_part(provider)?;
    let (auth_mode, rest) = rest.split_once(':').ok_or_else(credential_usage_error)?;
    let auth_mode = non_empty_credential_part(auth_mode)?;
    let (material_ref, scopes) = split_material_ref_and_scopes(rest)?;
    Ok(CredentialBinding {
        provider: provider.to_owned(),
        auth_mode: auth_mode.to_owned(),
        material_ref,
        scopes,
    })
}

fn split_material_ref_and_scopes(value: &str) -> Result<(String, Vec<String>), String> {
    let value = non_empty_credential_part(value)?;
    let Some(index) = value.rfind(':') else {
        return Ok((value.to_owned(), Vec::new()));
    };
    if value[index..].starts_with("://") {
        return Ok((value.to_owned(), Vec::new()));
    }
    let material_ref = non_empty_credential_part(&value[..index])?.to_owned();
    let scopes = value[index + 1..]
        .split(',')
        .map(str::trim)
        .filter(|scope| !scope.is_empty())
        .map(ToOwned::to_owned)
        .collect();
    Ok((material_ref, scopes))
}

fn non_empty_credential_part(value: &str) -> Result<&str, String> {
    let value = value.trim();
    if value.is_empty() {
        return Err(credential_usage_error());
    }
    Ok(value)
}

fn credential_usage_error() -> String {
    "runx skill --credential requires <provider>:<auth_mode>:<material_ref>".to_owned()
}

fn parse_secret_env(value: &str) -> Result<(String, String), String> {
    parse_secret_env_from(value, |name| env::var(name).ok())
}

fn parse_secret_env_from(
    value: &str,
    lookup: impl Fn(&str) -> Option<String>,
) -> Result<(String, String), String> {
    if value.contains('=') {
        return Err(
            "runx skill --secret-env accepts an environment variable name, not an inline value"
                .to_owned(),
        );
    }
    let name = value.trim();
    if name.is_empty() {
        return Err("runx skill --secret-env requires a non-empty env var name".to_owned());
    }
    let secret = lookup(name)
        .ok_or_else(|| format!("runx skill --secret-env env var '{name}' is not set"))?;
    if secret.trim().is_empty() {
        return Err("runx skill --secret-env requires a non-empty secret value".to_owned());
    }
    Ok((name.to_owned(), secret.to_owned()))
}

fn finalize_local_credential(
    state: &SkillParseState,
    env: &BTreeMap<String, String>,
    cwd: &Path,
) -> Result<Option<LocalCredentialDescriptor>, String> {
    if let Some(profile) = state.credential_profile.as_ref() {
        if state.credential.is_some() || state.secret_env.is_some() {
            return Err(
                "runx skill --credential-profile cannot be combined with --credential or --secret-env"
                    .to_owned(),
            );
        }
        let profile = load_credential_profile(profile, env, cwd)?;
        let credential = parse_credential_binding(&profile.credential)?;
        let secret_env = parse_secret_env_from(&profile.secret_env, |name| env.get(name).cloned())?;
        return Ok(Some(local_credential_descriptor(&credential, &secret_env)));
    }
    match (&state.credential, &state.secret_env) {
        (None, None) => Ok(None),
        (Some(_), None) => {
            Err("runx skill --credential requires --secret-env <ENV_VAR>".to_owned())
        }
        (binding, Some((env_var, secret))) => {
            let binding = binding.as_ref().ok_or_else(|| {
                "runx skill --secret-env requires --credential <provider>:<auth_mode>:<material_ref>"
                    .to_owned()
            })?;
            Ok(Some(local_credential_descriptor(
                binding,
                &(env_var.clone(), secret.clone()),
            )))
        }
    }
}

fn local_credential_descriptor(
    binding: &CredentialBinding,
    secret_env: &(String, String),
) -> LocalCredentialDescriptor {
    LocalCredentialDescriptor {
        provider: binding.provider.clone(),
        auth_mode: binding.auth_mode.clone(),
        env_var: secret_env.0.clone(),
        material_ref: binding.material_ref.clone(),
        scopes: binding.scopes.clone(),
        secret: secret_env.1.clone(),
    }
}

fn load_credential_profile(
    profile_name: &str,
    env: &BTreeMap<String, String>,
    cwd: &Path,
) -> Result<CredentialProfile, String> {
    let profile_name = profile_name.trim();
    if profile_name.is_empty() {
        return Err("runx skill --credential-profile requires a non-empty name".to_owned());
    }
    let paths = credential_profile_paths(env, cwd);
    for path in &paths {
        let contents = match fs::read_to_string(path) {
            Ok(contents) => contents,
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
            Err(error) => {
                return Err(format!(
                    "runx skill could not read credential profile file {}: {error}",
                    path.display()
                ));
            }
        };
        let parsed: CredentialProfilesFile = serde_json::from_str(&contents).map_err(|error| {
            format!(
                "runx skill credential profile file {} is invalid JSON: {error}",
                path.display()
            )
        })?;
        if let Some(profile) = parsed.profiles.into_iter().find_map(|(name, profile)| {
            if name == profile_name {
                Some(profile)
            } else {
                None
            }
        }) {
            return Ok(profile);
        }
    }
    let searched = paths
        .iter()
        .map(|path| path.display().to_string())
        .collect::<Vec<_>>()
        .join(", ");
    Err(format!(
        "runx skill credential profile '{profile_name}' was not found; searched {searched}"
    ))
}

fn credential_profile_paths(env: &BTreeMap<String, String>, cwd: &Path) -> Vec<PathBuf> {
    if let Some(path) = env
        .get("RUNX_CREDENTIAL_PROFILES")
        .filter(|value| !value.trim().is_empty())
    {
        return vec![resolve_path_from_user_input(path, env, cwd, true)];
    }
    let mut paths = Vec::new();
    if let Some(project_dir) = env
        .get("RUNX_PROJECT_DIR")
        .filter(|value| !value.trim().is_empty())
        .map(|value| resolve_path_from_user_input(value, env, cwd, true))
        .or_else(|| nearest_project_runx_dir(cwd))
    {
        paths.push(project_dir.join("credentials.json"));
    }
    paths.push(resolve_runx_home_dir(env, cwd).join("credentials.json"));
    paths.dedup();
    paths
}

fn nearest_project_runx_dir(cwd: &Path) -> Option<PathBuf> {
    cwd.ancestors()
        .map(|ancestor| ancestor.join(".runx"))
        .find(|candidate| candidate.is_dir())
}

fn reject_resolver_flags_for_skill_management_action(
    skill_path: &Path,
    state: &SkillParseState,
) -> Result<(), String> {
    if state.registry.is_none() && state.expected_digest.is_none() {
        return Ok(());
    }
    if !is_skill_management_action(skill_path) {
        return Ok(());
    }
    Err("runx skill --registry and --digest are only supported when running a skill ref".to_owned())
}

fn is_skill_management_action(skill_path: &Path) -> bool {
    if skill_path.components().count() != 1 {
        return false;
    }
    matches!(
        skill_path.to_str(),
        Some("add" | "inspect" | "publish" | "search" | "validate")
    )
}

// rust-style-allow: long-function because this is the single skill-flag dispatch
// match (--receipt-dir/--json/--credential and positionals); splitting the
// arms would scatter the CLI parse contract.
fn parse_skill_arg(
    args: &[OsString],
    mut index: usize,
    state: &mut SkillParseState,
) -> Result<usize, String> {
    let token = string_arg(args, index)?;
    if is_retired_skill_option(&token) {
        return Err(
            "retired runx skill receipt option is not supported; use --receipt-dir".to_owned(),
        );
    }
    match token.as_str() {
        value if value.starts_with("--receipt-dir=") => {
            state.receipt_dir = Some(PathBuf::from(value.trim_start_matches("--receipt-dir=")));
        }
        value if value.starts_with("-R=") => {
            state.receipt_dir = Some(PathBuf::from(value.trim_start_matches("-R=")));
        }
        value if value.starts_with("--receipts=") => {
            state.receipt_dir = Some(PathBuf::from(value.trim_start_matches("--receipts=")));
        }
        "--receipt-dir" => {
            index += 1;
            state.receipt_dir = Some(PathBuf::from(string_arg(args, index)?));
        }
        "--receipts" => {
            index += 1;
            state.receipt_dir = Some(PathBuf::from(string_arg(args, index)?));
        }
        "-R" => {
            index += 1;
            state.receipt_dir = Some(PathBuf::from(string_arg(args, index)?));
        }
        value if value.starts_with("--run-id=") || value == "--run-id" => {
            return Err(skill_resume_flag_error());
        }
        value if value.starts_with("--answers=") || value == "--answers" => {
            return Err(skill_resume_flag_error());
        }
        value if value.starts_with("--runner=") || value == "--runner" => {
            return Err(
                "runx skill --runner is no longer supported; use `runx skill <skill> <runner>`"
                    .to_owned(),
            );
        }
        value if value.starts_with("--registry=") => {
            state.registry = Some(non_empty_flag_value(
                "--registry",
                value.trim_start_matches("--registry="),
            )?);
        }
        "--registry" => {
            index += 1;
            state.registry = Some(non_empty_flag_value(
                "--registry",
                &string_arg(args, index)?,
            )?);
        }
        value if value.starts_with("--digest=") => {
            state.expected_digest = Some(non_empty_flag_value(
                "--digest",
                value.trim_start_matches("--digest="),
            )?);
        }
        "--digest" => {
            index += 1;
            state.expected_digest =
                Some(non_empty_flag_value("--digest", &string_arg(args, index)?)?);
        }
        value if value.starts_with("--input=") => {
            index = parse_input_arg(
                args,
                index,
                Some(value.trim_start_matches("--input=")),
                &mut state.inputs,
            )?;
        }
        value if value.starts_with("--input-json=") => {
            index = parse_json_input_arg(
                args,
                index,
                Some(value.trim_start_matches("--input-json=")),
                &mut state.inputs,
            )?;
        }
        value if value.starts_with("-i=") => {
            index = parse_input_arg(
                args,
                index,
                Some(value.trim_start_matches("-i=")),
                &mut state.inputs,
            )?;
        }
        "--input" => index = parse_input_arg(args, index, None, &mut state.inputs)?,
        "--input-json" => index = parse_json_input_arg(args, index, None, &mut state.inputs)?,
        "-i" => index = parse_input_arg(args, index, None, &mut state.inputs)?,
        "--run" => state.force_run = true,
        value if value.starts_with("--credential=") => {
            state.credential = Some(parse_credential_binding(
                value.trim_start_matches("--credential="),
            )?);
        }
        "--credential" => {
            index += 1;
            state.credential = Some(parse_credential_binding(&string_arg(args, index)?)?);
        }
        value if value.starts_with("--credential-profile=") => {
            state.credential_profile = Some(non_empty_flag_value(
                "--credential-profile",
                value.trim_start_matches("--credential-profile="),
            )?);
        }
        value if value.starts_with("--profile=") => {
            state.credential_profile = Some(non_empty_flag_value(
                "--profile",
                value.trim_start_matches("--profile="),
            )?);
        }
        "--credential-profile" => {
            index += 1;
            state.credential_profile = Some(non_empty_flag_value(
                "--credential-profile",
                &string_arg(args, index)?,
            )?);
        }
        "--profile" | "-p" => {
            index += 1;
            state.credential_profile = Some(non_empty_flag_value(
                "--profile",
                &string_arg(args, index)?,
            )?);
        }
        value if value.starts_with("--secret-env=") => {
            state.secret_env = Some(parse_secret_env(value.trim_start_matches("--secret-env="))?);
        }
        "--secret-env" => {
            index += 1;
            state.secret_env = Some(parse_secret_env(&string_arg(args, index)?)?);
        }
        "--json" | "-j" => state.json = true,
        "--non-interactive" => {}
        value if value.starts_with("--") => {
            index = parse_direct_input_arg(args, index, value, &mut state.inputs)?;
        }
        value => {
            if state.skill_path.is_none() && value == "inspect" {
                state.inspect = true;
            } else if state.skill_path.is_none() {
                state.skill_path = Some(PathBuf::from(value));
            } else if state.runner.is_none() {
                state.runner = Some(value.to_owned());
            } else {
                return Err(format!("unexpected runx skill argument {value}"));
            }
        }
    }
    Ok(index)
}

fn non_empty_flag_value(flag: &str, value: &str) -> Result<String, String> {
    let value = value.trim();
    if value.is_empty() {
        return Err(format!("runx skill {flag} requires a non-empty value"));
    }
    Ok(value.to_owned())
}

fn skill_resume_flag_error() -> String {
    "runx skill continuation flags are no longer supported; use `runx resume <run-id> <answers.json>`".to_owned()
}

fn is_retired_skill_option(token: &str) -> bool {
    let Some(flag) = token.strip_prefix("--") else {
        return false;
    };
    let name = flag.split_once('=').map_or(flag, |(name, _value)| name);
    name == "receipt" || name == retired_receipt_dir_option_name()
}

fn retired_receipt_dir_option_name() -> String {
    ["receipt", "Dir"].concat()
}

fn string_arg(args: &[OsString], index: usize) -> Result<String, String> {
    let value = args
        .get(index)
        .ok_or_else(|| "missing value for runx skill argument".to_owned())?;
    value
        .to_str()
        .map(ToOwned::to_owned)
        .ok_or_else(|| "runx skill arguments must be UTF-8".to_owned())
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::{SkillAction, SkillParseState, finalize_local_credential};

    #[test]
    fn credential_profile_resolves_project_descriptor_and_env_secret() -> Result<(), String> {
        let root = unique_temp_dir("runx-credential-profile")?;
        let runx_dir = root.join(".runx");
        fs::create_dir_all(&runx_dir).map_err(|error| error.to_string())?;
        fs::write(
            runx_dir.join("credentials.json"),
            r#"{
  "profiles": {
    "example": {
      "credential": "example:bearer:local://example/internal:example.review",
      "secret_env": "INTERNAL_SYNC_SECRET"
    }
  }
}
"#,
        )
        .map_err(|error| error.to_string())?;

        let state = SkillParseState {
            credential_profile: Some("example".to_owned()),
            ..Default::default()
        };
        let env = [
            (
                "RUNX_PROJECT_DIR".to_owned(),
                runx_dir.to_string_lossy().into_owned(),
            ),
            ("INTERNAL_SYNC_SECRET".to_owned(), "secret-value".to_owned()),
        ]
        .into_iter()
        .collect();
        let credential = finalize_local_credential(&state, &env, &root)?
            .ok_or_else(|| "credential profile did not resolve".to_owned())?;

        assert_eq!(credential.provider, "example");
        assert_eq!(credential.auth_mode, "bearer");
        assert_eq!(credential.material_ref, "local://example/internal");
        assert_eq!(credential.env_var, "INTERNAL_SYNC_SECRET");
        assert_eq!(credential.secret, "secret-value");
        assert_eq!(credential.scopes, vec!["example.review"]);

        fs::remove_dir_all(root).map_err(|error| error.to_string())?;
        Ok(())
    }

    #[test]
    fn credential_profile_rejects_manual_credential_flags() -> Result<(), String> {
        let state = SkillParseState {
            credential_profile: Some("example".to_owned()),
            secret_env: Some(("TOKEN".to_owned(), "secret".to_owned())),
            ..Default::default()
        };
        let error = finalize_local_credential(&state, &Default::default(), &std::env::temp_dir())
            .err()
            .ok_or_else(|| "profile unexpectedly combined with manual flags".to_owned())?;
        assert!(error.contains("cannot be combined"));
        Ok(())
    }

    #[test]
    fn short_human_flags_parse_for_skill_runs() -> Result<(), String> {
        let args = [
            "skill",
            "-p",
            "operator",
            "-i",
            "claim=abc",
            "-R",
            "receipts",
            "-j",
        ]
        .into_iter()
        .map(std::ffi::OsString::from)
        .collect::<Vec<_>>();
        let mut state = SkillParseState::default();
        let mut index = 1;
        while index < args.len() {
            index = super::parse_skill_arg(&args, index, &mut state)?;
            index += 1;
        }
        assert_eq!(state.credential_profile.as_deref(), Some("operator"));
        assert_eq!(
            state.inputs.get("claim"),
            Some(&runx_contracts::JsonValue::String("abc".to_owned()))
        );
        assert_eq!(
            state.receipt_dir.as_deref(),
            Some(std::path::Path::new("receipts"))
        );
        assert!(state.json);
        Ok(())
    }

    #[test]
    fn input_json_parses_strict_json_values() -> Result<(), String> {
        let args = [
            "skill",
            "skills/data-store",
            "--input-json",
            "event",
            r#"{"type":"posting.claimed","payload":{"actor":"agent-9"}}"#,
            "--input-json=limits={\"rows\":10}",
        ]
        .into_iter()
        .map(std::ffi::OsString::from)
        .collect::<Vec<_>>();
        let plan = super::parse_skill_plan(&args)?;
        assert_eq!(plan.action, SkillAction::Run);

        assert_eq!(
            plan.inputs
                .get("event")
                .and_then(runx_contracts::JsonValue::as_object)
                .and_then(|event| event.get("type"))
                .and_then(runx_contracts::JsonValue::as_str),
            Some("posting.claimed")
        );
        let rows = plan
            .inputs
            .get("limits")
            .and_then(runx_contracts::JsonValue::as_object)
            .and_then(|limits| limits.get("rows"))
            .and_then(|rows| match rows {
                runx_contracts::JsonValue::Number(number) => number.as_f64(),
                _ => None,
            });
        assert_eq!(rows, Some(10.0));
        Ok(())
    }

    #[test]
    fn skill_without_inputs_executes_default_runner() -> Result<(), String> {
        let args = ["skill", "skills/messageboard"]
            .into_iter()
            .map(std::ffi::OsString::from)
            .collect::<Vec<_>>();
        let plan = super::parse_skill_plan(&args)?;

        assert_eq!(plan.action, SkillAction::Run);
        assert_eq!(
            plan.skill_path,
            std::path::PathBuf::from("skills/messageboard")
        );
        assert_eq!(plan.runner, None);
        Ok(())
    }

    #[test]
    fn positional_runner_without_inputs_executes_runner() -> Result<(), String> {
        let args = ["skill", "skills/messageboard", "post_and_append"]
            .into_iter()
            .map(std::ffi::OsString::from)
            .collect::<Vec<_>>();
        let plan = super::parse_skill_plan(&args)?;

        assert_eq!(plan.action, SkillAction::Run);
        assert_eq!(plan.runner.as_deref(), Some("post_and_append"));
        Ok(())
    }

    #[test]
    fn explicit_inspect_returns_skill_card() -> Result<(), String> {
        let args = ["skill", "inspect", "skills/messageboard", "post_and_append"]
            .into_iter()
            .map(std::ffi::OsString::from)
            .collect::<Vec<_>>();
        let plan = super::parse_skill_plan(&args)?;

        assert_eq!(plan.action, SkillAction::Inspect);
        assert_eq!(
            plan.skill_path,
            std::path::PathBuf::from("skills/messageboard")
        );
        assert_eq!(plan.runner.as_deref(), Some("post_and_append"));
        Ok(())
    }

    #[test]
    fn positional_runner_with_inputs_executes_runner() -> Result<(), String> {
        let args = [
            "skill",
            "skills/messageboard",
            "post_and_append",
            "-i",
            "title=hello",
        ]
        .into_iter()
        .map(std::ffi::OsString::from)
        .collect::<Vec<_>>();
        let plan = super::parse_skill_plan(&args)?;

        assert_eq!(plan.action, SkillAction::Run);
        assert_eq!(plan.runner.as_deref(), Some("post_and_append"));
        assert_eq!(
            plan.inputs.get("title"),
            Some(&runx_contracts::JsonValue::String("hello".to_owned()))
        );
        Ok(())
    }

    #[test]
    fn run_flag_executes_zero_input_runner() -> Result<(), String> {
        let args = ["skill", "skills/ops-desk", "refresh", "--run"]
            .into_iter()
            .map(std::ffi::OsString::from)
            .collect::<Vec<_>>();
        let plan = super::parse_skill_plan(&args)?;

        assert_eq!(plan.action, SkillAction::Run);
        assert_eq!(plan.runner.as_deref(), Some("refresh"));
        Ok(())
    }

    #[test]
    fn input_json_rejects_non_json_values() -> Result<(), String> {
        let args = [
            "skill",
            "skills/data-store",
            "--input-json",
            "event",
            "plain text",
        ]
        .into_iter()
        .map(std::ffi::OsString::from)
        .collect::<Vec<_>>();
        let Err(error) = super::parse_skill_plan(&args) else {
            return Err("invalid json input should fail".to_owned());
        };

        assert!(error.contains("--input-json event is invalid JSON"));
        Ok(())
    }

    #[test]
    fn credential_parser_keeps_uri_material_ref_intact() -> Result<(), String> {
        let binding = super::parse_credential_binding(
            "frantic:bearer:local://frantic/internal:frantic.review,frantic.write",
        )?;
        assert_eq!(binding.provider, "frantic");
        assert_eq!(binding.auth_mode, "bearer");
        assert_eq!(binding.material_ref, "local://frantic/internal");
        assert_eq!(binding.scopes, vec!["frantic.review", "frantic.write"]);
        Ok(())
    }

    fn unique_temp_dir(name: &str) -> Result<std::path::PathBuf, String> {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|error| error.to_string())?
            .as_nanos();
        let path = std::env::temp_dir().join(format!("{name}-{}-{nanos}", std::process::id()));
        fs::create_dir_all(&path).map_err(|error| error.to_string())?;
        Ok(path)
    }
}
