// Handlers for CloudFront DistributionTenant ops (12 ops). Same
// REST-XML conventions as Distribution: ETag-based concurrency,
// IDs prefixed with `dt-`, ARNs under `arn:aws:cloudfront::`.

use chrono::Utc;
use http::header::LOCATION;
use http::{HeaderMap, StatusCode};
use uuid::Uuid;

use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};

use crate::policies::{
    not_found, precondition_failed, require_if_match, rfc3339, route_id, xml_with_etag,
};
use crate::router::Route;
use crate::service::{
    aws_error, esc, generate_id_with_prefix, invalid_argument, xml_response, CloudFrontService,
    DEFAULT_ACCOUNT,
};
use crate::tenants::{StoredDistributionTenant, StoredTenantInvalidation};
use crate::xml_io;

const NS: &str = crate::NAMESPACE;
const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;

#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct CreateDistributionTenantRequest {
    pub distribution_id: String,
    pub name: String,
    #[serde(default)]
    pub domains: Option<DomainItems>,
    #[serde(default)]
    pub connection_group_id: Option<String>,
    #[serde(default)]
    pub enabled: Option<bool>,
}

#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct UpdateDistributionTenantRequest {
    #[serde(default)]
    pub distribution_id: Option<String>,
    #[serde(default)]
    pub domains: Option<DomainItems>,
    #[serde(default)]
    pub connection_group_id: Option<String>,
    #[serde(default)]
    pub enabled: Option<bool>,
}

#[derive(Debug, Default, serde::Deserialize)]
struct DomainItems {
    #[serde(default, rename = "member")]
    pub members: Vec<DomainItem>,
}

#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DomainItem {
    pub domain: String,
}

#[derive(Debug, Default, serde::Deserialize)]
struct AssociateWebAclRequest {
    #[serde(rename = "WebACLArn")]
    pub web_acl_arn: String,
}

#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InvalidationBatchRequest {
    pub paths: PathsItems,
    pub caller_reference: String,
}

#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct PathsItems {
    #[serde(default)]
    pub items: Option<PathItems>,
}

#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct PathItems {
    #[serde(default, rename = "Path")]
    pub path: Vec<String>,
}

impl CloudFrontService {
    pub(crate) fn create_distribution_tenant(
        &self,
        req: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let parsed: CreateDistributionTenantRequest =
            xml_io::from_xml_root(&req.body).map_err(|e| {
                invalid_argument(format!("invalid CreateDistributionTenantRequest XML: {e}"))
            })?;
        if parsed.distribution_id.is_empty() {
            return Err(invalid_argument("DistributionId is required"));
        }
        if parsed.name.is_empty() {
            return Err(invalid_argument("Name is required"));
        }
        let mut state = self.state.write();
        let account = state
            .accounts
            .entry(DEFAULT_ACCOUNT.to_string())
            .or_default();
        if account
            .distribution_tenants
            .values()
            .any(|t| t.name == parsed.name)
        {
            return Err(aws_error(
                StatusCode::CONFLICT,
                "EntityAlreadyExists",
                format!("DistributionTenant {} already exists", parsed.name),
            ));
        }
        let id = generate_tenant_id();
        let arn = format!(
            "arn:aws:cloudfront::{}:distribution-tenant/{}",
            DEFAULT_ACCOUNT, id
        );
        let etag = generate_id_with_prefix("E");
        let now = Utc::now();
        let domains = parsed
            .domains
            .map(|d| d.members.into_iter().map(|i| i.domain).collect())
            .unwrap_or_default();
        let stored = StoredDistributionTenant {
            id: id.clone(),
            arn,
            name: parsed.name,
            distribution_id: parsed.distribution_id,
            domains,
            connection_group_id: parsed.connection_group_id,
            web_acl_arn: None,
            enabled: parsed.enabled.unwrap_or(true),
            status: "InProgress".to_string(),
            etag: etag.clone(),
            created_time: now,
            last_modified_time: now,
        };
        account
            .distribution_tenants
            .insert(id.clone(), stored.clone());
        drop(state);
        self.schedule_distribution_tenant_deploy(id.clone());
        let body = render_distribution_tenant(&stored);
        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
    }

