from typing import List
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Path, Query, Response, status
from sqlalchemy import text
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.ext.asyncio import create_async_engine

from app.auth.metadata_dependencies import (
    get_current_metadata_admin,
    get_metadata_repository,
)
from app.core.audit import AuditAction, log_audit_event
from app.domain.schemas.admin_metadata import (
    AdminProjectCreateRequest,
    AdminProjectResponse,
    AdminProjectUpdateRequest,
    MetadataUsersBatchSyncRequest,
    MetadataUserResponse,
    MetadataUserSyncRequest,
    MetadataUserSyncResult,
    MetadataUserUpdateRequest,
    ProjectDatabaseHealthResponse,
    ProjectDatabaseHealthRequest,
    ProjectDatabaseResponse,
    ProjectDatabaseUpsertRequest,
    ProjectDbRole,
    ProjectMemberCreateRequest,
    ProjectMemberResponse,
    ProjectMemberUpdateRequest,
)
from app.infra.db.metadb import models
from app.infra.db.metadb.repositories.metadata_repository import (
    MetadataRepository,
    ProjectDbRouting,
)

router = APIRouter()


def _project_response(project: models.Project) -> AdminProjectResponse:
    return AdminProjectResponse(
        project_id=project.id,
        name=project.name,
        code=project.code,
        description=project.description,
        gs_workspace=project.gs_workspace,
        map_extent=project.map_extent,
        status=project.status,
        created_at=project.created_at,
        updated_at=project.updated_at,
    )


def _project_database_response(
    record: models.ProjectDatabase,
) -> ProjectDatabaseResponse:
    return ProjectDatabaseResponse(
        id=record.id,
        project_id=record.project_id,
        db_role=record.db_role,
        db_type=record.db_type,
        pool_min_size=record.pool_min_size,
        pool_max_size=record.pool_max_size,
        has_dsn=bool(record.dsn_encrypted),
    )


def _database_audit_payload(payload: ProjectDatabaseUpsertRequest) -> dict:
    return {
        "db_role": payload.db_role,
        "db_type": _db_type_for_role(payload.db_role),
        "pool_min_size": payload.pool_min_size,
        "pool_max_size": payload.pool_max_size,
        "dsn_updated": payload.dsn is not None,
    }


def _to_async_sqlalchemy_url(dsn: str) -> str:
    parsed = make_url(dsn)
    if parsed.drivername in {"postgresql", "postgres"}:
        parsed = parsed.set(drivername="postgresql+psycopg")
    return parsed.render_as_string(hide_password=False)


def _db_type_for_role(db_role: str) -> str:
    if db_role == "iot_data":
        return "timescaledb"
    return "postgresql"


def _status_for_config_value_error(exc: ValueError) -> int:
    if "DATABASE_ENCRYPTION_KEY" in str(exc):
        return status.HTTP_503_SERVICE_UNAVAILABLE
    return status.HTTP_400_BAD_REQUEST


async def _check_database_connection(routing: ProjectDbRouting) -> None:
    engine = create_async_engine(
        _to_async_sqlalchemy_url(routing.dsn),
        pool_size=1,
        max_overflow=0,
        pool_pre_ping=True,
    )
    try:
        async with engine.connect() as conn:
            await conn.execute(text("SELECT 1"))
    finally:
        await engine.dispose()


def _database_health_error_detail(exc: Exception) -> str:
    message = str(exc)
    lower_message = message.lower()
    if "password authentication failed" in lower_message:
        return "连通性测试失败：用户名或密码错误，请检查 DSN 中的账号密码。"
    if "connection refused" in lower_message:
        return "连通性测试失败：目标主机或端口拒绝连接，请检查地址、端口和服务状态。"
    if "timeout" in lower_message or "timed out" in lower_message:
        return "连通性测试失败：连接超时，请检查网络、防火墙和数据库服务状态。"
    if "could not translate host name" in lower_message or "name or service not known" in lower_message:
        return "连通性测试失败：数据库主机名无法解析，请检查 DSN 中的主机地址。"
    first_line = message.splitlines()[0] if message else exc.__class__.__name__
    return f"连通性测试失败：{first_line}"


async def _upsert_and_audit_metadata_user(
    payload: MetadataUserSyncRequest,
    *,
    current_user,
    metadata_repo: MetadataRepository,
    response_status: int,
) -> MetadataUserResponse:
    user = await metadata_repo.upsert_user_from_keycloak(
        keycloak_id=payload.keycloak_id,
        username=payload.username,
        email=str(payload.email),
        role=payload.role,
        is_active=payload.is_active,
    )
    await log_audit_event(
        action=AuditAction.UPDATE,
        user_id=current_user.id,
        resource_type="metadata_user",
        resource_id=str(user.id),
        request_data=payload.model_dump(mode="json"),
        response_status=response_status,
        session=metadata_repo.session,
    )
    return MetadataUserResponse.model_validate(user)


