//! CRUD over the `app_domains` table.
//!
//! Parsing + shape_key derivation live in `orchestrator-core`'s
//! `routing::pattern::parse_app_domain` — this repo just stores what
//! the API handler hands it. Same-shape collisions surface as a unique
//! constraint violation on `shape_key`, mapped here to a clean error.

use async_trait::async_trait;
use picloud_shared::{AppDomain, AppId, DomainShape};
use sqlx::PgPool;
use uuid::Uuid;

use crate::repo::ScriptRepositoryError;

#[derive(Debug, Clone)]
pub struct NewAppDomain {
    pub app_id: AppId,
    pub pattern: String,
    pub shape: DomainShape,
    pub shape_key: String,
}

#[async_trait]
pub trait AppDomainRepository: Send + Sync {
    /// All domain claims across all apps — used by the orchestrator's
    /// `AppDomainTable` to build its lookup cache at startup and after
    /// every write.
    async fn list_all(&self) -> Result<Vec<AppDomain>, ScriptRepositoryError>;
    async fn list_for_app(&self, app_id: AppId) -> Result<Vec<AppDomain>, ScriptRepositoryError>;
    async fn get(&self, domain_id: Uuid) -> Result<Option<AppDomain>, ScriptRepositoryError>;
    async fn create(&self, input: NewAppDomain) -> Result<AppDomain, ScriptRepositoryError>;
    async fn delete(&self, domain_id: Uuid) -> Result<(), ScriptRepositoryError>;
}

pub struct PostgresAppDomainRepository {
    pool: PgPool,
}

impl PostgresAppDomainRepository {
    #[must_use]
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

#[async_trait]
impl AppDomainRepository for PostgresAppDomainRepository {
    async fn list_all(&self) -> Result<Vec<AppDomain>, ScriptRepositoryError> {
        let rows = sqlx::query_as::<_, DomainRow>(
            "SELECT id, app_id, pattern, shape, shape_key, created_at \
             FROM app_domains ORDER BY pattern",
        )
        .fetch_all(&self.pool)
        .await?;
        Ok(rows.into_iter().map(Into::into).collect())
    }

    async fn list_for_app(&self, app_id: AppId) -> Result<Vec<AppDomain>, ScriptRepositoryError> {
        let rows = sqlx::query_as::<_, DomainRow>(
            "SELECT id, app_id, pattern, shape, shape_key, created_at \
             FROM app_domains WHERE app_id = $1 ORDER BY pattern",
        )
        .bind(app_id.into_inner())
        .fetch_all(&self.pool)
        .await?;
        Ok(rows.into_iter().map(Into::into).collect())
    }

    async fn get(&self, domain_id: Uuid) -> Result<Option<AppDomain>, ScriptRepositoryError> {
        let row = sqlx::query_as::<_, DomainRow>(
            "SELECT id, app_id, pattern, shape, shape_key, created_at \
             FROM app_domains WHERE id = $1",
        )
        .bind(domain_id)
        .fetch_optional(&self.pool)
        .await?;
        Ok(row.map(Into::into))
    }

    async fn create(&self, input: NewAppDomain) -> Result<AppDomain, ScriptRepositoryError> {
        let res = sqlx::query_as::<_, DomainRow>(
            "INSERT INTO app_domains (app_id, pattern, shape, shape_key) \
             VALUES ($1, $2, $3, $4) \
             RETURNING id, app_id, pattern, shape, shape_key, created_at",
        )
        .bind(input.app_id.into_inner())
        .bind(&input.pattern)
        .bind(shape_str(input.shape))
        .bind(&input.shape_key)
        .fetch_one(&self.pool)
        .await;

        match res {
            Ok(row) => Ok(row.into()),
            Err(sqlx::Error::Database(e)) if e.is_unique_violation() => {
                Err(ScriptRepositoryError::Conflict(format!(
                    "domain {:?} (or another claim of the same shape) is already claimed",
                    input.pattern
                )))
            }
            Err(e) => Err(e.into()),
        }
    }

    async fn delete(&self, domain_id: Uuid) -> Result<(), ScriptRepositoryError> {
        let res = sqlx::query("DELETE FROM app_domains WHERE id = $1")
            .bind(domain_id)
            .execute(&self.pool)
            .await?;
        if res.rows_affected() == 0 {
            return Err(ScriptRepositoryError::Conflict(format!(
                "domain {domain_id} not found"
            )));
        }
        Ok(())
    }
}

const fn shape_str(s: DomainShape) -> &'static str {
    match s {
        DomainShape::Exact => "exact",
        DomainShape::Wildcard => "wildcard",
        DomainShape::Parameterized => "parameterized",
    }
}

#[derive(sqlx::FromRow)]
struct DomainRow {
    id: Uuid,
    app_id: Uuid,
    pattern: String,
    shape: String,
    shape_key: String,
    created_at: chrono::DateTime<chrono::Utc>,
}

impl From<DomainRow> for AppDomain {
    fn from(r: DomainRow) -> Self {
        Self {
            id: r.id,
            app_id: r.app_id.into(),
            pattern: r.pattern,
            shape: match r.shape.as_str() {
                "wildcard" => DomainShape::Wildcard,
                "parameterized" => DomainShape::Parameterized,
                _ => DomainShape::Exact,
            },
            shape_key: r.shape_key,
            created_at: r.created_at,
        }
    }
}
