//! FlashDNS Controller - Manages cluster.local DNS records for Services
//!
//! This controller continuously reconciles tenant-scoped Service resources into
//! authoritative `*.svc.cluster.local` A records and removes stale records when
//! Services disappear or lose their ClusterIP.

use crate::auth::{authorized_request, issue_controller_token};
use crate::storage::{Storage, TenantRef};
use anyhow::Result;
use flashdns_api::proto::record_service_client::RecordServiceClient;
use flashdns_api::proto::zone_service_client::ZoneServiceClient;
use flashdns_api::proto::{
    get_zone_request, record_data, ARecord, CreateRecordRequest, CreateZoneRequest,
    DeleteRecordRequest, GetZoneRequest, ListRecordsRequest, ListZonesRequest, RecordData,
    RecordInfo,
};
use k8shost_types::Service;
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
use tonic::{transport::Channel, Code};
use tracing::{debug, info, warn};

const CLUSTER_DOMAIN: &str = "cluster.local";
const DNS_RECORD_TTL: u32 = 60;
const CONTROLLER_PRINCIPAL_ID: &str = "k8shost-controller";
const RECORD_ID_ANNOTATION: &str = "flashdns.ultracloud.io/record-id";
const ZONE_ID_ANNOTATION: &str = "flashdns.ultracloud.io/zone-id";

/// FlashDNS controller for managing cluster.local DNS records
pub struct FlashDnsController {
    storage: Arc<Storage>,
    flashdns_addr: String,
    iam_server_addr: String,
    interval: Duration,
    zone_cache: Arc<tokio::sync::RwLock<HashMap<String, String>>>,
}

impl FlashDnsController {
    pub fn new(storage: Arc<Storage>, flashdns_addr: String, iam_server_addr: String) -> Self {
        Self {
            storage,
            flashdns_addr,
            iam_server_addr,
            interval: Duration::from_secs(10),
            zone_cache: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
        }
    }

    pub async fn run(self: Arc<Self>) {
        info!(
            "FlashDNS controller started (FlashDNS at {}, {}s interval)",
            self.flashdns_addr,
            self.interval.as_secs()
        );

        loop {
            if let Err(error) = self.reconcile_dns_records().await {
                warn!(error = %error, "FlashDNS controller cycle failed");
            }

            sleep(self.interval).await;
        }
    }

    async fn reconcile_dns_records(&self) -> Result<()> {
        let tenants = self.reconciliation_tenants().await?;
        if tenants.is_empty() {
            debug!("No active tenants need FlashDNS reconciliation");
            return Ok(());
        }

        for tenant in tenants {
            if let Err(error) = self.reconcile_tenant_dns(&tenant).await {
                warn!(
                    org_id = %tenant.org_id,
                    project_id = %tenant.project_id,
                    error = %error,
                    "failed to reconcile tenant DNS"
                );
            }
        }

        Ok(())
    }