@router.get("/me", response_model=MetadataUserResponse)
async def get_metadata_admin_me(
    current_user=Depends(get_current_metadata_admin),
) -> MetadataUserResponse:
    return MetadataUserResponse.model_validate(current_user)


@router.post("/users/sync", response_model=MetadataUserResponse)
async def sync_metadata_user(
    payload: MetadataUserSyncRequest,
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> MetadataUserResponse:
    try:
        return await _upsert_and_audit_metadata_user(
            payload,
            current_user=current_user,
            metadata_repo=metadata_repo,
            response_status=status.HTTP_200_OK,
        )
    except IntegrityError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="User keycloak_id, username, or email conflicts with an existing user",
        ) from exc
    except SQLAlchemyError as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Metadata database error: {exc}",
        ) from exc



@router.post("/users/sync/batch", response_model=List[MetadataUserSyncResult])
async def sync_metadata_users_batch(
    payload: MetadataUsersBatchSyncRequest,
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> List[MetadataUserSyncResult]:
    results: list[MetadataUserSyncResult] = []
    for item in payload.users:
        try:
            user = await _upsert_and_audit_metadata_user(
                item,
                current_user=current_user,
                metadata_repo=metadata_repo,
                response_status=status.HTTP_200_OK,
            )
        except IntegrityError as exc:
            results.append(
                MetadataUserSyncResult(
                    keycloak_id=item.keycloak_id,
                    success=False,
                    error="User keycloak_id, username, or email conflicts with an existing user",
                )
            )
            await metadata_repo.session.rollback()
        except SQLAlchemyError as exc:
            results.append(
                MetadataUserSyncResult(
                    keycloak_id=item.keycloak_id,
                    success=False,
                    error=f"Metadata database error: {exc}",
                )
            )
            await metadata_repo.session.rollback()
        else:
            results.append(
                MetadataUserSyncResult(
                    keycloak_id=item.keycloak_id,
                    success=True,
                    user=user,
                )
            )
    return results


@router.get("/users", response_model=List[MetadataUserResponse])
async def list_metadata_users(
    skip: int = Query(0, ge=0),
    limit: int = Query(100, ge=1, le=1000),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> List[MetadataUserResponse]:
    users = await metadata_repo.list_users(skip=skip, limit=limit)
    return [MetadataUserResponse.model_validate(user) for user in users]


@router.get("/projects", response_model=List[AdminProjectResponse])
async def list_admin_projects(
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> List[AdminProjectResponse]:
    projects = await metadata_repo.list_project_records()
    return [_project_response(project) for project in projects]


@router.post(
    "/projects",
    response_model=AdminProjectResponse,
    status_code=status.HTTP_201_CREATED,
)
async def create_admin_project(
    payload: AdminProjectCreateRequest,
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> AdminProjectResponse:
    try:
        project = await metadata_repo.create_project(
            name=payload.name,
            code=payload.code,
            description=payload.description,
            gs_workspace=payload.gs_workspace,
            map_extent=payload.map_extent,
            status=payload.status,
        )
    except IntegrityError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Project code or workspace conflicts with an existing project",
        ) from exc
    except SQLAlchemyError as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Metadata database error: {exc}",
        ) from exc

    await log_audit_event(
        action=AuditAction.CREATE,
        user_id=current_user.id,
        project_id=project.id,
        resource_type="project",
        resource_id=str(project.id),
        request_data=payload.model_dump(mode="json"),
        response_status=status.HTTP_201_CREATED,
        session=metadata_repo.session,
    )
    return _project_response(project)


@router.patch(
    "/projects/{project_id}",
    response_model=AdminProjectResponse,
)
async def update_admin_project(
    payload: AdminProjectUpdateRequest,
    project_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> AdminProjectResponse:
    updates = payload.model_dump(mode="json", exclude_unset=True)
    try:
        project = await metadata_repo.update_project(project_id, updates=updates)
    except IntegrityError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Project code or workspace conflicts with an existing project",
        ) from exc
    except SQLAlchemyError as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Metadata database error: {exc}",
        ) from exc
    if project is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")

    await log_audit_event(
        action=AuditAction.UPDATE,
        user_id=current_user.id,
        project_id=project.id,
        resource_type="project",
        resource_id=str(project.id),
        request_data=updates,
        response_status=status.HTTP_200_OK,
        session=metadata_repo.session,
    )
    return _project_response(project)


@router.get(
    "/projects/{project_id}/databases",
    response_model=List[ProjectDatabaseResponse],
)
async def list_project_databases(
    project_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> List[ProjectDatabaseResponse]:
    project = await metadata_repo.get_project_by_id(project_id)
    if project is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
    records = await metadata_repo.list_project_databases(project_id)
    return [_project_database_response(record) for record in records]


@router.put(
    "/projects/{project_id}/databases",
    response_model=ProjectDatabaseResponse,
)
async def upsert_project_database(
    payload: ProjectDatabaseUpsertRequest,
    project_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> ProjectDatabaseResponse:
    project = await metadata_repo.get_project_by_id(project_id)
    if project is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
    try:
        routing = (
            ProjectDbRouting(
                project_id=project_id,
                db_role=payload.db_role,
                db_type=_db_type_for_role(payload.db_role),
                dsn=payload.dsn,
                pool_min_size=payload.pool_min_size,
                pool_max_size=payload.pool_max_size,
            )
            if payload.dsn
            else await metadata_repo.get_project_db_routing(project_id, payload.db_role)
        )
        if routing is None:
            raise ValueError("dsn is required when creating project database config")
        await _check_database_connection(routing)
    except ValueError as exc:
        raise HTTPException(
            status_code=_status_for_config_value_error(exc),
            detail=str(exc),
        ) from exc
    except Exception as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=_database_health_error_detail(exc),
        ) from exc

    try:
        record = await metadata_repo.upsert_project_database_config(
            project_id,
            db_role=payload.db_role,
            db_type=_db_type_for_role(payload.db_role),
            dsn=payload.dsn,
            pool_min_size=payload.pool_min_size,
            pool_max_size=payload.pool_max_size,
        )
    except IntegrityError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Project database role conflicts with an existing config",
        ) from exc
    except SQLAlchemyError as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Metadata database error: {exc}",
        ) from exc

    await log_audit_event(
        action=AuditAction.CONFIG_CHANGE,
        user_id=current_user.id,
        project_id=project_id,
        resource_type="project_database",
        resource_id=payload.db_role,
        request_data=_database_audit_payload(payload),
        response_status=status.HTTP_200_OK,
        session=metadata_repo.session,
    )
    return _project_database_response(record)