    pub(crate) fn get_distribution_tenant(
        &self,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let id = route_id(route, "DistributionTenant")?;
        let state = self.state.read();
        let t = state
            .accounts
            .get(DEFAULT_ACCOUNT)
            .and_then(|a| a.distribution_tenants.get(&id).cloned())
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        drop(state);
        let body = render_distribution_tenant(&t);
        Ok(xml_with_etag(StatusCode::OK, body, &t.etag, None))
    }

    pub(crate) fn get_distribution_tenant_by_domain(
        &self,
        req: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let domain = req
            .query_params
            .get("domain")
            .or_else(|| req.query_params.get("Domain"))
            .cloned()
            .ok_or_else(|| invalid_argument("Domain query parameter is required"))?;
        let state = self.state.read();
        let t = state
            .accounts
            .get(DEFAULT_ACCOUNT)
            .and_then(|a| {
                a.distribution_tenants
                    .values()
                    .find(|t| t.domains.iter().any(|d| d == &domain))
                    .cloned()
            })
            .ok_or_else(|| not_found("DistributionTenant", &domain))?;
        drop(state);
        let body = render_distribution_tenant(&t);
        Ok(xml_with_etag(StatusCode::OK, body, &t.etag, None))
    }

    pub(crate) fn update_distribution_tenant(
        &self,
        req: &AwsRequest,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let id = route_id(route, "DistributionTenant")?;
        let if_match = require_if_match(req)?;
        let parsed: UpdateDistributionTenantRequest =
            xml_io::from_xml_root(&req.body).map_err(|e| {
                invalid_argument(format!("invalid UpdateDistributionTenantRequest XML: {e}"))
            })?;
        let mut state = self.state.write();
        let account = state
            .accounts
            .get_mut(DEFAULT_ACCOUNT)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        let t = account
            .distribution_tenants
            .get_mut(&id)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        if t.etag != if_match {
            return Err(precondition_failed());
        }
        if let Some(d) = parsed.distribution_id {
            t.distribution_id = d;
        }
        if let Some(d) = parsed.domains {
            t.domains = d.members.into_iter().map(|i| i.domain).collect();
        }
        if let Some(c) = parsed.connection_group_id {
            t.connection_group_id = Some(c);
        }
        if let Some(e) = parsed.enabled {
            t.enabled = e;
        }
        t.etag = generate_id_with_prefix("E");
        t.last_modified_time = Utc::now();
        // UpdateDistributionTenant kicks off a fresh edge propagation; mirror
        // the Distribution lifecycle by flipping back to InProgress.
        t.status = "InProgress".to_string();
        let snap = t.clone();
        drop(state);
        self.schedule_distribution_tenant_deploy(id.clone());
        let body = render_distribution_tenant(&snap);
        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
    }

    pub(crate) fn delete_distribution_tenant(
        &self,
        req: &AwsRequest,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let id = route_id(route, "DistributionTenant")?;
        let if_match = require_if_match(req)?;
        let mut state = self.state.write();
        let account = state
            .accounts
            .get_mut(DEFAULT_ACCOUNT)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        let t = account
            .distribution_tenants
            .get(&id)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        if t.etag != if_match {
            return Err(precondition_failed());
        }
        if t.enabled {
            return Err(aws_error(
                StatusCode::PRECONDITION_FAILED,
                "ResourceInUse",
                "DistributionTenant must be disabled before delete",
            ));
        }
        let arn = t.arn.clone();
        account.distribution_tenants.remove(&id);
        account
            .tenant_invalidations
            .retain(|_, inv| inv.tenant_id != id);
        account.tags.remove(&arn);
        drop(state);
        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
    }

