use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TemplateFile {
    pub version: u32,
    pub provider: String,
    #[serde(default)]
    pub categories: Vec<String>,
    pub commands: BTreeMap<String, CommandTemplate>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CommandTemplate {
    pub title: String,
    pub summary: String,
    pub description: String,
    #[serde(default)]
    pub categories: Vec<String>,
    pub annotations: Annotations,
    #[serde(default)]
    pub params: Vec<ParamSpec>,
    pub operation: OperationTemplate,
    pub result: ResultTemplate,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Annotations {
    pub mode: CommandMode,
    #[serde(default)]
    pub secrets: Vec<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct AllowRule {
    pub scheme: String,
    pub host: String,
    pub port: u16,
    pub path_prefix: String,
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
pub enum CommandMode {
    #[serde(rename = "read")]
    Read,
    #[serde(rename = "write")]
    Write,
}

impl CommandMode {
    pub fn as_str(&self) -> &'static str {
        match self {
            CommandMode::Read => "read",
            CommandMode::Write => "write",
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ParamSpec {
    pub name: String,
    #[serde(rename = "type")]
    pub r#type: ParamType,
    #[serde(default)]
    pub required: bool,
    pub default: Option<Value>,
    pub description: Option<String>,
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ParamType {
    String,
    Integer,
    Number,
    Boolean,
    Null,
    Array,
    Object,
}

impl std::fmt::Display for ParamType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let label = match self {
            ParamType::String => "string",
            ParamType::Integer => "integer",
            ParamType::Number => "number",
            ParamType::Boolean => "boolean",
            ParamType::Null => "null",
            ParamType::Array => "array",
            ParamType::Object => "object",
        };
        write!(f, "{label}")
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "protocol", rename_all = "snake_case")]
pub enum OperationTemplate {
    Http(HttpOperationTemplate),
    Graphql(GraphqlOperationTemplate),
    Grpc(GrpcOperationTemplate),
    Cli(CliOperationTemplate),
}

impl OperationTemplate {
    pub fn protocol(&self) -> OperationProtocol {
        match self {
            OperationTemplate::Http(_) => OperationProtocol::Http,
            OperationTemplate::Graphql(_) => OperationProtocol::Graphql,
            OperationTemplate::Grpc(_) => OperationProtocol::Grpc,
            OperationTemplate::Cli(_) => OperationProtocol::Cli,
        }
    }

    pub fn transport(&self) -> Option<&TransportTemplate> {
        match self {
            OperationTemplate::Http(op) => op.transport.as_ref(),
            OperationTemplate::Graphql(op) => op.transport.as_ref(),
            OperationTemplate::Grpc(op) => op.transport.as_ref(),
            OperationTemplate::Cli(op) => op.transport.as_ref(),
        }
    }

    pub fn auth(&self) -> Option<&AuthTemplate> {
        match self {
            OperationTemplate::Http(op) => op.auth.as_ref(),
            OperationTemplate::Graphql(op) => op.auth.as_ref(),
            OperationTemplate::Grpc(op) => op.auth.as_ref(),
            OperationTemplate::Cli(_) => None,
        }
    }

    pub fn request_url(&self) -> Option<&str> {
        match self {
            OperationTemplate::Http(op) => Some(op.url.as_str()),
            OperationTemplate::Graphql(op) => Some(op.url.as_str()),
            OperationTemplate::Grpc(op) => Some(op.url.as_str()),
            OperationTemplate::Cli(_) => None,
        }
    }

    pub fn grpc_service_method(&self) -> Option<(&str, &str)> {
        match self {
            OperationTemplate::Grpc(op) => {
                Some((op.grpc.service.as_str(), op.grpc.method.as_str()))
            }
            OperationTemplate::Http(_)
            | OperationTemplate::Graphql(_)
            | OperationTemplate::Cli(_) => None,
        }
    }

    pub fn cli_command(&self) -> Option<&str> {
        match self {
            OperationTemplate::Cli(op) => Some(op.cli.command.as_str()),
            OperationTemplate::Http(_)
            | OperationTemplate::Graphql(_)
            | OperationTemplate::Grpc(_) => None,
        }
    }
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OperationProtocol {
    Http,
    Graphql,
    Grpc,
    Cli,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HttpOperationTemplate {
    pub method: String,
    pub url: String,
    pub path: Option<String>,
    pub query: Option<BTreeMap<String, Value>>,
    pub headers: Option<BTreeMap<String, Value>>,
    pub cookies: Option<BTreeMap<String, Value>>,
    pub auth: Option<AuthTemplate>,
    pub body: Option<BodyTemplate>,
    pub transport: Option<TransportTemplate>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct GraphqlOperationTemplate {
    #[serde(default)]
    pub method: String,
    pub url: String,
    pub path: Option<String>,
    pub query: Option<BTreeMap<String, Value>>,
    pub headers: Option<BTreeMap<String, Value>>,
    pub cookies: Option<BTreeMap<String, Value>>,
    pub auth: Option<AuthTemplate>,
    pub graphql: GraphqlTemplate,
    pub transport: Option<TransportTemplate>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct GrpcOperationTemplate {
    pub url: String,
    pub headers: Option<BTreeMap<String, Value>>,
    pub auth: Option<AuthTemplate>,
    pub grpc: GrpcTemplate,
    pub transport: Option<TransportTemplate>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct GrpcTemplate {
    pub service: String,
    pub method: String,
    pub body: Option<Value>,
    pub descriptor_set_file: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct GraphqlTemplate {
    pub query: String,
    pub operation_name: Option<String>,
    pub variables: Option<Value>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CliOperationTemplate {
    pub cli: CliCommandTemplate,
    pub transport: Option<TransportTemplate>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CliCommandTemplate {
    pub command: String,
    #[serde(default)]
    pub args: Vec<Value>,
    pub env: Option<BTreeMap<String, Value>>,
    pub cwd: Option<String>,
    pub stdin: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AuthTemplate {
    None,
    ApiKey {
        location: ApiKeyLocation,
        name: String,
        secret: String,
    },
    Bearer {
        secret: String,
    },
    Basic {
        username: String,
        password_secret: String,
    },
    OAuth2Profile {
        profile: String,
    },
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ApiKeyLocation {
    Header,
    Query,
    Cookie,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum BodyTemplate {
    None,
    Json {
        value: Value,
    },
    FormUrlencoded {
        fields: BTreeMap<String, Value>,
    },
    Multipart {
        parts: Vec<MultipartPartTemplate>,
    },
    RawText {
        value: String,
        content_type: Option<String>,
    },
    RawBytesBase64 {
        value: String,
        content_type: Option<String>,
    },
    FileStream {
        path: String,
        content_type: Option<String>,
    },
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MultipartPartTemplate {
    pub name: String,
    pub value: Option<String>,
    pub bytes_base64: Option<String>,
    pub file_path: Option<String>,
    pub content_type: Option<String>,
    pub filename: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TransportTemplate {
    pub timeout_ms: Option<u64>,
    pub max_response_bytes: Option<u64>,
    pub redirects: Option<RedirectTemplate>,
    pub retry: Option<RetryTemplate>,
    pub compression: Option<bool>,
    pub tls: Option<TlsTemplate>,
    pub proxy_profile: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RedirectTemplate {
    #[serde(default = "default_follow_redirects")]
    pub follow: bool,
    #[serde(default = "default_redirect_hops")]
    pub max_hops: usize,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RetryTemplate {
    #[serde(default)]
    pub max_attempts: usize,
    #[serde(default = "default_backoff_ms")]
    pub backoff_ms: u64,
    #[serde(default)]
    pub retry_on_status: Vec<u16>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TlsTemplate {
    pub min_version: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ResultTemplate {
    #[serde(default)]
    pub decode: ResultDecode,
    pub extract: Option<ResultExtract>,
    pub output: String,
    pub result_alias: Option<String>,
}

#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultDecode {
    #[default]
    Auto,
    Json,
    Text,
    Html,
    Xml,
    Binary,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ResultExtract {
    JsonPointer { json_pointer: String },
    Regex { regex: String },
    XPath { xpath: String },
    CssSelector { css_selector: String },
}

fn default_follow_redirects() -> bool {
    true
}

fn default_redirect_hops() -> usize {
    5
}

fn default_backoff_ms() -> u64 {
    250
}