@router.delete(
    "/projects/{project_id}/databases/{db_role}",
    status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_project_database(
    project_id: UUID = Path(...),
    db_role: ProjectDbRole = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> None:
    removed = await metadata_repo.delete_project_database_config(project_id, db_role)
    if not removed:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Project database config not found",
        )
    await log_audit_event(
        action=AuditAction.CONFIG_CHANGE,
        user_id=current_user.id,
        project_id=project_id,
        resource_type="project_database",
        resource_id=db_role,
        request_data={"deleted": True},
        response_status=status.HTTP_204_NO_CONTENT,
        session=metadata_repo.session,
    )


@router.post(
    "/projects/{project_id}/databases/{db_role}/health",
    response_model=ProjectDatabaseHealthResponse,
)
async def check_project_database_health(
    response: Response,
    project_id: UUID = Path(...),
    db_role: ProjectDbRole = Path(...),
    payload: ProjectDatabaseHealthRequest | None = None,
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> ProjectDatabaseHealthResponse:
    dsn_to_test = payload.dsn if payload and payload.dsn else None
    if dsn_to_test:
        routing = ProjectDbRouting(
            project_id=project_id,
            db_role=db_role,
            db_type=_db_type_for_role(db_role),
            dsn=dsn_to_test,
            pool_min_size=1,
            pool_max_size=1,
        )
    else:
        try:
            routing = await metadata_repo.get_project_db_routing(project_id, db_role)
        except ValueError as exc:
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail=f"Project database routing DSN is invalid: {exc}",
            ) from exc
        if routing is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Project database config not found",
            )

    try:
        await _check_database_connection(routing)
    except Exception as exc:  # health endpoint should return diagnostic status
        response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
        return ProjectDatabaseHealthResponse(
            project_id=project_id,
            db_role=db_role,
            db_type=routing.db_type,
            ok=False,
            detail=_database_health_error_detail(exc),
        )
    return ProjectDatabaseHealthResponse(
        project_id=project_id,
        db_role=db_role,
        db_type=routing.db_type,
        ok=True,
        detail="连通性测试通过",
    )


