from __future__ import annotations
import re
import secrets
from datetime import datetime, timezone
from uuid import UUID
from sqlalchemy import select
from src.core.exceptions import ConflictError, NotFoundError, ValidationError
from src.domain.domains.models import WorkspaceDomain, DomainAssignment
from src.domain.domains.repository import DomainRepository
from src.domain.projects.models import Project
from src.domain.deployments.models import ProjectDomain

_LABEL = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")


def normalize_domain(raw: str) -> str:
    value = raw.strip().lower()
    had_scheme = False
    for prefix in ("https://", "http://"):
        if value.startswith(prefix):
            value = value[len(prefix):]
            had_scheme = True
    if "/" in value and not had_scheme:
        raise ValidationError("Enter a valid domain name or hostname")
    value = value.split("/", 1)[0].rstrip(".")
    if ".." in value or len(value) > 255:
        raise ValidationError("Enter a valid domain name or hostname")
    labels = value.split(".")
    if len(labels) < 2 or any(not _LABEL.match(x) for x in labels):
        raise ValidationError("Enter a valid domain name or hostname")
    return value


def parent_domain(hostname: str) -> str:
    labels = hostname.split(".")
    return ".".join(labels[-2:])


class DomainService:
    def __init__(self, repo: DomainRepository): self.repo = repo

    async def connect(self, workspace_id: UUID, domain: str, actor_id: UUID) -> WorkspaceDomain:
        normalized = normalize_domain(domain)
        existing = await self.repo.get_by_name(normalized)
        if existing and existing.workspace_id != workspace_id:
            raise ConflictError("This domain is already claimed by another workspace")
        if existing: return existing
        d = WorkspaceDomain(
            workspace_id=workspace_id,
            domain_name=normalized,
            normalized_domain=normalized,
            domain_type="external",
            registration_status="unmanaged",
            verification_status="pending",
            metadata_json={"verification_token": "p2hs-verify=" + secrets.token_urlsafe(24)},
            created_by=actor_id,
        )
        self.repo.db.add(d)
        await self.repo.db.flush()
        await self.repo.add_event(workspace_id, "domain.connected", actor_id, d.id, {"domain": normalized})
        return d

    async def assign(self, workspace_id: UUID, domain_id: UUID, project_id: UUID, hostname: str | None, subdomain: str | None, environment: str, actor_id: UUID) -> DomainAssignment:
        d = await self.repo.get(domain_id, workspace_id)
        if not d: raise NotFoundError("Domain not found in this workspace")
        if d.verification_status != "verified":
            raise ValidationError("Verify the domain before assigning it to a project")
        if hostname and subdomain: raise ValidationError("Choose hostname or subdomain, not both")
        host = normalize_domain(hostname or (f"{subdomain.strip().lower()}.{d.normalized_domain}" if subdomain else d.normalized_domain))
        if host != d.normalized_domain and not host.endswith("." + d.normalized_domain):
            raise ValidationError("Hostname must belong to the selected domain")
        conflict = await self.repo.active_hostname(host)
        if conflict: raise ConflictError("That exact hostname is already assigned")
        project = await self.repo.db.scalar(select(Project).where(Project.id == project_id, Project.workspace_id == workspace_id, Project.deleted_at.is_(None)))
        if not project:
            raise NotFoundError("Project not found in this workspace")
        legacy = await self.repo.db.scalar(select(ProjectDomain).where(ProjectDomain.project_id == project_id, ProjectDomain.domain == host, ProjectDomain.environment == environment))
        if not legacy:
            self.repo.db.add(ProjectDomain(project_id=project_id, domain=host, environment=environment, verification_txt=None, verified_at=datetime.now(timezone.utc)))
        assignment = DomainAssignment(domain_id=d.id, workspace_id=workspace_id, project_id=project_id, hostname=host, environment=environment, assignment_status="active", dns_status="pending", tls_status="pending")
        self.repo.db.add(assignment)
        await self.repo.db.flush()
        await self.repo.add_event(workspace_id, "domain.assignment.created", actor_id, d.id, {"project_id": str(project_id), "hostname": host})
        return assignment

    async def unassign(self, workspace_id: UUID, assignment_id: UUID, actor_id: UUID) -> None:
        assignment = await self.repo.db.scalar(select(DomainAssignment).where(DomainAssignment.id == assignment_id, DomainAssignment.workspace_id == workspace_id))
        if not assignment:
            raise NotFoundError("Domain assignment not found")
        await self.repo.add_event(workspace_id, "domain.assignment.deleted", actor_id, assignment.domain_id, {"project_id": str(assignment.project_id), "hostname": assignment.hostname})
        legacy = await self.repo.db.scalar(select(ProjectDomain).where(ProjectDomain.project_id == assignment.project_id, ProjectDomain.domain == assignment.hostname, ProjectDomain.environment == assignment.environment))
        if legacy:
            await self.repo.db.delete(legacy)
        await self.repo.db.delete(assignment)
