"""Deployments domain - data access layer."""
from __future__ import annotations
import secrets
from datetime import datetime, timezone
from typing import Any, Optional
from uuid import UUID

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.domain.deployments.models import ProjectDeploy, ProjectDeploymentTarget, ProjectDomain


class DeployRepository:
    def __init__(self, db: AsyncSession):
        self._db = db

    async def get_by_id(self, deploy_id: UUID) -> Optional[ProjectDeploy]:
        result = await self._db.execute(
            select(ProjectDeploy).where(ProjectDeploy.id == deploy_id)
        )
        return result.scalar_one_or_none()

    async def find_for_revision_environment(
        self,
        project_id: UUID,
        revision_id: UUID,
        environment: str,
    ) -> Optional[ProjectDeploy]:
        result = await self._db.execute(
            select(ProjectDeploy)
            .where(
                ProjectDeploy.project_id == project_id,
                ProjectDeploy.revision_id == revision_id,
                ProjectDeploy.environment == environment,
            )
            .order_by(ProjectDeploy.created_at.desc())
            .limit(1)
        )
        return result.scalar_one_or_none()

    async def list_for_project(self, project_id: UUID) -> list[ProjectDeploy]:
        result = await self._db.execute(
            select(ProjectDeploy)
            .where(ProjectDeploy.project_id == project_id)
            .order_by(ProjectDeploy.created_at.desc())
        )
        return list(result.scalars().all())

    async def get_active_for_project_environment(
        self, project_id: UUID, environment: str
    ) -> Optional[ProjectDeploy]:
        """Return the current in-flight deployment, if one exists."""
        result = await self._db.execute(
            select(ProjectDeploy)
            .where(
                ProjectDeploy.project_id == project_id,
                ProjectDeploy.environment == environment,
                ProjectDeploy.status.in_(("pending", "deploying")),
            )
            .order_by(ProjectDeploy.created_at.desc())
            .limit(1)
        )
        return result.scalar_one_or_none()

    async def create(
        self,
        project_id: UUID,
        revision_id: UUID,
        environment: str,
        created_by: Optional[UUID] = None,
        deployment_metadata: Optional[dict[str, Any]] = None,
    ) -> ProjectDeploy:
        d = ProjectDeploy(
            project_id=project_id,
            revision_id=revision_id,
            environment=environment,
            status="pending",
            created_by=created_by,
            deployment_metadata=dict(deployment_metadata) if deployment_metadata else {},
        )
        self._db.add(d)
        await self._db.flush()
        await self._db.refresh(d)
        return d

    async def update_status(self, deploy_id: UUID, status: str, **kwargs) -> Optional[ProjectDeploy]:
        d = await self.get_by_id(deploy_id)
        if not d:
            return None
        d.status = status
        allowed = {
            "deploy_url",
            "provider_deploy_id",
            "created_by",
            "deployed_at",
            "rolled_back_at",
            "rolled_back_to",
            "build_id",
            "revision_id",
        }
        for k, v in kwargs.items():
            if k in allowed and v is not None:
                setattr(d, k, v)
        if status == "live" and d.deployed_at is None:
            d.deployed_at = datetime.now(timezone.utc)
        if status == "failed" and d.deployed_at is None:
            d.deployed_at = datetime.now(timezone.utc)
        await self._db.flush()
        return d

    async def apply_hosting_outcome(
        self,
        deploy_id: UUID,
        *,
        status: str,
        deploy_url: Optional[str] = None,
        provider_deploy_id: Optional[str] = None,
        hosting_provider: Optional[str] = None,
        provider_namespace: Optional[str] = None,
        provider_version_id: Optional[str] = None,
        source_git_sha: Optional[str] = None,
        activation_status: Optional[str] = None,
        last_completed_step: Optional[str] = None,
        metadata_patch: Optional[dict[str, Any]] = None,
    ) -> Optional[ProjectDeploy]:
        d = await self.get_by_id(deploy_id)
        if not d:
            return None
        d.status = status
        if deploy_url is not None:
            d.deploy_url = deploy_url
        if provider_deploy_id is not None:
            d.provider_deploy_id = provider_deploy_id
        if hosting_provider is not None:
            d.hosting_provider = hosting_provider
        if provider_namespace is not None:
            d.provider_namespace = provider_namespace
        if provider_version_id is not None:
            d.provider_version_id = provider_version_id
        if source_git_sha is not None:
            d.source_git_sha = source_git_sha
        if activation_status is not None:
            d.activation_status = activation_status
        if last_completed_step is not None:
            d.last_completed_step = last_completed_step
        if metadata_patch:
            base = dict(d.deployment_metadata or {})
            base.update(metadata_patch)
            d.deployment_metadata = base
        if status == "live" and d.deployed_at is None:
            d.deployed_at = datetime.now(timezone.utc)
        if status == "failed" and d.deployed_at is None:
            d.deployed_at = datetime.now(timezone.utc)
        await self._db.flush()
        return d