@router.get("/users/{user_id}", response_model=MetadataUserResponse)
async def get_metadata_user(
    user_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> MetadataUserResponse:
    user = await metadata_repo.get_user_by_id(user_id)
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
    return MetadataUserResponse.model_validate(user)


@router.patch("/users/{user_id}", response_model=MetadataUserResponse)
async def update_metadata_user(
    payload: MetadataUserUpdateRequest,
    user_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> MetadataUserResponse:
    updates = payload.model_dump(mode="json", exclude_unset=True)
    if user_id == current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Users cannot modify themselves",
        )
    user = await metadata_repo.update_user_admin(
        user_id,
        updates=updates,
    )
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")

    await log_audit_event(
        action=AuditAction.UPDATE,
        user_id=current_user.id,
        resource_type="metadata_user",
        resource_id=str(user.id),
        request_data=updates,
        response_status=status.HTTP_200_OK,
        session=metadata_repo.session,
    )
    return MetadataUserResponse.model_validate(user)


@router.get(
    "/projects/{project_id}/members",
    response_model=List[ProjectMemberResponse],
)
async def list_project_members(
    project_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> List[ProjectMemberResponse]:
    project = await metadata_repo.get_project_by_id(project_id)
    if project is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
        )
    members = await metadata_repo.list_project_members(project_id)
    return [ProjectMemberResponse(**member.__dict__) for member in members]


@router.post(
    "/projects/{project_id}/members",
    response_model=ProjectMemberResponse,
    status_code=status.HTTP_201_CREATED,
)
async def add_project_member(
    payload: ProjectMemberCreateRequest,
    project_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> ProjectMemberResponse:
    if payload.user_id == current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Users cannot modify their own project membership",
        )
    project = await metadata_repo.get_project_by_id(project_id)
    if project is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
        )
    user = await metadata_repo.get_user_by_id(payload.user_id)
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
    existing = await metadata_repo.get_project_membership(project_id, payload.user_id)
    if existing is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="User is already a project member",
        )

    membership = await metadata_repo.add_project_member(
        project_id, payload.user_id, payload.project_role
    )
    await log_audit_event(
        action=AuditAction.PERMISSION_CHANGE,
        user_id=current_user.id,
        project_id=project_id,
        resource_type="project_member",
        resource_id=str(payload.user_id),
        request_data=payload.model_dump(mode="json"),
        response_status=status.HTTP_201_CREATED,
        session=metadata_repo.session,
    )
    return ProjectMemberResponse(
        id=membership.id,
        user_id=membership.user_id,
        project_id=membership.project_id,
        project_role=membership.project_role,
        username=user.username,
        email=user.email,
        is_active=user.is_active,
    )


@router.patch(
    "/projects/{project_id}/members/{user_id}",
    response_model=ProjectMemberResponse,
)
async def update_project_member(
    payload: ProjectMemberUpdateRequest,
    project_id: UUID = Path(...),
    user_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> ProjectMemberResponse:
    if user_id == current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Users cannot modify their own project membership",
        )
    user = await metadata_repo.get_user_by_id(user_id)
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
    membership = await metadata_repo.update_project_member_role(
        project_id, user_id, payload.project_role
    )
    if membership is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Project member not found"
        )
    await log_audit_event(
        action=AuditAction.PERMISSION_CHANGE,
        user_id=current_user.id,
        project_id=project_id,
        resource_type="project_member",
        resource_id=str(user_id),
        request_data=payload.model_dump(mode="json"),
        response_status=status.HTTP_200_OK,
        session=metadata_repo.session,
    )
    return ProjectMemberResponse(
        id=membership.id,
        user_id=membership.user_id,
        project_id=membership.project_id,
        project_role=membership.project_role,
        username=user.username,
        email=user.email,
        is_active=user.is_active,
    )


@router.delete("/projects/{project_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_project_member(
    project_id: UUID = Path(...),
    user_id: UUID = Path(...),
    current_user=Depends(get_current_metadata_admin),
    metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> None:
    if user_id == current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Users cannot modify their own project membership",
        )
    removed = await metadata_repo.remove_project_member(project_id, user_id)
    if not removed:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Project member not found"
        )
    await log_audit_event(
        action=AuditAction.PERMISSION_CHANGE,
        user_id=current_user.id,
        project_id=project_id,
        resource_type="project_member",
        resource_id=str(user_id),
        response_status=status.HTTP_204_NO_CONTENT,
        session=metadata_repo.session,
    )
