//! Host → app_id resolver. The first phase of the orchestrator's
//! two-phase dispatch (the second phase is the per-app route matcher
//! in `routing::table::RouteTable`).
//!
//! Cached in memory; the manager rebuilds the table after each
//! domain-claim CRUD operation (same pattern as `RouteTable`).

use std::sync::RwLock;

use picloud_shared::AppId;

use super::pattern::{HostPattern, HostSpecificity};

/// A parsed domain claim ready for runtime matching.
#[derive(Debug, Clone)]
pub struct CompiledAppDomain {
    pub app_id: AppId,
    pub pattern: HostPattern,
    pub shape_key: String,
}

#[derive(Default)]
pub struct AppDomainTable {
    inner: RwLock<Vec<CompiledAppDomain>>,
}

impl AppDomainTable {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Atomic full replacement; called at startup and after every
    /// domain CRUD operation.
    pub fn replace(&self, domains: Vec<CompiledAppDomain>) {
        let mut guard = self.inner.write().expect("app domain table poisoned");
        *guard = domains;
    }

    /// Resolve a request's `Host` header to an `AppId`. Most-specific
    /// claim wins: exact > longest wildcard > shorter wildcard. Returns
    /// `None` when no claim covers `host` (orchestrator should 404).
    #[must_use]
    pub fn resolve_app(&self, host: &str) -> Option<AppId> {
        let host = strip_port(host).to_ascii_lowercase();
        let guard = self.inner.read().expect("app domain table poisoned");
        let mut best: Option<(HostSpecificity, AppId)> = None;
        for claim in guard.iter() {
            if let Some(()) = host_matches(&claim.pattern, &host) {
                let s = claim.pattern.specificity();
                if best.is_none_or(|(prev, _)| s > prev) {
                    best = Some((s, claim.app_id));
                }
            }
        }
        best.map(|(_, app_id)| app_id)
    }

    #[must_use]
    pub fn snapshot(&self) -> Vec<CompiledAppDomain> {
        self.inner
            .read()
            .expect("app domain table poisoned")
            .clone()
    }
}

fn strip_port(host: &str) -> &str {
    host.split(':').next().unwrap_or(host)
}

fn host_matches(pattern: &HostPattern, host: &str) -> Option<()> {
    match pattern {
        HostPattern::Any => Some(()),
        HostPattern::Strict(s) => {
            if s.eq_ignore_ascii_case(host) {
                Some(())
            } else {
                None
            }
        }
        HostPattern::Wildcard { suffix, .. } => {
            let dotted = format!(".{}", suffix.to_ascii_lowercase());
            host.strip_suffix(&dotted)
                .filter(|p| !p.is_empty())
                .map(|_| ())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::routing::pattern::parse_app_domain;
    use uuid::Uuid;

    fn id() -> AppId {
        AppId::from(Uuid::new_v4())
    }

    fn compile(app_id: AppId, raw: &str) -> CompiledAppDomain {
        let d = parse_app_domain(raw).unwrap();
        CompiledAppDomain {
            app_id,
            pattern: d.pattern,
            shape_key: d.shape_key,
        }
    }

    #[test]
    fn resolves_exact_over_wildcard() {
        let app_a = id();
        let app_b = id();
        let table = AppDomainTable::new();
        table.replace(vec![
            compile(app_a, "foo.example.com"),
            compile(app_b, "*.example.com"),
        ]);
        assert_eq!(table.resolve_app("foo.example.com"), Some(app_a));
        assert_eq!(table.resolve_app("bar.example.com"), Some(app_b));
    }

    #[test]
    fn longer_wildcard_beats_shorter() {
        let inner = id();
        let outer = id();
        let table = AppDomainTable::new();
        table.replace(vec![
            compile(inner, "*.api.example.com"),
            compile(outer, "*.example.com"),
        ]);
        assert_eq!(
            table.resolve_app("v1.api.example.com"),
            Some(inner),
            "more-specific wildcard should win"
        );
        assert_eq!(table.resolve_app("v1.example.com"), Some(outer));
    }

    #[test]
    fn parameterized_resolves_like_wildcard() {
        let app = id();
        let table = AppDomainTable::new();
        table.replace(vec![compile(app, "{tenant}.example.com")]);
        assert_eq!(table.resolve_app("acme.example.com"), Some(app));
        assert!(table.resolve_app("example.com").is_none());
    }

    #[test]
    fn returns_none_when_no_claim() {
        let app = id();
        let table = AppDomainTable::new();
        table.replace(vec![compile(app, "foo.example.com")]);
        assert!(table.resolve_app("nope.com").is_none());
        assert!(table.resolve_app("").is_none());
    }

    #[test]
    fn strips_port() {
        let app = id();
        let table = AppDomainTable::new();
        table.replace(vec![compile(app, "localhost")]);
        assert_eq!(table.resolve_app("localhost:18080"), Some(app));
    }
}