class DeploymentTargetRepository:
    def __init__(self, db: AsyncSession):
        self._db = db

    async def get(self, project_id: UUID, environment: str) -> Optional[ProjectDeploymentTarget]:
        result = await self._db.execute(
            select(ProjectDeploymentTarget)
            .where(ProjectDeploymentTarget.project_id == project_id)
            .where(ProjectDeploymentTarget.environment == environment)
        )
        return result.scalar_one_or_none()

    async def upsert(
        self,
        project_id: UUID,
        environment: str,
        target_kind: str,
        *,
        provider_project_name: Optional[str] = None,
        provider_project_id: Optional[str] = None,
        provider_deployment_id: Optional[str] = None,
        provider_preview_url: Optional[str] = None,
        provider_production_url: Optional[str] = None,
        routing_target_descriptor: Optional[dict] = None,
        latest_commit_sha: Optional[str] = None,
        latest_revision_id: Optional[UUID] = None,
        repo_linkage_status: Optional[str] = None,
        domain_status: Optional[str] = None,
    ) -> ProjectDeploymentTarget:
        from datetime import datetime, timezone

        row = await self.get(project_id, environment)
        if row is None:
            row = ProjectDeploymentTarget(
                project_id=project_id,
                environment=environment,
                target_kind=target_kind,
            )
            self._db.add(row)

        row.target_kind = target_kind
        if provider_project_name is not None:
            row.provider_project_name = provider_project_name
        if provider_project_id is not None:
            row.provider_project_id = provider_project_id
        if provider_deployment_id is not None:
            row.provider_deployment_id = provider_deployment_id
        if provider_preview_url is not None:
            row.provider_preview_url = provider_preview_url
        if provider_production_url is not None:
            row.provider_production_url = provider_production_url
        if routing_target_descriptor is not None:
            row.routing_target_descriptor = routing_target_descriptor
        if latest_commit_sha is not None:
            row.latest_commit_sha = latest_commit_sha
        if latest_revision_id is not None:
            row.latest_revision_id = latest_revision_id
        if repo_linkage_status is not None:
            row.repo_linkage_status = repo_linkage_status
        if domain_status is not None:
            row.domain_status = domain_status
        row.updated_at = datetime.now(timezone.utc)

        await self._db.flush()
        await self._db.refresh(row)
        return row


class DomainRepository:
    def __init__(self, db: AsyncSession):
        self._db = db

    async def get_by_id(self, domain_id: UUID) -> Optional[ProjectDomain]:
        result = await self._db.execute(
            select(ProjectDomain).where(ProjectDomain.id == domain_id)
        )
        return result.scalar_one_or_none()

    async def get_by_domain(self, domain: str) -> Optional[ProjectDomain]:
        result = await self._db.execute(
            select(ProjectDomain).where(ProjectDomain.domain == domain.strip().lower())
        )
        return result.scalar_one_or_none()

    async def list_for_project(self, project_id: UUID) -> list[ProjectDomain]:
        result = await self._db.execute(
            select(ProjectDomain).where(ProjectDomain.project_id == project_id)
        )
        return list(result.scalars().all())

    async def create(self, project_id: UUID, domain: str, environment: str = "production") -> ProjectDomain:
        txt = "p2hs-verify=" + secrets.token_urlsafe(24)
        d = ProjectDomain(
            project_id=project_id,
            domain=domain,
            environment=environment,
            verification_txt=txt,
        )
        self._db.add(d)
        await self._db.flush()
        await self._db.refresh(d)
        return d

    async def mark_verified(self, domain_id: UUID) -> Optional[ProjectDomain]:
        d = await self.get_by_id(domain_id)
        if not d:
            return None
        d.verified_at = datetime.now(timezone.utc)
        await self._db.flush()
        return d

    async def update_cf_hostname(
        self, domain_id: UUID, *, cf_hostname_id: str, zone_id: str
    ) -> Optional[ProjectDomain]:
        d = await self.get_by_id(domain_id)
        if not d:
            return None
        d.cf_hostname_id = cf_hostname_id
        d.cloudflare_zone_id = zone_id
        await self._db.flush()
        return d

    async def mark_tls_provisioned(self, domain_id: UUID) -> Optional[ProjectDomain]:
        d = await self.get_by_id(domain_id)
        if not d:
            return None
        d.tls_provisioned_at = datetime.now(timezone.utc)
        await self._db.flush()
        return d

    async def set_primary(self, domain_id: UUID) -> Optional[ProjectDomain]:
        d = await self.get_by_id(domain_id)
        if not d:
            return None
        result = await self._db.execute(
            select(ProjectDomain)
            .where(ProjectDomain.project_id == d.project_id)
            .where(ProjectDomain.is_primary.is_(True))
        )
        for existing in result.scalars().all():
            existing.is_primary = False
        d.is_primary = True
        await self._db.flush()
        return d

    async def list_verified_for_environment(self, project_id: UUID, environment: str) -> list[ProjectDomain]:
        """Return all verified (non-expired) custom domains for a project+environment."""
        result = await self._db.execute(
            select(ProjectDomain)
            .where(ProjectDomain.project_id == project_id)
            .where(ProjectDomain.environment == environment)
            .where(ProjectDomain.verified_at.is_not(None))
        )
        return list(result.scalars().all())

    async def delete(self, domain_id: UUID) -> None:
        d = await self.get_by_id(domain_id)
        if d:
            await self._db.delete(d)
            await self._db.flush()