    async fn reconcile_tenant_dns(&self, tenant: &TenantRef) -> Result<()> {
        let auth_token = issue_controller_token(
            &self.iam_server_addr,
            CONTROLLER_PRINCIPAL_ID,
            &tenant.org_id,
            &tenant.project_id,
        )
        .await?;
        let services = self
            .storage
            .list_services(&tenant.org_id, &tenant.project_id, None)
            .await?;

        for service in services
            .iter()
            .filter(|service| !service_requires_dns(service) && has_dns_state(service))
        {
            self.clear_service_dns_state(
                &tenant.org_id,
                &tenant.project_id,
                service_namespace(service),
                &service.metadata.name,
            )
            .await?;
        }

        let desired_services = services
            .into_iter()
            .filter(service_requires_dns)
            .collect::<Vec<_>>();
        let has_desired_services = !desired_services.is_empty();
        if has_desired_services {
            self.storage
                .remember_controller_tenant("flashdns", tenant)
                .await?;
        }
        let zone_id = match self
            .zone_id_for_tenant(tenant, &auth_token, !desired_services.is_empty())
            .await?
        {
            Some(zone_id) => zone_id,
            None => return Ok(()),
        };

        let mut record_client = match RecordServiceClient::connect(self.flashdns_addr.clone()).await
        {
            Ok(client) => client,
            Err(error) => {
                warn!(
                    address = %self.flashdns_addr,
                    error = %error,
                    "failed to connect to FlashDNS"
                );
                return Ok(());
            }
        };

        let mut existing_by_name = self
            .list_zone_records(&mut record_client, &zone_id, &auth_token)
            .await?
            .into_iter()
            .filter(|record| is_managed_record(record))
            .fold(
                HashMap::<String, Vec<RecordInfo>>::new(),
                |mut acc, record| {
                    acc.entry(record.name.clone()).or_default().push(record);
                    acc
                },
            );

        for service in desired_services {
            let namespace = service_namespace(&service).to_string();
            let record_name = service_record_name(&service);
            let cluster_ip = service
                .spec
                .cluster_ip
                .as_deref()
                .expect("service_requires_dns guarantees cluster_ip");
            let existing = existing_by_name.remove(&record_name).unwrap_or_default();
            let record = self
                .ensure_service_record(
                    &mut record_client,
                    &auth_token,
                    &zone_id,
                    &record_name,
                    cluster_ip,
                    existing,
                )
                .await?;

            self.reconcile_service_dns_state(
                &tenant.org_id,
                &tenant.project_id,
                &namespace,
                &service.metadata.name,
                &zone_id,
                &record.id,
            )
            .await?;
        }

        for stale_records in existing_by_name.into_values() {
            for record in stale_records {
                delete_record(&mut record_client, &auth_token, &record.id).await?;
            }
        }

        if !has_desired_services {
            self.zone_cache
                .write()
                .await
                .remove(&tenant_cache_key(tenant));
            self.storage
                .forget_controller_tenant("flashdns", tenant)
                .await?;
        }

        Ok(())
    }

    async fn reconciliation_tenants(&self) -> Result<Vec<TenantRef>> {
        let mut tenants = self
            .storage
            .list_active_tenants()
            .await?
            .into_iter()
            .collect::<BTreeSet<_>>();

        for tenant in self.storage.list_controller_tenants("flashdns").await? {
            tenants.insert(tenant);
        }

        Ok(tenants.into_iter().collect())
    }

    async fn ensure_service_record(
        &self,
        client: &mut RecordServiceClient<Channel>,
        auth_token: &str,
        zone_id: &str,
        record_name: &str,
        cluster_ip: &str,
        existing: Vec<RecordInfo>,
    ) -> Result<RecordInfo> {
        let mut matching = None;

        for record in existing {
            let is_match = record.record_type == "A" && record_a_value(&record) == Some(cluster_ip);
            if is_match && matching.is_none() {
                matching = Some(record);
                continue;
            }

            delete_record(client, auth_token, &record.id).await?;
        }

        if let Some(record) = matching {
            return Ok(record);
        }

        Ok(client
            .create_record(authorized_request(
                CreateRecordRequest {
                    zone_id: zone_id.to_string(),
                    name: record_name.to_string(),
                    record_type: "A".to_string(),
                    ttl: DNS_RECORD_TTL,
                    data: Some(RecordData {
                        data: Some(record_data::Data::A(ARecord {
                            address: cluster_ip.to_string(),
                        })),
                    }),
                },
                auth_token,
            ))
            .await?
            .into_inner()
            .record
            .ok_or_else(|| anyhow::anyhow!("FlashDNS returned empty CreateRecord response"))?)
    }