    pub(crate) fn list_distribution_tenants(
        &self,
        _req: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let state = self.state.read();
        let mut items: Vec<StoredDistributionTenant> = state
            .accounts
            .get(DEFAULT_ACCOUNT)
            .map(|a| a.distribution_tenants.values().cloned().collect())
            .unwrap_or_default();
        drop(state);
        items.sort_by(|a, b| a.id.cmp(&b.id));
        let body = render_tenant_list(&items, "ListDistributionTenantsResult");
        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
    }

    pub(crate) fn list_distribution_tenants_by_customization(
        &self,
        _req: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let state = self.state.read();
        let mut items: Vec<StoredDistributionTenant> = state
            .accounts
            .get(DEFAULT_ACCOUNT)
            .map(|a| a.distribution_tenants.values().cloned().collect())
            .unwrap_or_default();
        drop(state);
        items.sort_by(|a, b| a.id.cmp(&b.id));
        let body = render_tenant_list(&items, "ListDistributionTenantsByCustomizationResult");
        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
    }

    pub(crate) fn associate_distribution_tenant_web_acl(
        &self,
        req: &AwsRequest,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let id = route_id(route, "DistributionTenant")?;
        let if_match = require_if_match(req)?;
        let parsed: AssociateWebAclRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
            invalid_argument(format!(
                "invalid AssociateDistributionTenantWebACLRequest XML: {e}"
            ))
        })?;
        let mut state = self.state.write();
        let account = state
            .accounts
            .get_mut(DEFAULT_ACCOUNT)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        let t = account
            .distribution_tenants
            .get_mut(&id)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        if t.etag != if_match {
            return Err(precondition_failed());
        }
        t.web_acl_arn = Some(parsed.web_acl_arn);
        t.etag = generate_id_with_prefix("E");
        t.last_modified_time = Utc::now();
        let snap = t.clone();
        drop(state);
        let body = render_associate_web_acl(&snap);
        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
    }

    pub(crate) fn disassociate_distribution_tenant_web_acl(
        &self,
        req: &AwsRequest,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let id = route_id(route, "DistributionTenant")?;
        let if_match = require_if_match(req)?;
        let mut state = self.state.write();
        let account = state
            .accounts
            .get_mut(DEFAULT_ACCOUNT)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        let t = account
            .distribution_tenants
            .get_mut(&id)
            .ok_or_else(|| not_found("DistributionTenant", &id))?;
        if t.etag != if_match {
            return Err(precondition_failed());
        }
        t.web_acl_arn = None;
        t.etag = generate_id_with_prefix("E");
        t.last_modified_time = Utc::now();
        let snap = t.clone();
        drop(state);
        let body = render_disassociate_web_acl(&snap);
        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
    }

    pub(crate) fn create_invalidation_for_distribution_tenant(
        &self,
        req: &AwsRequest,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let tenant_id = route_id(route, "DistributionTenant")?;
        let parsed: InvalidationBatchRequest = xml_io::from_xml_root(&req.body)
            .map_err(|e| invalid_argument(format!("invalid InvalidationBatch XML: {e}")))?;
        if parsed.caller_reference.is_empty() {
            return Err(invalid_argument("CallerReference is required"));
        }
        let paths = parsed.paths.items.map(|i| i.path).unwrap_or_default();
        if paths.is_empty() {
            return Err(invalid_argument(
                "InvalidationBatch.Paths must be non-empty",
            ));
        }
        let mut state = self.state.write();
        let account = state.entry(DEFAULT_ACCOUNT);
        if !account.distribution_tenants.contains_key(&tenant_id) {
            return Err(not_found("DistributionTenant", &tenant_id));
        }
        let id = generate_invalidation_id();
        let stored = StoredTenantInvalidation {
            id: id.clone(),
            tenant_id: tenant_id.clone(),
            status: "Completed".to_string(),
            create_time: Utc::now(),
            paths,
            caller_reference: parsed.caller_reference,
        };
        account
            .tenant_invalidations
            .insert(id.clone(), stored.clone());
        drop(state);
        let body = render_tenant_invalidation(&stored);
        let mut headers = HeaderMap::new();
        if let Ok(v) = http::HeaderValue::from_str(&format!(
            "/2020-05-31/distribution-tenant/{tenant_id}/invalidation/{}",
            stored.id
        )) {
            headers.insert(LOCATION, v);
        }
        Ok(xml_response(StatusCode::CREATED, body, headers))
    }

    pub(crate) fn get_invalidation_for_distribution_tenant(
        &self,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let tenant_id = route
            .id
            .as_deref()
            .ok_or_else(|| invalid_argument("missing distribution tenant id"))?;
        let inv_id = route
            .second_id
            .as_deref()
            .ok_or_else(|| invalid_argument("missing invalidation id"))?;
        let state = self.state.read();
        let account = state
            .accounts
            .get(DEFAULT_ACCOUNT)
            .ok_or_else(|| not_found("Invalidation", inv_id))?;
        if !account.distribution_tenants.contains_key(tenant_id) {
            return Err(not_found("DistributionTenant", tenant_id));
        }
        let inv = account
            .tenant_invalidations
            .get(inv_id)
            .filter(|i| i.tenant_id == tenant_id)
            .ok_or_else(|| not_found("Invalidation", inv_id))?
            .clone();
        drop(state);
        let body = render_tenant_invalidation(&inv);
        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
    }

    pub(crate) fn list_invalidations_for_distribution_tenant(
        &self,
        route: &Route,
    ) -> Result<AwsResponse, AwsServiceError> {
        let tenant_id = route
            .id
            .as_deref()
            .ok_or_else(|| invalid_argument("missing distribution tenant id"))?;
        let state = self.state.read();
        let account = state
            .accounts
            .get(DEFAULT_ACCOUNT)
            .ok_or_else(|| not_found("DistributionTenant", tenant_id))?;
        if !account.distribution_tenants.contains_key(tenant_id) {
            return Err(not_found("DistributionTenant", tenant_id));
        }
        let mut items: Vec<&StoredTenantInvalidation> = account
            .tenant_invalidations
            .values()
            .filter(|i| i.tenant_id == tenant_id)
            .collect();
        items.sort_by_key(|a| a.create_time);
        let body = render_tenant_invalidation_list(&items);
        drop(state);
        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
    }
}

