use clap::Args;

use crate::util::{
    admin_surface_enabled, api_request, exit_error, is_admin_api_path, read_json_from_file,
    resolve_token,
};

#[derive(Args)]
pub struct ApiArgs {
    /// HTTP method (GET, POST, PUT, DELETE, PATCH)
    pub method: String,

    /// API path (e.g. /v1/events)
    pub path: String,

    /// Request body as JSON string
    #[arg(long, short = 'd')]
    pub data: Option<String>,

    /// Read request body from file (use '-' for stdin)
    #[arg(long, short = 'f', conflicts_with = "data")]
    pub data_file: Option<String>,

    /// Query parameters (repeatable: key=value)
    #[arg(long, short = 'q')]
    pub query: Vec<String>,

    /// Extra headers (repeatable: Key:Value)
    #[arg(long, short = 'H')]
    pub header: Vec<String>,

    /// Skip pretty-printing (raw JSON for piping)
    #[arg(long)]
    pub raw: bool,

    /// Include HTTP status and headers in response wrapper
    #[arg(long, short = 'i')]
    pub include: bool,

    /// Skip authentication (for public endpoints like /health)
    #[arg(long)]
    pub no_auth: bool,

    /// Allow low-level async analysis job creation via POST /v1/analysis/jobs (advanced/explicit background use only)
    #[arg(long)]
    pub allow_async_analysis_job: bool,
}

pub async fn run(api_url: &str, args: ApiArgs) -> i32 {
    if is_admin_api_path(&args.path) && !admin_surface_enabled() {
        exit_error(
            "Admin API paths are disabled in CLI by default.",
            Some("Set KURA_ENABLE_ADMIN_SURFACE=1 only in trusted developer/admin sessions."),
        );
    }

    // Parse method
    let method = match args.method.to_uppercase().as_str() {
        "GET" => reqwest::Method::GET,
        "POST" => reqwest::Method::POST,
        "PUT" => reqwest::Method::PUT,
        "DELETE" => reqwest::Method::DELETE,
        "PATCH" => reqwest::Method::PATCH,
        "HEAD" => reqwest::Method::HEAD,
        "OPTIONS" => reqwest::Method::OPTIONS,
        other => exit_error(
            &format!("Unknown HTTP method: {other}"),
            Some("Supported methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS"),
        ),
    };

    if is_async_analysis_job_create_path(&args.path)
        && method == reqwest::Method::POST
        && !args.allow_async_analysis_job
    {
        exit_error(
            "Direct async analysis-job creation via `kura api POST /v1/analysis/jobs` is blocked by default.",
            Some(
                "Use `kura analyze --kernel-json '{...}'` for user-facing analyses. Pass `--allow-async-analysis-job` only for explicit background-job workflows.",
            ),
        );
    }

    if is_routine_training_raw_api_path(&args.path) && is_mutating_training_method(&method) {
        exit_error(
            "Direct raw API task-write detours are blocked in the CLI.",
            Some(
                "Use `kura record_activity ...`, `kura record_recovery ...`, `kura record_measurement ...`, `kura update_profile ...`, and the other task-first commands instead of POSTing activity/training routes through `kura api`.",
            ),
        );
    }

    // Parse query parameters
    let mut query = Vec::new();
    for q in &args.query {
        match q.split_once('=') {
            Some((k, v)) => query.push((k.to_string(), v.to_string())),
            None => exit_error(
                &format!("Invalid query parameter: '{q}'"),
                Some("Format: key=value, e.g. --query event_type=meal.logged"),
            ),
        }
    }

    // Parse extra headers
    let mut headers = Vec::new();
    for h in &args.header {
        match h.split_once(':') {
            Some((k, v)) => headers.push((k.trim().to_string(), v.trim().to_string())),
            None => exit_error(
                &format!("Invalid header: '{h}'"),
                Some("Format: Key:Value, e.g. --header Content-Type:application/json"),
            ),
        }
    }

    // Resolve body
    let body = if let Some(ref d) = args.data {
        match serde_json::from_str(d) {
            Ok(v) => Some(v),
            Err(e) => exit_error(
                &format!("Invalid JSON in --data: {e}"),
                Some("Provide valid JSON string"),
            ),
        }
    } else if let Some(ref f) = args.data_file {
        match read_json_from_file(f) {
            Ok(v) => Some(v),
            Err(e) => exit_error(&e, Some("Provide a valid JSON file or use '-' for stdin")),
        }
    } else {
        None
    };

    if is_mutating_training_method(&method)
        && contains_routine_training_event_write(&args.path, body.as_ref())
    {
        exit_error(
            "Direct raw event writes for routine activity are blocked in the CLI.",
            Some(
                "Use `kura record_activity ...` for routine activity instead of POSTing activity or training events through `kura api`.",
            ),
        );
    }

    // Resolve auth
    let token = if args.no_auth {
        None
    } else {
        match resolve_token(api_url).await {
            Ok(t) => Some(t),
            Err(e) => exit_error(
                &e.to_string(),
                Some("Run `kura login`, set KURA_API_KEY, or use --no-auth for public endpoints"),
            ),
        }
    };

    api_request(
        api_url,
        method,
        &args.path,
        token.as_deref(),
        body,
        &query,
        &headers,
        args.raw,
        args.include,
    )
    .await
}

