"""入口模块管理服务实现。"""

from __future__ import annotations

import json
from datetime import datetime
from pathlib import Path
from typing import Any

from sqlalchemy import bindparam, text

from fastapi_admin.config import OSS_BASE_URL, OSS_BUCKET
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException

from fastapi_modules.fastapi_leaudit.domian.Dto.entryModuleDto import (
    EntryModuleAreaDTO,
    EntryModuleCreateDTO,
    EntryModuleTenantDTO,
    EntryModuleUpdateDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.entryModuleAdminVo import (
    EntryModuleImageUploadVO,
    EntryModuleListVO,
    EntryModuleTenantVO,
    EntryModuleVO,
)
from fastapi_modules.fastapi_leaudit.services.entryModuleAdminService import IEntryModuleAdminService
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolution, TenantResolver


class EntryModuleAdminServiceImpl(IEntryModuleAdminService):
    """入口模块管理服务实现。"""

    def __init__(self) -> None:
        self.OssService = OssServiceImpl()
        self.TenantResolver = TenantResolver()
        self._tenant_table_exists_cache: bool | None = None
        self._entry_module_tenant_table_exists_cache: bool | None = None

    async def ListModules(
        self,
        Name: str | None,
        Area: str | None,
        TenantCode: str | None,
        Page: int,
        PageSize: int,
    ) -> EntryModuleListVO:
        """分页查询入口模块。"""
        offset = max(Page - 1, 0) * PageSize
        filters = ["em.deleted_at IS NULL"]
        params: dict[str, object] = {"limit": PageSize, "offset": offset}

        if Name:
            filters.append("em.name ILIKE :name")
            params["name"] = f"%{Name.strip()}%"

        resolved_filter_tenant_code = await self._resolveFilterTenantCode(Area=Area, TenantCode=TenantCode)
        has_tenant_mapping_table = await self._entry_module_tenant_table_exists()
        if resolved_filter_tenant_code:
            legacy_area = (Area or "").strip()
            if not legacy_area:
                tenant_name_map = await self._loadTenantNameMap([resolved_filter_tenant_code])
                legacy_area = tenant_name_map.get(resolved_filter_tenant_code, "")
            if has_tenant_mapping_table:
                filters.append(
                    """
                    (
                        EXISTS (
                            SELECT 1
                            FROM leaudit_entry_module_tenants emt
                            WHERE emt.entry_module_id = em.id
                              AND emt.deleted_at IS NULL
                              AND emt.tenant_code = :tenant_code
                        )
                        OR (
                            NOT EXISTS (
                                SELECT 1
                                FROM leaudit_entry_module_tenants emt0
                                WHERE emt0.entry_module_id = em.id
                                  AND emt0.deleted_at IS NULL
                            )
                            AND EXISTS (
                                SELECT 1
                                FROM jsonb_array_elements(COALESCE(em.areas, '[]'::jsonb)) AS area_item
                                WHERE area_item->>'area' = :legacy_area
                            )
                        )
                    )
                    """
                )
                params["tenant_code"] = resolved_filter_tenant_code
            else:
                filters.append(
                    """
                    EXISTS (
                        SELECT 1
                        FROM jsonb_array_elements(COALESCE(em.areas, '[]'::jsonb)) AS area_item
                        WHERE area_item->>'area' = :legacy_area
                    )
                    """
                )
            params["legacy_area"] = legacy_area

        where_clause = " AND ".join(filters)
        tenant_select_sql = (
            """
            COALESCE(
                (
                    SELECT jsonb_agg(
                        jsonb_build_object(
                            'tenant_code', emt.tenant_code,
                            'tenant_name', emt.tenant_name,
                            'enabled', emt.is_enabled,
                            'sort_order', emt.sort_order
                        )
                        ORDER BY emt.sort_order ASC, emt.id ASC
                    )
                    FROM leaudit_entry_module_tenants emt
                    WHERE emt.entry_module_id = em.id
                      AND emt.deleted_at IS NULL
                ),
                '[]'::jsonb
            ) AS tenants
            """
            if has_tenant_mapping_table
            else "'[]'::jsonb AS tenants"
        )

        async with GetAsyncSession() as session:
            total = int(
                (
                    await session.execute(
                        text(f"SELECT COUNT(*) FROM leaudit_entry_modules em WHERE {where_clause}"),
                        params,
                    )
                ).scalar_one()
            )

            rows = (
                await session.execute(
                    text(
                        f"""
                        SELECT
                            em.id,
                            em.name,
                            em.description,
                            em.path,
                            em.icon_path,
                            em.areas,
                            em.sort_order,
                            em.is_enabled,
                            em.created_at,
                            em.updated_at,
                            {tenant_select_sql}
                        FROM leaudit_entry_modules em
                        WHERE {where_clause}
                        ORDER BY em.sort_order ASC, em.id ASC
                        LIMIT :limit OFFSET :offset
                        """
                    ),
                    params,
                )
            ).mappings().all()

        items = [await self._toModuleVo(row) for row in rows]
        return EntryModuleListVO(total=total, page=Page, page_size=PageSize, items=items)

    async def GetModule(self, ModuleId: int) -> EntryModuleVO:
        """获取入口模块详情。"""
        row = await self._getModuleRow(ModuleId)
        return await self._toModuleVo(row)

    async def CreateModule(self, Body: EntryModuleCreateDTO) -> EntryModuleVO:
        """创建入口模块。"""
        route_path = (Body.route_path if Body.route_path is not None else Body.path or "").strip() or None
        normalized_tenants = await self._normalizeTenants(
            Tenants=Body.tenants,
            Areas=Body.areas,
        )
        self._ensureTenantAssignments(normalized_tenants)
        legacy_areas_json = self._legacyAreasJson(normalized_tenants)

        async with GetAsyncSession() as session:
            try:
                row = (
                    await session.execute(
                        text(
                            """
                            INSERT INTO leaudit_entry_modules (
                                name, description, path, icon_path, areas, sort_order, is_enabled, created_at, updated_at, deleted_at
                            ) VALUES (
                                :name, :description, :route_path, :icon_path, CAST(:areas AS jsonb), :sort_order, TRUE, NOW(), NOW(), NULL
                            )
                            RETURNING id
                            """
                        ),
                        {
                            "name": Body.name.strip(),
                            "description": (Body.description or "").strip() or None,
                            "route_path": route_path,
                            "icon_path": None,
                            "areas": legacy_areas_json,
                            "sort_order": await self._nextSortOrder(session),
                        },
                    )
                ).mappings().one()
                module_id = int(row["id"])
                await self._syncModuleTenants(session, module_id, normalized_tenants)
                await session.commit()
            except Exception as exc:
                await session.rollback()
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"创建入口模块失败: {exc}") from exc

        return await self.GetModule(module_id)

    async def UpdateModule(self, ModuleId: int, Body: EntryModuleUpdateDTO) -> EntryModuleVO:
        """更新入口模块。"""
        current = await self._getModuleRow(ModuleId)
        incoming_route_path = Body.route_path if Body.route_path is not None else Body.path

        if Body.tenants is not None or Body.areas is not None:
            normalized_tenants = await self._normalizeTenants(Tenants=Body.tenants, Areas=Body.areas)
        else:
            normalized_tenants = await self._extractTenantsFromRow(current)
        self._ensureTenantAssignments(normalized_tenants)

        async with GetAsyncSession() as session:
            row = (
                await session.execute(
                    text(
                        """
                        UPDATE leaudit_entry_modules
                        SET
                            name = :name,
                            description = :description,
                            path = :route_path,
                            areas = CAST(:areas AS jsonb),
                            updated_at = NOW()
                        WHERE id = :module_id
                          AND deleted_at IS NULL
                        RETURNING id
                        """
                    ),
                    {
                        "module_id": ModuleId,
                        "name": Body.name.strip() if Body.name is not None else current["name"],
                        "description": Body.description.strip() if Body.description is not None else current.get("description"),
                        "route_path": incoming_route_path.strip() if incoming_route_path is not None else current.get("path"),
                        "areas": self._legacyAreasJson(normalized_tenants),
                    },
                )
            ).mappings().first()
            if not row:
                await session.rollback()
                raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "入口模块不存在")

            await self._syncModuleTenants(session, ModuleId, normalized_tenants)
            await session.commit()

        return await self.GetModule(ModuleId)

    async def DeleteModule(self, ModuleId: int) -> None:
        """删除入口模块。"""
        async with GetAsyncSession() as session:
            await session.execute(
                text(
                    """
                    UPDATE leaudit_entry_modules
                    SET deleted_at = NOW(), updated_at = NOW()
                    WHERE id = :module_id AND deleted_at IS NULL
                    """
                ),
                {"module_id": ModuleId},
            )
            if await self._entry_module_tenant_table_exists():
                await session.execute(
                    text(
                        """
                        UPDATE leaudit_entry_module_tenants
                        SET deleted_at = NOW(), updated_at = NOW()
                        WHERE entry_module_id = :module_id AND deleted_at IS NULL
                        """
                    ),
                    {"module_id": ModuleId},
                )
            await session.commit()

    async def UploadModuleImage(self, ModuleId: int, FileName: str, ContentType: str, Content: bytes) -> EntryModuleImageUploadVO:
        """上传入口模块图标。"""
        module = await self._getModuleRow(ModuleId)
        suffix = Path(FileName).suffix.lower() or ".png"
        object_key = f"documents/mz/static/img/entry_module_{ModuleId}{suffix}"
        await self.OssService.UploadBytes(ObjectKey=object_key, Content=Content, ContentType=ContentType or "application/octet-stream")

        async with GetAsyncSession() as session:
            await session.execute(
                text(
                    """
                    UPDATE leaudit_entry_modules
                    SET icon_path = :icon_path, updated_at = NOW()
                    WHERE id = :module_id AND deleted_at IS NULL
                    """
                ),
                {"module_id": ModuleId, "icon_path": object_key},
            )
            await session.commit()

        return EntryModuleImageUploadVO(
            module_id=ModuleId,
            path=object_key,
            url=f"{OSS_BASE_URL.rstrip('/')}/{OSS_BUCKET}/{object_key}",
            message=f"入口模块 {module['name']} 图标上传成功",
        )

    async def _getModuleRow(self, ModuleId: int):
        """查询入口模块原始记录。"""
        has_tenant_mapping_table = await self._entry_module_tenant_table_exists()
        tenant_select_sql = (
            """
            COALESCE(
                (
                    SELECT jsonb_agg(
                        jsonb_build_object(
                            'tenant_code', emt.tenant_code,
                            'tenant_name', emt.tenant_name,
                            'enabled', emt.is_enabled,
                            'sort_order', emt.sort_order
                        )
                        ORDER BY emt.sort_order ASC, emt.id ASC
                    )
                    FROM leaudit_entry_module_tenants emt
                    WHERE emt.entry_module_id = em.id
                      AND emt.deleted_at IS NULL
                ),
                '[]'::jsonb
            ) AS tenants
            """
            if has_tenant_mapping_table
            else "'[]'::jsonb AS tenants"
        )
        async with GetAsyncSession() as session:
            row = (
                await session.execute(
                    text(
                        f"""
                        SELECT
                            em.id,
                            em.name,
                            em.description,
                            em.path,
                            em.icon_path,
                            em.areas,
                            em.sort_order,
                            em.is_enabled,
                            em.created_at,
                            em.updated_at,
                            {tenant_select_sql}
                        FROM leaudit_entry_modules em
                        WHERE em.id = :module_id
                          AND em.deleted_at IS NULL
                        """
                    ),
                    {"module_id": ModuleId},
                )
            ).mappings().first()
        if not row:
            raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "入口模块不存在")
        return row

    async def _nextSortOrder(self, Session) -> int:
        """获取下一个排序号。"""
        max_sort = (
            await Session.execute(text("SELECT COALESCE(MAX(sort_order), 0) FROM leaudit_entry_modules WHERE deleted_at IS NULL"))
        ).scalar_one()
        return int(max_sort or 0) + 10

    async def _toModuleVo(self, Row) -> EntryModuleVO:
        """把数据库记录转换为 VO。"""
        tenants = await self._extractTenantsFromRow(Row)
        return EntryModuleVO(
            id=int(Row["id"]),
            name=str(Row["name"] or ""),
            description=Row.get("description"),
            path=Row.get("icon_path"),
            route_path=Row.get("path"),
            sort_order=int(Row.get("sort_order") or 0),
            is_enabled=bool(Row.get("is_enabled", True)),
            tenants=tenants,
            created_at=self._toIso(Row.get("created_at")),
            updated_at=self._toIso(Row.get("updated_at")),
        )

    async def _extractTenantsFromRow(self, Row) -> list[EntryModuleTenantVO]:
        raw_tenants = Row.get("tenants") or []
        normalized: list[EntryModuleTenantVO] = []
        if isinstance(raw_tenants, list) and raw_tenants:
            for item in raw_tenants:
                if not isinstance(item, dict) or not item.get("tenant_code"):
                    continue
                normalized.append(
                    EntryModuleTenantVO(
                        tenant_code=str(item["tenant_code"]),
                        tenant_name=item.get("tenant_name"),
                        enabled=bool(item.get("enabled", True)),
                        sort_order=int(item.get("sort_order", 0)),
                    )
                )
            normalized.sort(key=lambda item: (item.sort_order, item.tenant_code))
            if normalized:
                return normalized

        raw_areas = Row.get("areas") or []
        if not isinstance(raw_areas, list):
            return []

        fallback_tenants: list[EntryModuleTenantVO] = []
        for index, item in enumerate(raw_areas, start=1):
            if not isinstance(item, dict) or not item.get("area"):
                continue
            resolution = await self._resolveLegacyTenantValue(
                RawValue=str(item.get("area") or ""),
                Source="entry_module_legacy_area",
            )
            fallback_tenants.append(
                EntryModuleTenantVO(
                    tenant_code=resolution.tenant_code,
                    tenant_name=resolution.tenant_name,
                    enabled=bool(item.get("enabled", True)),
                    sort_order=int(item.get("sort_order", index)),
                )
            )

        unique: dict[str, EntryModuleTenantVO] = {}
        for item in fallback_tenants:
            unique[item.tenant_code] = item
        return sorted(unique.values(), key=lambda item: (item.sort_order, item.tenant_code))

    async def _normalizeTenants(
        self,
        *,
        Tenants: list[EntryModuleTenantDTO] | None,
        Areas: list[EntryModuleAreaDTO] | None,
    ) -> list[dict[str, Any]]:
        if Tenants is not None:
            return await self._normalizeTenantDtos(Tenants)
        if Areas is not None:
            # 仅兼容旧请求体，优先要求前端使用 tenants。
            return await self._normalizeAreas(Areas)
        return []

    async def _normalizeTenantDtos(self, Tenants: list[EntryModuleTenantDTO]) -> list[dict[str, Any]]:
        if not Tenants:
            return []

        tenant_codes = [str(item.tenant_code).strip() for item in Tenants if str(item.tenant_code).strip()]
        tenant_name_map = await self._loadTenantNameMap(tenant_codes)

        normalized: dict[str, dict[str, Any]] = {}
        for index, item in enumerate(Tenants, start=1):
            tenant_code = str(item.tenant_code).strip()
            if not tenant_code:
                continue
            if tenant_code not in tenant_name_map:
                raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, f"未知租户编码: {tenant_code}")
            normalized[tenant_code] = {
                "tenant_code": tenant_code,
                "tenant_name": str(item.tenant_name or tenant_name_map[tenant_code] or "").strip() or tenant_name_map[tenant_code],
                "enabled": bool(item.enabled),
                "sort_order": int(item.sort_order or index),
            }
        return sorted(normalized.values(), key=lambda item: (int(item["sort_order"]), str(item["tenant_code"])))

    async def _normalizeAreas(self, Areas: list[EntryModuleAreaDTO]) -> list[dict[str, Any]]:
        if not Areas:
            return []

        normalized: dict[str, dict[str, Any]] = {}
        for index, item in enumerate(Areas, start=1):
            area_name = str(item.area or "").strip()
            if not area_name:
                continue
            resolution = await self._resolveLegacyTenantValue(
                RawValue=area_name,
                Source="entry_module_area_payload",
            )
            normalized[resolution.tenant_code] = {
                "tenant_code": resolution.tenant_code,
                "tenant_name": resolution.tenant_name or area_name,
                "enabled": bool(item.enabled),
                "sort_order": int(item.sort_order or index),
            }
        return sorted(normalized.values(), key=lambda item: (int(item["sort_order"]), str(item["tenant_code"])))

    async def _loadTenantNameMap(self, TenantCodes: list[str]) -> dict[str, str]:
        tenant_codes = [code for code in {str(item).strip() for item in TenantCodes} if code]
        if not tenant_codes:
            return {}
        if not await self._tenant_table_exists():
            return {
                code: ("公共" if code == "PUBLIC" else "省局" if code == "PROVINCIAL" else code)
                for code in tenant_codes
            }
        async with GetAsyncSession() as session:
            rows = (
                await session.execute(
                    text(
                        """
                        SELECT tenant_code, tenant_name
                        FROM sys_tenants
                        WHERE tenant_code IN :tenant_codes
                          AND deleted_at IS NULL
                          AND is_enabled = TRUE
                        """
                    ).bindparams(bindparam("tenant_codes", expanding=True)),
                    {"tenant_codes": tenant_codes},
                )
            ).mappings().all()
        return {str(row["tenant_code"]): str(row["tenant_name"] or "") for row in rows}

    async def _tenant_table_exists(self) -> bool:
        """兼容旧环境未落 sys_tenants 时的入口模块读写。"""
        if self._tenant_table_exists_cache is not None:
            return self._tenant_table_exists_cache
        async with GetAsyncSession() as session:
            exists = bool(
                (
                    await session.execute(
                        text(
                            """
                            SELECT EXISTS (
                                SELECT 1
                                FROM information_schema.tables
                                WHERE table_schema = current_schema()
                                  AND table_name = 'sys_tenants'
                            )
                            """
                        )
                    )
                ).scalar_one()
            )
        self._tenant_table_exists_cache = exists
        return exists

    async def _entry_module_tenant_table_exists(self) -> bool:
        """兼容旧环境未落入口模块租户映射表时的读写。"""
        if self._entry_module_tenant_table_exists_cache is not None:
            return self._entry_module_tenant_table_exists_cache
        async with GetAsyncSession() as session:
            exists = bool(
                (
                    await session.execute(
                        text(
                            """
                            SELECT EXISTS (
                                SELECT 1
                                FROM information_schema.tables
                                WHERE table_schema = current_schema()
                                  AND table_name = 'leaudit_entry_module_tenants'
                            )
                            """
                        )
                    )
                ).scalar_one()
            )
        self._entry_module_tenant_table_exists_cache = exists
        return exists

    async def _resolveLegacyTenantValue(self, *, RawValue: str, Source: str) -> TenantResolution:
        resolution = await self.TenantResolver.Resolve(
            RawValue=RawValue,
            Source=Source,
        )
        if resolution.tenant_code:
            return resolution

        normalized = str(RawValue or "").strip()
        if normalized in {"", "default", "省级", "公共"}:
            public_resolution = await self.TenantResolver.Resolve(
                RawValue="",
                Source=Source,
            )
            if public_resolution.tenant_code:
                return public_resolution
            return TenantResolution(
                tenant_code="PUBLIC",
                tenant_name="公共",
                tenant_type="PUBLIC",
                raw_value=RawValue,
                normalized_value=normalized,
                source=Source,
                is_public=True,
            )
        if normalized == "省局":
            return TenantResolution(
                tenant_code="PROVINCIAL",
                tenant_name="省局",
                tenant_type="PROVINCIAL",
                raw_value=RawValue,
                normalized_value=normalized,
                source=Source,
                is_public=False,
            )
        return TenantResolution(
            tenant_code=normalized or None,
            tenant_name=normalized or None,
            tenant_type="LEGACY_AREA" if normalized else None,
            raw_value=RawValue,
            normalized_value=normalized,
            source=Source,
            is_public=False,
        )

    async def _syncModuleTenants(self, Session, ModuleId: int, Tenants: list[dict[str, Any]]) -> None:
        if not await self._entry_module_tenant_table_exists():
            return
        tenant_codes = [str(item["tenant_code"]) for item in Tenants if item.get("tenant_code")]

        if tenant_codes:
            await Session.execute(
                text(
                    """
                    UPDATE leaudit_entry_module_tenants
                    SET deleted_at = NOW(), updated_at = NOW()
                    WHERE entry_module_id = :module_id
                      AND deleted_at IS NULL
                      AND tenant_code NOT IN :tenant_codes
                    """
                ).bindparams(bindparam("tenant_codes", expanding=True)),
                {"module_id": ModuleId, "tenant_codes": tenant_codes},
            )
        else:
            await Session.execute(
                text(
                    """
                    UPDATE leaudit_entry_module_tenants
                    SET deleted_at = NOW(), updated_at = NOW()
                    WHERE entry_module_id = :module_id
                      AND deleted_at IS NULL
                    """
                ),
                {"module_id": ModuleId},
            )

        for item in Tenants:
            await Session.execute(
                text(
                    """
                    INSERT INTO leaudit_entry_module_tenants (
                        entry_module_id, tenant_code, tenant_name, is_enabled, sort_order, created_at, updated_at, deleted_at
                    ) VALUES (
                        :module_id, :tenant_code, :tenant_name, :is_enabled, :sort_order, NOW(), NOW(), NULL
                    )
                    ON CONFLICT (entry_module_id, tenant_code)
                    DO UPDATE SET
                        tenant_name = EXCLUDED.tenant_name,
                        is_enabled = EXCLUDED.is_enabled,
                        sort_order = EXCLUDED.sort_order,
                        updated_at = NOW(),
                        deleted_at = NULL
                    """
                ),
                {
                    "module_id": ModuleId,
                    "tenant_code": item["tenant_code"],
                    "tenant_name": item.get("tenant_name"),
                    "is_enabled": bool(item.get("enabled", True)),
                    "sort_order": int(item.get("sort_order", 0)),
                },
            )

    async def _resolveFilterTenantCode(self, *, Area: str | None, TenantCode: str | None) -> str | None:
        if TenantCode and TenantCode.strip():
            return TenantCode.strip()
        if Area and Area.strip():
            resolution = await self._resolveLegacyTenantValue(
                RawValue=Area.strip(),
                Source="entry_module_filter_area",
            )
            return resolution.tenant_code
        return None

    @staticmethod
    def _legacyAreasJson(Tenants: list[dict[str, Any]]) -> str:
        # 继续回写 areas 仅用于兼容旧读链路，主配置来源是 leaudit_entry_module_tenants。
        areas = [
            {
                "area": str(item.get("tenant_name") or item.get("tenant_code") or ""),
                "enabled": bool(item.get("enabled", True)),
                "sort_order": int(item.get("sort_order", 0)),
            }
            for item in Tenants
            if item.get("tenant_code")
        ]
        return json.dumps(areas, ensure_ascii=False)

    @staticmethod
    def _ensureTenantAssignments(Tenants: list[dict[str, Any]]) -> None:
        if Tenants:
            return
        raise LeauditException(
            StatusCodeEnum.HTTP_400_BAD_REQUEST,
            "入口模块至少需要配置一个适用租户",
        )

    @staticmethod
    def _toIso(Value) -> str | None:
        """时间转 ISO 字符串。"""
        if Value is None:
            return None
        if isinstance(Value, datetime):
            return Value.isoformat()
        return str(Value)