fn generate_tenant_id() -> String {
    let raw = Uuid::new_v4().simple().to_string().to_uppercase();
    format!("DT{}", &raw[..12])
}

fn generate_invalidation_id() -> String {
    let raw = Uuid::new_v4().simple().to_string().to_uppercase();
    format!("I{}", &raw[..13])
}

fn render_distribution_tenant(t: &StoredDistributionTenant) -> String {
    let mut out = String::with_capacity(512);
    out.push_str(XML_DECL);
    out.push_str(&format!("<DistributionTenant xmlns=\"{NS}\">"));
    push_tenant_inner(&mut out, t);
    out.push_str("</DistributionTenant>");
    out
}

fn push_tenant_inner(out: &mut String, t: &StoredDistributionTenant) {
    out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
    out.push_str(&format!(
        "<DistributionId>{}</DistributionId>",
        esc(&t.distribution_id)
    ));
    out.push_str(&format!("<Name>{}</Name>", esc(&t.name)));
    out.push_str(&format!("<Arn>{}</Arn>", esc(&t.arn)));
    out.push_str(&format!("<Status>{}</Status>", esc(&t.status)));
    out.push_str(&format!("<Enabled>{}</Enabled>", t.enabled));
    out.push_str(&format!(
        "<CreatedTime>{}</CreatedTime>",
        rfc3339(&t.created_time)
    ));
    out.push_str(&format!(
        "<LastModifiedTime>{}</LastModifiedTime>",
        rfc3339(&t.last_modified_time)
    ));
    if let Some(c) = &t.connection_group_id {
        out.push_str(&format!(
            "<ConnectionGroupId>{}</ConnectionGroupId>",
            esc(c)
        ));
    }
    out.push_str("<Domains>");
    for d in &t.domains {
        out.push_str("<DomainResult>");
        out.push_str(&format!("<Domain>{}</Domain>", esc(d)));
        out.push_str("<Status>active</Status>");
        out.push_str("</DomainResult>");
    }
    out.push_str("</Domains>");
}