    async fn reconcile_service_dns_state(
        &self,
        org_id: &str,
        project_id: &str,
        namespace: &str,
        name: &str,
        zone_id: &str,
        record_id: &str,
    ) -> Result<()> {
        let Some(mut service) = self
            .storage
            .get_service(org_id, project_id, namespace, name)
            .await?
        else {
            return Ok(());
        };

        let mut changed = false;
        changed |= set_annotation(&mut service, RECORD_ID_ANNOTATION, record_id);
        changed |= set_annotation(&mut service, ZONE_ID_ANNOTATION, zone_id);

        if !changed {
            return Ok(());
        }

        bump_resource_version(&mut service);
        self.storage.put_service(&service).await?;
        Ok(())
    }

    async fn clear_service_dns_state(
        &self,
        org_id: &str,
        project_id: &str,
        namespace: &str,
        name: &str,
    ) -> Result<()> {
        let Some(mut service) = self
            .storage
            .get_service(org_id, project_id, namespace, name)
            .await?
        else {
            return Ok(());
        };

        let mut changed = false;
        changed |= service
            .metadata
            .annotations
            .remove(RECORD_ID_ANNOTATION)
            .is_some();
        changed |= service
            .metadata
            .annotations
            .remove(ZONE_ID_ANNOTATION)
            .is_some();

        if !changed {
            return Ok(());
        }

        bump_resource_version(&mut service);
        self.storage.put_service(&service).await?;
        Ok(())
    }

    async fn zone_id_for_tenant(
        &self,
        tenant: &TenantRef,
        auth_token: &str,
        create_if_missing: bool,
    ) -> Result<Option<String>> {
        let cache_key = tenant_cache_key(tenant);
        if let Some(zone_id) = self.zone_cache.read().await.get(&cache_key).cloned() {
            return Ok(Some(zone_id));
        }

        let mut zone_client = ZoneServiceClient::connect(self.flashdns_addr.clone()).await?;
        if let Some(zone_id) = self
            .lookup_zone_id(&mut zone_client, CLUSTER_DOMAIN, auth_token)
            .await?
        {
            self.zone_cache
                .write()
                .await
                .insert(cache_key, zone_id.clone());
            return Ok(Some(zone_id));
        }

        if !create_if_missing {
            return Ok(None);
        }

        let response = match zone_client
            .create_zone(authorized_request(
                CreateZoneRequest {
                    name: CLUSTER_DOMAIN.to_string(),
                    org_id: tenant.org_id.clone(),
                    project_id: tenant.project_id.clone(),
                    primary_ns: "ns1.ultracloud.io".to_string(),
                    admin_email: "admin@ultracloud.io".to_string(),
                },
                auth_token,
            ))
            .await
        {
            Ok(response) => response,
            Err(status) if status.code() == Code::AlreadyExists => {
                for _ in 0..5 {
                    if let Some(zone_id) = self
                        .lookup_zone_id(&mut zone_client, CLUSTER_DOMAIN, auth_token)
                        .await?
                    {
                        self.zone_cache
                            .write()
                            .await
                            .insert(cache_key.clone(), zone_id.clone());
                        return Ok(Some(zone_id));
                    }
                    sleep(Duration::from_millis(200)).await;
                }

                return Err(anyhow::anyhow!(
                    "zone {} already exists for tenant {}/{} but could not be listed",
                    CLUSTER_DOMAIN,
                    tenant.org_id,
                    tenant.project_id
                ));
            }
            Err(status) => return Err(status.into()),
        };

        let zone_id = response
            .into_inner()
            .zone
            .ok_or_else(|| anyhow::anyhow!("FlashDNS returned empty zone"))?
            .id;
        self.zone_cache
            .write()
            .await
            .insert(cache_key, zone_id.clone());
        Ok(Some(zone_id))
    }

