from __future__ import annotations

from datetime import UTC, datetime

from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.core.agent_enums import (
    AgentAssetContentType,
    AgentAssetStatus,
    AgentAssetType,
    AgentReviewStatus,
)
from app.core.logging import get_logger
from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion
from app.repositories.agent_asset import AgentAssetRepository
from app.schemas.agent_asset import (
    AgentAssetCreate,
    AgentAssetListItem,
    AgentAssetRead,
    AgentAssetReviewCreate,
    AgentAssetReviewRead,
    AgentAssetUpdate,
    AgentAssetVersionCreate,
    AgentAssetVersionRead,
)
from app.services.agent_asset_access import (
    AgentAssetAccessScope,
    platform_resource_identity,
)
from app.services.agent_asset_json_rules import AgentAssetJsonRuleMixin
from app.services.agent_asset_onlyoffice import AgentAssetOnlyOfficeMixin
from app.services.agent_asset_risk_rule_feedback import AgentAssetRiskRuleFeedbackMixin
from app.services.agent_asset_risk_rule_level import AgentAssetRiskRuleLevelMixin
from app.services.agent_asset_risk_rule_publish import AgentAssetRiskRulePublishMixin
from app.services.agent_asset_risk_rule_simulation import AgentAssetRiskRuleSimulationMixin
from app.services.agent_asset_risk_rule_testing import AgentAssetRiskRuleTestingMixin
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_serialization import AgentAssetSerializationMixin
from app.services.agent_asset_spreadsheet import AgentAssetSpreadsheetManager
from app.services.agent_asset_spreadsheet_helpers import AgentAssetSpreadsheetHelperMixin
from app.services.agent_asset_timeline import AgentAssetTimelineMixin
from app.services.agent_foundation import AgentFoundationService
from app.services.audit import AuditLogService
from app.services.pagination import PageResult, normalize_page_params
from app.services.risk_rule_manifest_classifier import is_budget_risk_manifest
from app.services.risk_rule_score_backfill import backfill_missing_risk_rule_score

logger = get_logger("app.services.agent_assets")