fn is_async_analysis_job_create_path(path: &str) -> bool {
    let trimmed = path.trim();
    if trimmed.is_empty() {
        return false;
    }

    let normalized = if trimmed.starts_with('/') {
        trimmed.to_ascii_lowercase()
    } else {
        format!("/{}", trimmed.to_ascii_lowercase())
    };
    normalized == "/v1/analysis/jobs" || normalized == "/v1/analysis/jobs/"
}

fn is_mutating_training_method(method: &reqwest::Method) -> bool {
    matches!(
        *method,
        reqwest::Method::POST
            | reqwest::Method::PUT
            | reqwest::Method::PATCH
            | reqwest::Method::DELETE
    )
}

fn is_routine_training_raw_api_path(path: &str) -> bool {
    let trimmed = path.trim();
    if trimmed.is_empty() {
        return false;
    }

    let normalized = if trimmed.starts_with('/') {
        trimmed.to_ascii_lowercase()
    } else {
        format!("/{}", trimmed.to_ascii_lowercase())
    };

    matches!(
        normalized.as_str(),
        "/v1/agent/exercise-resolve"
            | "/v4/agent/write-event"
            | "/v3/agent/training"
            | "/v4/agent/write-activity-session"
            | "/v4/agent/write-activity-protocol-profile"
            | "/v4/agent/record-activity"
            | "/v4/agent/record-recovery"
            | "/v4/agent/report-health"
            | "/v4/agent/record-nutrition"
            | "/v4/agent/record-measurement"
            | "/v4/agent/record-benchmark"
            | "/v4/agent/update-profile"
            | "/v4/agent/update-plan"
    ) || normalized.starts_with("/v3/agent/")
}

fn is_low_level_event_write_path(path: &str) -> bool {
    let trimmed = path.trim();
    if trimmed.is_empty() {
        return false;
    }

    let normalized = if trimmed.starts_with('/') {
        trimmed.to_ascii_lowercase()
    } else {
        format!("/{}", trimmed.to_ascii_lowercase())
    };

    matches!(normalized.as_str(), "/v1/events" | "/v1/events/batch")
}

fn is_routine_training_event_type(event_type: &str) -> bool {
    let normalized = event_type.trim().to_ascii_lowercase();
    matches!(
        normalized.as_str(),
        "set.logged"
            | "set.corrected"
            | "session.logged"
            | "training.session.logged"
            | "run.logged"
    ) || normalized.starts_with("training.truth_v4.")
}

fn contains_routine_training_event_write(path: &str, body: Option<&serde_json::Value>) -> bool {
    if !is_low_level_event_write_path(path) {
        return false;
    }

    let Some(body) = body else {
        return false;
    };

    if let Some(event_type) = body.get("event_type").and_then(serde_json::Value::as_str) {
        return is_routine_training_event_type(event_type);
    }

    body.get("events")
        .and_then(serde_json::Value::as_array)
        .map(|events| {
            events.iter().any(|event| {
                event
                    .get("event_type")
                    .and_then(serde_json::Value::as_str)
                    .map(is_routine_training_event_type)
                    .unwrap_or(false)
            })
        })
        .unwrap_or(false)
}