    async fn lookup_zone_id(
        &self,
        zone_client: &mut ZoneServiceClient<Channel>,
        zone_name: &str,
        auth_token: &str,
    ) -> Result<Option<String>> {
        let get_req = GetZoneRequest {
            identifier: Some(get_zone_request::Identifier::Name(zone_name.to_string())),
        };

        match zone_client
            .get_zone(authorized_request(get_req, auth_token))
            .await
        {
            Ok(response) => Ok(response.into_inner().zone.map(|zone| zone.id)),
            Err(error) if error.code() == Code::NotFound => Ok(None),
            Err(error) => {
                debug!("exact zone lookup failed for {}: {}", zone_name, error);
                let list_req = ListZonesRequest {
                    org_id: String::new(),
                    project_id: String::new(),
                    name_filter: zone_name.to_string(),
                    page_size: 100,
                    page_token: String::new(),
                };

                match zone_client
                    .list_zones(authorized_request(list_req, auth_token))
                    .await
                {
                    Ok(response) => Ok(response
                        .into_inner()
                        .zones
                        .into_iter()
                        .find(|zone| {
                            zone.name.trim_end_matches('.') == zone_name.trim_end_matches('.')
                        })
                        .map(|zone| zone.id)),
                    Err(list_error) => {
                        debug!(
                            "zone list fallback failed for {}: {}",
                            zone_name, list_error
                        );
                        Ok(None)
                    }
                }
            }
        }
    }

    async fn list_zone_records(
        &self,
        client: &mut RecordServiceClient<Channel>,
        zone_id: &str,
        auth_token: &str,
    ) -> Result<Vec<RecordInfo>> {
        let mut records = Vec::new();
        let mut page_token = String::new();

        loop {
            let response = client
                .list_records(authorized_request(
                    ListRecordsRequest {
                        zone_id: zone_id.to_string(),
                        name_filter: String::new(),
                        type_filter: String::new(),
                        page_size: 256,
                        page_token: page_token.clone(),
                    },
                    auth_token,
                ))
                .await?
                .into_inner();

            records.extend(response.records);
            if response.next_page_token.is_empty() {
                break;
            }
            page_token = response.next_page_token;
        }

        Ok(records)
    }
}

fn tenant_cache_key(tenant: &TenantRef) -> String {
    format!("{}/{}", tenant.org_id, tenant.project_id)
}

fn service_requires_dns(service: &Service) -> bool {
    service.spec.cluster_ip.is_some()
}

fn has_dns_state(service: &Service) -> bool {
    service
        .metadata
        .annotations
        .contains_key(RECORD_ID_ANNOTATION)
        || service
            .metadata
            .annotations
            .contains_key(ZONE_ID_ANNOTATION)
}

fn service_namespace(service: &Service) -> &str {
    service.metadata.namespace.as_deref().unwrap_or("default")
}

fn service_record_name(service: &Service) -> String {
    format!(
        "{}.{}.svc",
        service.metadata.name,
        service_namespace(service)
    )
}

fn is_managed_record(record: &RecordInfo) -> bool {
    record.name.ends_with(".svc")
}

fn record_a_value(record: &RecordInfo) -> Option<&str> {
    record
        .data
        .as_ref()
        .and_then(|data| data.data.as_ref())
        .and_then(|data| match data {
            record_data::Data::A(record) => Some(record.address.as_str()),
            _ => None,
        })
}

fn set_annotation(service: &mut Service, key: &str, value: &str) -> bool {
    match service.metadata.annotations.get(key) {
        Some(current) if current == value => false,
        _ => {
            service
                .metadata
                .annotations
                .insert(key.to_string(), value.to_string());
            true
        }
    }
}

fn bump_resource_version(service: &mut Service) {
    let current = service
        .metadata
        .resource_version
        .as_deref()
        .and_then(|value| value.parse::<u64>().ok())
        .unwrap_or(0);
    service.metadata.resource_version = Some((current + 1).to_string());
}

async fn delete_record(
    client: &mut RecordServiceClient<Channel>,
    auth_token: &str,
    record_id: &str,
) -> Result<()> {
    match client
        .delete_record(authorized_request(
            DeleteRecordRequest {
                id: record_id.to_string(),
            },
            auth_token,
        ))
        .await
    {
        Ok(_) => Ok(()),
        Err(status) if status.code() == Code::NotFound => Ok(()),
        Err(status) => Err(status.into()),
    }
}
