// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

use crate::config::Config;
use crate::port::Port;
use crate::services::flex::gcl::ToGcl;
use crate::services::flex::policy::PolicyConfig;
use convert_case::{Case, Casing};
use indoc::{formatdoc, writedoc};
use std::fmt::Write;
use std::ops::Add;

/// Configuration for a local API service.
#[derive(Debug, Clone)]
pub struct ApiConfig {
    pub(super) name: String,
    upstream: String,
    pub(super) port: Port,
    path: String,
    policies: Vec<PolicyConfig>,
    outbound_policies: Vec<PolicyConfig>,
}

impl ApiConfig {
    fn new() -> Self {
        Self {
            name: "ingress-http".to_string(),
            upstream: "http://backend:80".to_string(),
            port: 8081,
            path: "/anything/echo/".to_string(),
            policies: vec![],
            outbound_policies: vec![],
        }
    }

    /// Creates a builder for initialize a [`ApiConfig`].
    pub fn builder() -> ApiConfigBuilder {
        ApiConfigBuilder::new()
    }
}

/// A builder for initilizing a [`ApiConfig`].
#[derive(Debug, Clone)]
pub struct ApiConfigBuilder {
    config: ApiConfig,
}

impl ApiConfigBuilder {
    fn new() -> Self {
        Self {
            config: ApiConfig::new(),
        }
    }

    /// Set the API name.
    pub fn name<T: Into<String>>(self, name: T) -> Self {
        Self {
            config: ApiConfig {
                name: name.into(),
                ..self.config
            },
        }
    }

    /// Set the API upstream.
    pub fn upstream(self, upstream: &dyn Config) -> Self {
        Self {
            config: ApiConfig {
                upstream: format!(
                    "{}://{}:{}",
                    upstream.schema(),
                    upstream.hostname(),
                    upstream.port()
                ),
                ..self.config
            },
        }
    }

    /// Set the API port.
    pub fn port(self, port: Port) -> Self {
        Self {
            config: ApiConfig {
                port,
                ..self.config
            },
        }
    }

    /// Set the API path.
    pub fn path<T: Into<String>>(self, path: T) -> Self {
        Self {
            config: ApiConfig {
                path: path.into(),
                ..self.config
            },
        }
    }

    /// Set the API policies.
    pub fn policies<T>(self, policies: T) -> Self
    where
        T: IntoIterator<Item = PolicyConfig>,
    {
        Self {
            config: ApiConfig {
                policies: policies.into_iter().collect(),
                ..self.config
            },
        }
    }

    /// Set the upstream policies.
    pub fn outbound_policies<T>(self, policies: T) -> Self
    where
        T: IntoIterator<Item = PolicyConfig>,
    {
        Self {
            config: ApiConfig {
                outbound_policies: policies.into_iter().collect(),
                ..self.config
            },
        }
    }

    /// Builds a new [`ApiConfig`].
    pub fn build(self) -> ApiConfig {
        self.config
    }
}

fn as_inline_gcl(indentation: &str, policies: &[PolicyConfig]) -> String {
    let mut result = "".to_string();
    if !policies.is_empty() {
        let _ = writedoc!(result, "{indentation}policies:");
    }

    for policy_config in policies.iter() {
        let _ = writedoc!(
            result,
            r#"

{indentation}  - policyRef:
{indentation}      name: {name}
{indentation}      namespace: default
{indentation}    config: {config}"#,
            name = policy_config.name,
            config = policy_config.config.as_str(),
        );
    }

    if !result.is_empty() {
        result = result.add("\n");
    }

    result
}

impl ToGcl for ApiConfig {
    fn to_gcl(&self) -> String {
        let name = self.name.to_case(Case::Kebab);
        let port = self.port;
        let upstream = self.upstream.as_str();
        let path = self.path.as_str();
        let inbound = as_inline_gcl("      ", &self.outbound_policies);
        let outbound = as_inline_gcl("  ", &self.policies);

        formatdoc!(
            "
        # Copyright (c) 2026, Salesforce, Inc.,
        # All rights reserved.
        # For full license text, see the LICENSE.txt file
        ---
        apiVersion: gateway.mulesoft.com/v1alpha1
        kind: ApiInstance
        metadata:
          name: {name}
        spec:
          address: http://0.0.0.0:{port}
          services:
            upstream:
              address: {upstream}
              routes:
                - config:
                    destinationPath: {path}
        {inbound}{outbound}"
        )
    }

    fn name(&self) -> &str {
        &self.name
    }
}

#[cfg(test)]
mod tests {
    use crate::services::flex::gcl::ToGcl;
    use crate::services::flex::{ApiConfig, PolicyConfig};
    use crate::services::httpmock::HttpMockConfig;
    use indoc::formatdoc;

    #[test]
    fn generate_api_gcl() {
        let upstream_config = HttpMockConfig::builder().hostname("mock").port(80).build();

        let policy_config = PolicyConfig::builder()
            .name("simple-oauth-2-validation-v1-0-impl")
            .configuration(serde_json::json!({
                    "authorization": "whatever",
                    "oauthService": "http://mock:80/auth",
            }))
            .build();

        let out_policy_config = PolicyConfig::builder()
            .name("out")
            .configuration(serde_json::json!({
                    "out": "out",
            }))
            .build();

        let api_config = ApiConfig::builder()
            .upstream(&upstream_config)
            .policies([policy_config])
            .outbound_policies([out_policy_config])
            .build();

        let expected = formatdoc!(
            r#"
        # Copyright (c) 2026, Salesforce, Inc.,
        # All rights reserved.
        # For full license text, see the LICENSE.txt file
        ---
        apiVersion: gateway.mulesoft.com/v1alpha1
        kind: ApiInstance
        metadata:
          name: ingress-http
        spec:
          address: http://0.0.0.0:8081
          services:
            upstream:
              address: http://mock:80
              routes:
                - config:
                    destinationPath: /anything/echo/
              policies:
                - policyRef:
                    name: out
                    namespace: default
                  config: {{"out":"out"}}
          policies:
            - policyRef:
                name: simple-oauth-2-validation-v1-0-impl
                namespace: default
              config: {{"authorization":"whatever","oauthService":"http://mock:80/auth"}}
"#
        );

        assert_eq!(api_config.to_gcl(), expected)
    }
}