fn render_tenant_list(items: &[StoredDistributionTenant], wrapper: &str) -> String {
    let mut out = String::with_capacity(512);
    out.push_str(XML_DECL);
    out.push_str(&format!("<{wrapper} xmlns=\"{NS}\">"));
    out.push_str("<NextMarker></NextMarker>");
    out.push_str("<DistributionTenantList>");
    for t in items {
        out.push_str("<DistributionTenantSummary>");
        push_tenant_inner(&mut out, t);
        out.push_str("</DistributionTenantSummary>");
    }
    out.push_str("</DistributionTenantList>");
    out.push_str(&format!("</{wrapper}>"));
    out
}

fn render_associate_web_acl(t: &StoredDistributionTenant) -> String {
    let mut out = String::with_capacity(256);
    out.push_str(XML_DECL);
    out.push_str(&format!(
        "<AssociateDistributionTenantWebACLResult xmlns=\"{NS}\">"
    ));
    out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
    if let Some(arn) = &t.web_acl_arn {
        out.push_str(&format!("<WebACLArn>{}</WebACLArn>", esc(arn)));
    }
    out.push_str("</AssociateDistributionTenantWebACLResult>");
    out
}

fn render_disassociate_web_acl(t: &StoredDistributionTenant) -> String {
    let mut out = String::with_capacity(256);
    out.push_str(XML_DECL);
    out.push_str(&format!(
        "<DisassociateDistributionTenantWebACLResult xmlns=\"{NS}\">"
    ));
    out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
    out.push_str("</DisassociateDistributionTenantWebACLResult>");
    out
}

fn render_tenant_invalidation(inv: &StoredTenantInvalidation) -> String {
    let mut out = String::with_capacity(512);
    out.push_str(XML_DECL);
    out.push_str(&format!("<Invalidation xmlns=\"{NS}\">"));
    out.push_str(&format!("<Id>{}</Id>", esc(&inv.id)));
    out.push_str(&format!("<Status>{}</Status>", esc(&inv.status)));
    out.push_str(&format!(
        "<CreateTime>{}</CreateTime>",
        rfc3339(&inv.create_time)
    ));
    out.push_str("<InvalidationBatch>");
    out.push_str(&format!(
        "<CallerReference>{}</CallerReference>",
        esc(&inv.caller_reference)
    ));
    out.push_str("<Paths>");
    out.push_str(&format!("<Quantity>{}</Quantity>", inv.paths.len()));
    out.push_str("<Items>");
    for p in &inv.paths {
        out.push_str(&format!("<Path>{}</Path>", esc(p)));
    }
    out.push_str("</Items>");
    out.push_str("</Paths>");
    out.push_str("</InvalidationBatch>");
    out.push_str("</Invalidation>");
    out
}

fn render_tenant_invalidation_list(items: &[&StoredTenantInvalidation]) -> String {
    let mut out = String::with_capacity(1024);
    out.push_str(XML_DECL);
    out.push_str(&format!("<InvalidationList xmlns=\"{NS}\">"));
    out.push_str("<Marker></Marker>");
    out.push_str("<MaxItems>100</MaxItems>");
    out.push_str("<IsTruncated>false</IsTruncated>");
    out.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
    if !items.is_empty() {
        out.push_str("<Items>");
        for inv in items {
            out.push_str("<InvalidationSummary>");
            out.push_str(&format!("<Id>{}</Id>", esc(&inv.id)));
            out.push_str(&format!(
                "<CreateTime>{}</CreateTime>",
                rfc3339(&inv.create_time)
            ));
            out.push_str(&format!("<Status>{}</Status>", esc(&inv.status)));
            out.push_str("</InvalidationSummary>");
        }
        out.push_str("</Items>");
    }
    out.push_str("</InvalidationList>");
    out
}
