from dataclasses import dataclass
from typing import Optional, List
from uuid import UUID

from cryptography.fernet import InvalidToken
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.encryption import (
    get_database_encryptor,
    get_encryptor,
    is_database_encryption_configured,
    is_encryption_configured,
)
from app.infra.db.metadb import models


def _normalize_postgres_dsn(dsn: str) -> str:
    if not dsn or "://" not in dsn:
        return dsn
    scheme, rest = dsn.split("://", 1)
    if scheme not in ("postgresql", "postgres", "postgresql+psycopg"):
        return dsn
    if "@" not in rest:
        return dsn
    userinfo, hostinfo = rest.rsplit("@", 1)
    if ":" not in userinfo:
        return dsn
    username, password = userinfo.split(":", 1)
    if "@" not in password:
        return dsn
    password = password.replace("@", "%40")
    return f"{scheme}://{username}:{password}@{hostinfo}"


@dataclass(frozen=True)
class ProjectDbRouting:
    project_id: UUID
    db_role: str
    db_type: str
    dsn: str
    pool_min_size: int
    pool_max_size: int


@dataclass(frozen=True)
class ProjectGeoServerInfo:
    project_id: UUID
    gs_base_url: Optional[str]
    gs_admin_user: Optional[str]
    gs_admin_password: Optional[str]
    gs_datastore_name: str
    default_extent: Optional[dict]
    srid: int


@dataclass(frozen=True)
class ProjectSummary:
    project_id: UUID
    name: str
    code: str
    description: Optional[str]
    gs_workspace: str
    status: str
    project_role: str


class MetadataRepository:
    """元数据访问层（system_hub）"""

    def __init__(self, session: AsyncSession):
        self.session = session

    async def get_user_by_keycloak_id(self, keycloak_id: UUID) -> Optional[models.User]:
        result = await self.session.execute(
            select(models.User).where(models.User.keycloak_id == keycloak_id)
        )
        return result.scalar_one_or_none()

    async def get_user_by_username(self, username: str) -> Optional[models.User]:
        result = await self.session.execute(
            select(models.User).where(models.User.username == username)
        )
        return result.scalar_one_or_none()

    async def get_project_by_id(self, project_id: UUID) -> Optional[models.Project]:
        result = await self.session.execute(
            select(models.Project).where(models.Project.id == project_id)
        )
        return result.scalar_one_or_none()

    async def get_membership_role(
        self, project_id: UUID, user_id: UUID
    ) -> Optional[str]:
        result = await self.session.execute(
            select(models.UserProjectMembership.project_role).where(
                models.UserProjectMembership.project_id == project_id,
                models.UserProjectMembership.user_id == user_id,
            )
        )
        return result.scalar_one_or_none()

    async def get_project_db_routing(
        self, project_id: UUID, db_role: str
    ) -> Optional[ProjectDbRouting]:
        result = await self.session.execute(
            select(models.ProjectDatabase).where(
                models.ProjectDatabase.project_id == project_id,
                models.ProjectDatabase.db_role == db_role,
            )
        )
        record = result.scalar_one_or_none()
        if not record:
            return None
        if not is_database_encryption_configured():
            raise ValueError("DATABASE_ENCRYPTION_KEY is not configured")
        encryptor = get_database_encryptor()
        try:
            dsn = encryptor.decrypt(record.dsn_encrypted)
        except InvalidToken:
            raise ValueError(
                "Failed to decrypt project DB DSN: DATABASE_ENCRYPTION_KEY mismatch "
                "or invalid dsn_encrypted value"
            )
        dsn = _normalize_postgres_dsn(dsn)
        return ProjectDbRouting(
            project_id=record.project_id,
            db_role=record.db_role,
            db_type=record.db_type,
            dsn=dsn,
            pool_min_size=record.pool_min_size,
            pool_max_size=record.pool_max_size,
        )

    async def get_geoserver_config(
        self, project_id: UUID
    ) -> Optional[ProjectGeoServerInfo]:
        result = await self.session.execute(
            select(models.ProjectGeoServerConfig).where(
                models.ProjectGeoServerConfig.project_id == project_id
            )
        )
        record = result.scalar_one_or_none()
        if not record:
            return None
        if record.gs_admin_password_encrypted:
            if is_encryption_configured():
                encryptor = get_encryptor()
                password = encryptor.decrypt(record.gs_admin_password_encrypted)
            else:
                password = record.gs_admin_password_encrypted
        else:
            password = None
        return ProjectGeoServerInfo(
            project_id=record.project_id,
            gs_base_url=record.gs_base_url,
            gs_admin_user=record.gs_admin_user,
            gs_admin_password=password,
            gs_datastore_name=record.gs_datastore_name,
            default_extent=record.default_extent,
            srid=record.srid,
        )

    async def list_projects_for_user(self, user_id: UUID) -> List[ProjectSummary]:
        stmt = (
            select(models.Project, models.UserProjectMembership.project_role)
            .join(
                models.UserProjectMembership,
                models.UserProjectMembership.project_id == models.Project.id,
            )
            .where(models.UserProjectMembership.user_id == user_id)
            .order_by(models.Project.name)
        )
        result = await self.session.execute(stmt)
        return [
            ProjectSummary(
                project_id=project.id,
                name=project.name,
                code=project.code,
                description=project.description,
                gs_workspace=project.gs_workspace,
                status=project.status,
                project_role=role,
            )
            for project, role in result.all()
        ]

    async def list_all_projects(self) -> List[ProjectSummary]:
        result = await self.session.execute(
            select(models.Project).order_by(models.Project.name)
        )
        return [
            ProjectSummary(
                project_id=project.id,
                name=project.name,
                code=project.code,
                description=project.description,
                gs_workspace=project.gs_workspace,
                status=project.status,
                project_role="owner",
            )
            for project in result.scalars().all()
        ]
