//! `pic routes` subcommands: `ls`, `create`, `rm`, `check`, `match`.

use anyhow::{anyhow, Result};
use picloud_shared::{AppId, DispatchMode, HostKind, PathKind};

use crate::client::{CheckRouteBody, Client, CreateRouteBody, MatchRouteBody};
use crate::config;
use crate::output::{KvBlock, OutputMode, Table};

pub async fn ls(script_id: &str, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let routes = client.routes_list_for_script(script_id).await?;
    let mut table = Table::new([
        "id",
        "method",
        "host",
        "path_kind",
        "path",
        "dispatch",
        "created_at",
    ]);
    for r in routes {
        table.row([
            r.id.to_string(),
            r.method.clone().unwrap_or_else(|| "ANY".into()),
            host_label(r.host_kind, &r.host),
            path_kind_label(r.path_kind).to_string(),
            r.path.clone(),
            dispatch_label(r.dispatch_mode).to_string(),
            r.created_at.to_rfc3339(),
        ]);
    }
    table.print(mode);
    Ok(())
}

/// `pic routes ls --group <g>` — read-only view of a group's §11 tail route
/// TEMPLATES (inherited live by every descendant app). Authoring is declarative
/// via the `[group]` manifest `[[routes]]`.
pub async fn ls_group(group: &str, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let rows = client.group_routes_list(group).await?;
    let mut table = Table::new([
        "method",
        "host",
        "path_kind",
        "path",
        "script",
        "dispatch",
        "enabled",
        "sealed",
    ]);
    for r in rows {
        table.row([
            r.method,
            r.host,
            r.path_kind,
            r.path,
            r.script,
            r.dispatch,
            r.enabled.to_string(),
            r.sealed.to_string(),
        ]);
    }
    table.print(mode);
    Ok(())
}

#[allow(clippy::too_many_arguments)]
pub async fn create(
    script_id: &str,
    method: Option<&str>,
    path: &str,
    path_kind: PathKind,
    host: &str,
    host_kind: HostKind,
    host_param_name: Option<&str>,
    dispatch_mode: DispatchMode,
) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let body = CreateRouteBody {
        host_kind,
        host,
        host_param_name,
        path_kind,
        path,
        method,
        dispatch_mode,
    };
    let r = client.routes_create(script_id, &body).await?;
    println!(
        "Created route {} ({} {} {})",
        r.id,
        r.method.unwrap_or_else(|| "ANY".into()),
        host_label(r.host_kind, &r.host),
        r.path
    );
    Ok(())
}

pub async fn rm(route_id: &str) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    client.routes_delete(route_id).await?;
    println!("Deleted route {route_id}");
    Ok(())
}

pub async fn check(
    app: &str,
    method: Option<&str>,
    path: &str,
    path_kind: PathKind,
    host: &str,
    host_kind: HostKind,
    mode: OutputMode,
) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let app_id = resolve_app_id(&client, app).await?;
    let body = CheckRouteBody {
        app_id,
        host_kind,
        host: host.to_string(),
        path_kind,
        path: path.to_string(),
        method: method.map(str::to_string),
    };
    let resp = client.routes_check(&body).await?;
    let mut block = KvBlock::new();
    block.field("ok", resp.ok.to_string());
    if let Some(reason) = resp.conflict_reason {
        block.field("conflict_reason", reason);
    }
    if let Some(r) = resp.conflicting_route {
        block
            .field("conflicting_route_id", r.id.to_string())
            .field(
                "conflicting_method",
                r.method.unwrap_or_else(|| "ANY".into()),
            )
            .field("conflicting_host", host_label(r.host_kind, &r.host))
            .field("conflicting_path", r.path);
    }
    block.print(mode);
    Ok(())
}

pub async fn match_route(app: &str, url: &str, method: &str, mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let app_id = resolve_app_id(&client, app).await?;
    let resp = client
        .routes_match(&MatchRouteBody {
            app_id,
            url,
            method,
        })
        .await?;
    let mut block = KvBlock::new();
    match resp.matched {
        None => {
            block.field("matched", "false");
        }
        Some(m) => {
            block
                .field("matched", "true")
                .field("route_id", m.route_id)
                .field("script_id", m.script_id.to_string());
            if !m.params.is_empty() {
                for (k, v) in m.params {
                    block.field(format!("param.{k}"), v);
                }
            }
            if let Some(rest) = m.rest {
                block.field("rest", rest);
            }
        }
    }
    block.print(mode);
    Ok(())
}

async fn resolve_app_id(client: &Client, ident: &str) -> Result<AppId> {
    // Accept either a slug or a UUID. The apps_get endpoint takes both.
    let lookup = client
        .apps_get(ident)
        .await
        .map_err(|e| anyhow!("resolve app `{ident}`: {e}"))?;
    Ok(lookup.app.id)
}

const fn dispatch_label(d: DispatchMode) -> &'static str {
    match d {
        DispatchMode::Sync => "sync",
        DispatchMode::Async => "async",
    }
}

const fn path_kind_label(k: PathKind) -> &'static str {
    match k {
        PathKind::Exact => "exact",
        PathKind::Prefix => "prefix",
        PathKind::Param => "param",
    }
}

fn host_label(kind: HostKind, host: &str) -> String {
    match kind {
        HostKind::Any => "*".to_string(),
        HostKind::Strict => host.to_string(),
        HostKind::Wildcard => format!("*.{host}"),
    }
}