#[cfg(test)]
mod tests {
    use super::{
        contains_routine_training_event_write, is_async_analysis_job_create_path,
        is_routine_training_raw_api_path,
    };
    use serde_json::json;

    #[test]
    fn test_query_parsing() {
        let input = "event_type=set.logged";
        let (k, v) = input.split_once('=').unwrap();
        assert_eq!(k, "event_type");
        assert_eq!(v, "set.logged");
    }

    #[test]
    fn test_header_parsing() {
        let input = "Content-Type: application/json";
        let (k, v) = input.split_once(':').unwrap();
        assert_eq!(k.trim(), "Content-Type");
        assert_eq!(v.trim(), "application/json");
    }

    #[test]
    fn test_method_parsing() {
        for m in &[
            "get", "GET", "Get", "post", "POST", "delete", "DELETE", "put", "patch",
        ] {
            let parsed = match m.to_uppercase().as_str() {
                "GET" => Some(reqwest::Method::GET),
                "POST" => Some(reqwest::Method::POST),
                "PUT" => Some(reqwest::Method::PUT),
                "DELETE" => Some(reqwest::Method::DELETE),
                "PATCH" => Some(reqwest::Method::PATCH),
                _ => None,
            };
            assert!(parsed.is_some(), "Failed to parse method: {m}");
        }
    }

    #[test]
    fn async_analysis_job_create_path_detection_is_exact_to_create_endpoint() {
        assert!(is_async_analysis_job_create_path("/v1/analysis/jobs"));
        assert!(is_async_analysis_job_create_path("v1/analysis/jobs"));
        assert!(is_async_analysis_job_create_path("/v1/analysis/jobs/"));
        assert!(!is_async_analysis_job_create_path("/v1/analysis/jobs/run"));
        assert!(!is_async_analysis_job_create_path("/v1/analysis/jobs/123"));
        assert!(!is_async_analysis_job_create_path("/v1/agent/context"));
    }

    #[test]
    fn routine_training_raw_api_path_detection_catches_workout_detours() {
        assert!(is_routine_training_raw_api_path(
            "/v1/agent/exercise-resolve"
        ));
        assert!(is_routine_training_raw_api_path("/v3/agent/legacy-detour"));
        assert!(is_routine_training_raw_api_path("/v4/agent/write-event"));
        assert!(is_routine_training_raw_api_path(
            "/v4/agent/record-activity"
        ));
        assert!(is_routine_training_raw_api_path("/v4/agent/update-profile"));
        assert!(is_routine_training_raw_api_path(
            "/v4/agent/write-activity-session"
        ));
        assert!(is_routine_training_raw_api_path(
            "/v4/agent/write-activity-protocol-profile"
        ));
        assert!(is_routine_training_raw_api_path("v3/agent/training"));
        assert!(!is_routine_training_raw_api_path(
            "/v4/agent/write-correction"
        ));
        assert!(!is_routine_training_raw_api_path("/v1/events"));
    }

    #[test]
    fn routine_training_event_write_detection_only_blocks_training_events() {
        assert!(contains_routine_training_event_write(
            "/v1/events",
            Some(&json!({"event_type": "set.logged", "data": {"reps": 5}}))
        ));
        assert!(contains_routine_training_event_write(
            "/v1/events/batch",
            Some(&json!({"events": [{"event_type": "training.truth_v4.set"}]}))
        ));
        assert!(!contains_routine_training_event_write(
            "/v1/events",
            Some(&json!({"event_type": "meal.logged"}))
        ));
        assert!(!contains_routine_training_event_write(
            "/v4/agent/write-event",
            Some(&json!({"event_type": "set.logged"}))
        ));
        assert!(!contains_routine_training_event_write(
            "/v4/agent/record-activity",
            Some(&json!({"event_type": "set.logged"}))
        ));
    }
}