class AgentAssetVersionMixin:
    def _validate_version_payload(
        self, asset: AgentAsset, payload: AgentAssetVersionCreate
    ) -> None:
        if (
            asset.asset_type == AgentAssetType.RULE.value
            and payload.content_type != AgentAssetContentType.MARKDOWN
        ):
            raise ValueError("规则资产版本内容必须使用 markdown。")
        if (
            asset.asset_type not in {AgentAssetType.RULE.value, AgentAssetType.TASK.value}
            and payload.content_type != AgentAssetContentType.JSON
        ):
            raise ValueError("技能、MCP 资产版本内容必须使用 json。")
        if payload.content_type == AgentAssetContentType.MARKDOWN and not isinstance(
            payload.content, str
        ):
            raise ValueError("Markdown 内容必须是字符串。")
        if payload.content_type == AgentAssetContentType.JSON and not isinstance(
            payload.content, (dict, list)
        ):
            raise ValueError("JSON 内容必须是对象或数组。")

    def restore_version_as_working_copy(
        self,
        asset_id: str,
        source_version: str,
        *,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAssetRead:
        self._ensure_ready()
        asset = self.repository.get(asset_id)
        if asset is None:
            raise LookupError("Asset not found")

        source = self.repository.get_version(asset_id, source_version)
        if source is None:
            raise LookupError(f"版本 {source_version} 不存在")

        if (
            asset.asset_type == AgentAssetType.RULE.value
            and str((asset.config_json or {}).get("detail_mode") or "").strip().lower()
            == "spreadsheet"
        ):
            metadata = self.spreadsheet_manager.parse_version_markdown(str(source.content or ""))
            if metadata is None:
                raise FileNotFoundError("历史规则表快照不存在，无法恢复。")
            file_path = self.spreadsheet_manager.resolve_storage_path(metadata.storage_key)
            if not file_path.exists():
                raise FileNotFoundError(metadata.file_name)
            restored = self.upload_rule_spreadsheet(
                asset.id,
                filename=metadata.file_name,
                content=file_path.read_bytes(),
                actor=actor,
                request_id=request_id,
                change_note=f"基于历史版本 {source_version} 恢复生成工作稿",
                source="restore",
            )
            self.audit_service.log_action(
                actor=actor,
                action="restore_agent_asset_version",
                resource_type=asset.asset_type,
                resource_id=asset.id,
                before_json={"source_version": source_version},
                after_json={"working_version": restored.working_version},
                request_id=request_id,
            )
            return restored

        next_version = self._increment_version(self._resolve_working_version(asset))
        self.create_version(
            asset.id,
            AgentAssetVersionCreate(
                version=next_version,
                content=self._deserialize_content(source),
                content_type=AgentAssetContentType(source.content_type),
                change_note=f"基于历史版本 {source_version} 恢复生成工作稿",
                created_by=actor,
            ),
            actor=actor,
            request_id=request_id,
        )
        restored = self.get_asset(asset.id)
        self.audit_service.log_action(
            actor=actor,
            action="restore_agent_asset_version",
            resource_type=asset.asset_type,
            resource_id=asset.id,
            before_json={"source_version": source_version},
            after_json={"working_version": next_version},
            request_id=request_id,
        )
        return restored  # type: ignore[return-value]


class AgentAssetService(
    AgentAssetSerializationMixin,
    AgentAssetVersionMixin,
    AgentAssetOnlyOfficeMixin,
    AgentAssetSpreadsheetHelperMixin,
    AgentAssetRiskRuleLevelMixin,
    AgentAssetRiskRulePublishMixin,
    AgentAssetRiskRuleFeedbackMixin,
    AgentAssetRiskRuleTestingMixin,
    AgentAssetRiskRuleSimulationMixin,
    AgentAssetTimelineMixin,
    AgentAssetJsonRuleMixin,
):
    def __init__(
        self,
        db: Session,
        *,
        current_user: CurrentUserContext | None = None,
    ) -> None:
        self.db = db
        self.access_scope = (
            AgentAssetAccessScope.from_user(current_user) if current_user is not None else None
        )
        self.repository = AgentAssetRepository(db, access_scope=self.access_scope)
        self.audit_service = AuditLogService(db)
        self.spreadsheet_manager = AgentAssetSpreadsheetManager()
        self.rule_library_manager = AgentAssetRuleLibraryManager()

    def list_assets(
        self,
        *,
        asset_type: str | None = None,
        status: str | None = None,
        domain: str | None = None,
        keyword: str | None = None,
    ) -> list[AgentAssetListItem]:
        self._ensure_ready()
        if asset_type in {None, "", AgentAssetType.RULE.value}:
            self.sync_rule_assets_from_libraries()
        assets = self.repository.list(
            asset_type=asset_type, status=status, domain=domain, keyword=keyword
        )
        assets = self._filter_excluded_risk_assets(assets)
        version_stats = self._collect_version_stats(assets)
        return [self._serialize_list_item(asset, version_stats.get(asset.id)) for asset in assets]

    def list_assets_page(
        self,
        *,
        asset_type: str | None = None,
        status: str | None = None,
        domain: str | None = None,
        keyword: str | None = None,
        page: int | None,
        page_size: int | None,
    ) -> PageResult[AgentAssetListItem]:
        self._ensure_ready()
        if asset_type in {None, "", AgentAssetType.RULE.value}:
            self.sync_rule_assets_from_libraries()
        assets = self.repository.list(
            asset_type=asset_type,
            status=status,
            domain=domain,
            keyword=keyword,
        )
        assets = self._filter_excluded_risk_assets(assets)
        page_params = normalize_page_params(page, page_size)
        paged_assets = assets[page_params.offset : page_params.offset + page_params.page_size]
        version_stats = self._collect_version_stats(paged_assets)
        return PageResult(
            items=[
                self._serialize_list_item(asset, version_stats.get(asset.id))
                for asset in paged_assets
            ],
            total=len(assets),
            page=page_params.page,
            page_size=page_params.page_size,
        )

    def get_asset(self, asset_id: str) -> AgentAssetRead | None:
        self._ensure_ready()
        asset = self.repository.get(asset_id)
        if asset is None:
            return None
        try:
            can_persist_backfill = self.access_scope is None or self.access_scope.can_write(asset)
            if can_persist_backfill and backfill_missing_risk_rule_score(asset):
                asset = self.repository.save_asset(asset)
        except Exception:
            logger.warning(
                "Failed to backfill risk rule score asset_id=%s", asset_id, exc_info=True
            )

        working_version = self._resolve_working_version(asset)
        recent_versions = self._sort_versions(
            self.repository.list_versions(asset_id, limit=5),
            working_version,
        )
        latest_review = (
            self.repository.get_review(asset_id, working_version)
            if working_version
            else next(iter(self.repository.list_reviews(asset_id, limit=1)), None)
        )
        current_version = (
            self.repository.get_version(asset_id, working_version) if working_version else None
        )
        version_stats = self._collect_version_stats([asset]).get(asset.id)
        return AgentAssetRead(
            **self._serialize_list_item(asset, version_stats).model_dump(),
            current_version_content=self._deserialize_content(current_version)
            if current_version
            else None,
            current_version_content_type=current_version.content_type if current_version else None,
            current_version_change_note=current_version.change_note if current_version else None,
            recent_versions=[self._serialize_version(item, asset) for item in recent_versions],
            latest_review=AgentAssetReviewRead.model_validate(latest_review)
            if latest_review
            else None,
            latest_test_summary=self.get_latest_risk_rule_test_summary(asset)
            if str((asset.config_json or {}).get("detail_mode") or "").strip().lower()
            == "json_risk"
            and working_version
            and asset.status
            not in {AgentAssetStatus.GENERATING.value, AgentAssetStatus.FAILED.value}
            else None,
        )

    @staticmethod
    def _filter_excluded_risk_assets(assets: list[AgentAsset]) -> list[AgentAsset]:
        return [
            asset for asset in assets if not AgentAssetService._is_excluded_budget_risk_asset(asset)
        ]

    @staticmethod
    def _is_excluded_budget_risk_asset(asset: AgentAsset) -> bool:
        if asset.asset_type != AgentAssetType.RULE.value:
            return False
        config_json = asset.config_json if isinstance(asset.config_json, dict) else {}
        if str(config_json.get("detail_mode") or "").strip().lower() != "json_risk":
            return False
        manifest_like = {
            **config_json,
            "rule_code": str(asset.code or "").strip(),
            "name": str(asset.name or "").strip(),
            "description": str(asset.description or "").strip(),
            "metadata": config_json,
        }
        return is_budget_risk_manifest(manifest_like)

    def create_asset(
        self,
        payload: AgentAssetCreate,
        *,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAssetRead:
        self._ensure_ready()
        if self.repository.get_by_code(payload.code):
            raise ValueError(f"资产编码 {payload.code} 已存在")
        if payload.status == AgentAssetStatus.ACTIVE:
            raise ValueError("请先创建资产并完成审核，再通过上线接口激活。")

        if self.access_scope is None:
            tenant_id, resource_scope = platform_resource_identity()
        elif payload.scope == "platform":
            if not self.access_scope.is_platform_admin:
                raise PermissionError("只有平台管理员可以创建平台资产。")
            tenant_id, resource_scope = platform_resource_identity()
        else:
            tenant_id = self.access_scope.tenant_id
            resource_scope = "tenant"

        asset = AgentAsset(
            tenant_id=tenant_id,
            scope=resource_scope,
            asset_type=payload.asset_type.value,
            code=payload.code,
            name=payload.name,
            description=payload.description,
            domain=payload.domain.value,
            scenario_json=payload.scenario_json,
            owner=payload.owner,
            reviewer=payload.reviewer,
            status=payload.status.value,
            config_json=payload.config_json,
        )
        created = self.repository.create_asset(asset)
        self.audit_service.log_action(
            actor=actor,
            action="create_agent_asset",
            resource_type=created.asset_type,
            resource_id=created.id,
            before_json=None,
            after_json=self._asset_snapshot(created),
            request_id=request_id,
        )
        logger.info("Created agent asset id=%s code=%s", created.id, created.code)
        return self.get_asset(created.id)  # type: ignore[return-value]

    def update_asset(
        self,
        asset_id: str,
        payload: AgentAssetUpdate,
        *,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAssetRead:
        self._ensure_ready()
        asset = self.repository.get(asset_id)
        if asset is None:
            raise LookupError("Asset not found")

        before = self._asset_snapshot(asset)

        config_json = asset.config_json if isinstance(asset.config_json, dict) else {}
        is_json_risk = (
            asset.asset_type == AgentAssetType.RULE.value
            and str(config_json.get("detail_mode") or "").strip().lower() == "json_risk"
        )
        if is_json_risk and payload.published_version is not None:
            raise ValueError("JSON 风险规则发布版本只能由 shadow/Canary 发布流程变更。")
        if is_json_risk and payload.config_json is not None:
            raise ValueError("JSON 风险规则运行配置只能由专用规则接口变更。")

        if payload.status == AgentAssetStatus.ACTIVE:
            raise ValueError("请使用上线接口激活资产。")

        for field_name in (
            "name",
            "description",
            "owner",
            "reviewer",
            "current_version",
            "published_version",
            "working_version",
            "config_json",
            "scenario_json",
        ):
            value = getattr(payload, field_name)
            if value is not None:
                setattr(asset, field_name, value)

        if payload.domain is not None:
            asset.domain = payload.domain.value
        if payload.status is not None:
            asset.status = payload.status.value
        if payload.current_version is not None and not self.repository.get_version(
            asset_id, payload.current_version
        ):
            raise LookupError(f"版本 {payload.current_version} 不存在")
        if payload.published_version is not None and not self.repository.get_version(
            asset_id, payload.published_version
        ):
            raise LookupError(f"版本 {payload.published_version} 不存在")
        if payload.working_version is not None and not self.repository.get_version(
            asset_id, payload.working_version
        ):
            raise LookupError(f"版本 {payload.working_version} 不存在")
        if payload.current_version is not None and payload.working_version is None:
            asset.working_version = payload.current_version
        if payload.working_version is not None:
            asset.current_version = payload.working_version

        updated = self.repository.save_asset(asset)
        self.audit_service.log_action(
            actor=actor,
            action="update_agent_asset",
            resource_type=updated.asset_type,
            resource_id=updated.id,
            before_json=before,
            after_json=self._asset_snapshot(updated),
            request_id=request_id,
        )
        logger.info("Updated agent asset id=%s code=%s", updated.id, updated.code)
        return self.get_asset(updated.id)  # type: ignore[return-value]

    def list_versions(self, asset_id: str, *, limit: int = 20) -> list[AgentAssetVersionRead]:
        self._ensure_ready()
        asset = self.repository.get(asset_id)
        if asset is None:
            raise LookupError("Asset not found")
        versions = self._sort_versions(
            self.repository.list_versions(asset_id, limit=limit),
            self._resolve_working_version(asset),
        )
        return [self._serialize_version(item, asset) for item in versions]

    def create_version(
        self,
        asset_id: str,
        payload: AgentAssetVersionCreate,
        *,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAssetVersionRead:
        self._ensure_ready()
        asset = self.repository.get(asset_id)
        if asset is None:
            raise LookupError("Asset not found")
        if self.repository.get_version(asset_id, payload.version):
            raise ValueError(f"版本号 {payload.version} 已存在")

        self._validate_version_payload(asset, payload)
        serialized_content = self._serialize_content(payload.content, payload.content_type.value)
        version = AgentAssetVersion(
            asset_id=asset_id,
            version=payload.version,
            content=serialized_content,
            content_type=payload.content_type.value,
            change_note=payload.change_note,
            created_by=actor,
        )
        created = self.repository.create_version(version)

        before = self._asset_snapshot(asset)
        asset.working_version = payload.version
        asset.current_version = payload.version
        updated_asset = self.repository.save_asset(asset)

        self.audit_service.log_action(
            actor=actor,
            action="save_agent_asset_version",
            resource_type=updated_asset.asset_type,
            resource_id=updated_asset.id,
            before_json=before,
            after_json={
                "current_version": updated_asset.current_version,
                "working_version": updated_asset.working_version,
                "published_version": updated_asset.published_version,
                "status": updated_asset.status,
            },
            request_id=request_id,
        )
        logger.info("Created agent asset version asset_id=%s version=%s", asset_id, payload.version)
        return self._serialize_version(created, updated_asset)

    def create_review(
        self,
        asset_id: str,
        payload: AgentAssetReviewCreate,
        *,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAssetReviewRead:
        self._ensure_ready()
        asset = self.repository.get(asset_id)
        if asset is None:
            raise LookupError("Asset not found")
        if (
            asset.asset_type == AgentAssetType.RULE.value
            and payload.review_status == AgentReviewStatus.PENDING
            and payload.version != self._resolve_working_version(asset)
        ):
            if self.repository.get_version(asset_id, payload.version) is not None:
                raise ValueError(f"版本 {payload.version} 已存在，不能重复送审。")
            asset = self._create_named_working_copy_for_review(
                asset,
                target_version=payload.version,
                actor=actor,
                request_id=request_id,
            )
        if self.repository.get_version(asset_id, payload.version) is None:
            raise LookupError(f"版本 {payload.version} 不存在")
        if asset.asset_type == AgentAssetType.RULE.value:
            if (
                str((asset.config_json or {}).get("detail_mode") or "").strip().lower()
                == "json_risk"
                and payload.review_status == AgentReviewStatus.PENDING
                and not self.get_latest_risk_rule_test_summary(asset).test_passed
            ):
                raise PermissionError("当前规则版本尚未完成测试通过确认，不能提交审核。")
            working_version = self._resolve_working_version(asset)
            if payload.version != working_version:
                raise ValueError("只能对当前工作版本发起审核。")

        review = AgentAssetReview(
            asset_id=asset_id,
            version=payload.version,
            reviewer=actor,
            review_status=payload.review_status.value,
            review_note=payload.review_note,
            reviewed_at=None
            if payload.review_status == AgentReviewStatus.PENDING
            else datetime.now(UTC),
            created_at=datetime.now(UTC),
        )
        created = self.repository.create_review(review)

        before = self._asset_snapshot(asset)
        asset.reviewer = actor
        if payload.review_status == AgentReviewStatus.PENDING:
            if not asset.published_version:
                asset.status = AgentAssetStatus.REVIEW.value
        elif payload.review_status == AgentReviewStatus.REJECTED:
            if not asset.published_version:
                asset.status = AgentAssetStatus.DRAFT.value
        elif not asset.published_version:
            asset.status = AgentAssetStatus.REVIEW.value
        self.repository.save_asset(asset)

        self.audit_service.log_action(
            actor=actor,
            action="review_agent_asset",
            resource_type=asset.asset_type,
            resource_id=asset.id,
            before_json=before,
            after_json={
                "review_version": payload.version,
                "review_status": payload.review_status.value,
                "asset_status": asset.status,
            },
            request_id=request_id,
        )
        logger.info(
            "Created review asset_id=%s version=%s status=%s",
            asset_id,
            payload.version,
            payload.review_status.value,
        )
        return AgentAssetReviewRead.model_validate(created)

    def _create_named_working_copy_for_review(
        self,
        asset: AgentAsset,
        *,
        target_version: str,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAsset:
        working_version = self._resolve_working_version(asset)
        if not working_version:
            raise ValueError("当前规则尚未配置工作版本，无法提交审核。")

        source = self.repository.get_version(asset.id, working_version)
        if source is None:
            raise LookupError(f"版本 {working_version} 不存在")

        is_spreadsheet_rule = (
            asset.asset_type == AgentAssetType.RULE.value
            and str((asset.config_json or {}).get("detail_mode") or "").strip().lower()
            == "spreadsheet"
        )
        if is_spreadsheet_rule:
            _, metadata = self._resolve_spreadsheet_version_meta(asset, version=working_version)
            file_path = self.spreadsheet_manager.resolve_storage_path(metadata.storage_key)
            if not file_path.exists():
                raise FileNotFoundError(metadata.file_name)
            snapshot_meta = self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
                library=self._resolve_spreadsheet_rule_library(asset),
                asset_id=asset.id,
                version=target_version,
                file_name=metadata.file_name,
                content=file_path.read_bytes(),
                actor_name=actor,
                source="review-submit",
            )
            next_content = self.spreadsheet_manager.build_version_markdown(
                rule_name=asset.name,
                version=target_version,
                metadata=snapshot_meta,
            )
            next_content_type = AgentAssetContentType.MARKDOWN
        else:
            next_content = self._deserialize_content(source)
            next_content_type = AgentAssetContentType(source.content_type)

        self.create_version(
            asset.id,
            AgentAssetVersionCreate(
                version=target_version,
                content=next_content,
                content_type=next_content_type,
                change_note=f"提交审核前固化工作稿为 {target_version}",
                created_by=actor,
            ),
            actor=actor,
            request_id=request_id,
        )

        refreshed = self.repository.get(asset.id)
        if refreshed is None:
            raise LookupError("Asset not found")

        if is_spreadsheet_rule:
            config_json = dict(refreshed.config_json or {})
            current_document_meta = self._read_current_rule_document_meta(refreshed)
            if current_document_meta is not None:
                rule_document = self.spreadsheet_manager.build_rule_document_config(
                    current_document_meta,
                    asset_version=target_version,
                )
                rule_document["storage_key"] = current_document_meta.storage_key
                config_json["rule_document"] = rule_document
                refreshed.config_json = config_json
                self.repository.save_asset(refreshed)

        return refreshed

    def activate_asset(
        self,
        asset_id: str,
        *,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAssetRead:
        self._ensure_ready()
        asset = self.repository.get(asset_id)
        if asset is None:
            raise LookupError("Asset not found")
        candidate_version = self._resolve_working_version(asset)
        if not candidate_version:
            raise ValueError("资产尚未设置工作版本，无法上线。")

        if asset.asset_type == AgentAssetType.RULE.value:
            config_json = asset.config_json if isinstance(asset.config_json, dict) else {}
            if str(config_json.get("detail_mode") or "").strip().lower() == "json_risk":
                raise ValueError("JSON 风险规则必须通过 shadow/Canary 发布流程上线。")
            review = self.repository.get_review(
                asset.id, candidate_version, AgentReviewStatus.APPROVED.value
            )
            if review is None:
                raise PermissionError("规则工作版本尚未审核通过，不能上线。")

        before = self._asset_snapshot(asset)
        asset.published_version = candidate_version
        asset.status = AgentAssetStatus.ACTIVE.value
        updated = self.repository.save_asset(asset)
        self.audit_service.log_action(
            actor=actor,
            action="activate_agent_asset",
            resource_type=updated.asset_type,
            resource_id=updated.id,
            before_json=before,
            after_json=self._asset_snapshot(updated),
            request_id=request_id,
        )
        logger.info("Activated agent asset id=%s code=%s", updated.id, updated.code)
        return self.get_asset(updated.id)  # type: ignore[return-value]

    def _ensure_ready(self) -> None:
        AgentFoundationService(self.db).ensure_foundation_ready()

    def sync_platform_risk_rules_from_library(self) -> int:
        manifest_count = AgentFoundationService(self.db).sync_platform_risk_rules_from_library()
        self.db.commit()
        return manifest_count

    def sync_rule_assets_from_libraries(self) -> int:
        foundation = AgentFoundationService(self.db)
        synced_count = foundation.sync_finance_rule_assets_from_catalog()
        synced_count += foundation.sync_platform_risk_rules_from_library()
        self.db.commit()
        return synced_count
