from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

import jwt

from app.api.deps import CurrentUserContext
from app.core.config import get_settings
from app.models.agent_asset import AgentAsset
from app.schemas.agent_asset import AgentAssetOnlyOfficeConfigRead, AgentAssetRead
from app.services.agent_asset_access import stable_user_principal
from app.services.agent_asset_onlyoffice_security import (
    AGENT_ASSET_ONLYOFFICE_CALLBACK_SCOPE,
    AGENT_ASSET_ONLYOFFICE_CONTENT_SCOPE,
    AgentAssetOnlyOfficeSecurityError,
    AgentAssetOnlyOfficeSessionService,
    AgentAssetOnlyOfficeValidatedSession,
)
from app.services.agent_asset_spreadsheet import (
    COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
    FINANCE_RULES_LIBRARY,
    SPREADSHEET_MIME_TYPE,
    AgentAssetSpreadsheetManager,
    RuleSpreadsheetMeta,
)
from app.services.knowledge_onlyoffice_security import download_onlyoffice_document
from app.services.settings import resolve_onlyoffice_settings

PREVIEW_RULE_ASSET_ID = "preview-rule-expense-company-travel-expense"
PREVIEW_RULE_CURRENT_VERSION = "v1.2.0"
PREVIEW_RULE_VERSION_FILENAMES = {
    PREVIEW_RULE_CURRENT_VERSION: COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
    "v1.1.0": "鍏徃宸梾璐规姤閿€瑙勫垯-v1.1.0.xlsx",
    "v1.0.0": "鍏徃宸梾璐规姤閿€瑙勫垯-v1.0.0.xlsx",
}


@dataclass(slots=True)
class OnlyOfficeCallbackPayload:
    status: int
    download_url: str
    document_key: str


class AgentAssetOnlyOfficeMixin:
    @staticmethod
    def _resolve_onlyoffice_settings():
        return resolve_onlyoffice_settings()

    def build_rule_spreadsheet_onlyoffice_config(
        self,
        asset_id: str,
        current_user: CurrentUserContext,
        *,
        version: str | None = None,
    ) -> AgentAssetOnlyOfficeConfigRead:
        self._ensure_ready()
        if asset_id == PREVIEW_RULE_ASSET_ID:
            resolved_version, metadata = self._ensure_preview_rule_spreadsheet(version=version)
            return self._build_onlyoffice_spreadsheet_config(
                asset_id=asset_id,
                tenant_id="platform",
                resource_scope="platform",
                document_version=resolved_version,
                current_user=current_user,
                metadata=metadata,
                editable=(
                    resolved_version == PREVIEW_RULE_CURRENT_VERSION
                    and current_user.is_admin
                ),
            )

        asset = self._require_spreadsheet_rule(asset_id)
        resolved_version, metadata = self._resolve_current_spreadsheet_meta(asset)
        editable = self._can_edit_current_spreadsheet(current_user) and (
            asset.scope == "tenant" or current_user.is_admin
        )
        return self._build_onlyoffice_spreadsheet_config(
            asset_id=asset.id,
            tenant_id=asset.tenant_id,
            resource_scope=asset.scope,
            document_version=resolved_version,
            current_user=current_user,
            metadata=metadata,
            editable=editable,
        )

    def get_rule_spreadsheet_content(
        self,
        asset_id: str,
        *,
        version: str | None = None,
        validated_session: AgentAssetOnlyOfficeValidatedSession | None = None,
    ) -> tuple[Path, str, str]:
        self._ensure_ready()
        if asset_id == PREVIEW_RULE_ASSET_ID:
            resolved_version, metadata = self._ensure_preview_rule_spreadsheet(
                version=(validated_session.document_version if validated_session else version)
            )
            if validated_session is not None:
                self._require_matching_onlyoffice_document(
                    validated_session,
                    tenant_id="platform",
                    resource_scope="platform",
                    document_version=resolved_version,
                    metadata=metadata,
                )
            file_path = self.spreadsheet_manager.resolve_storage_path(metadata.storage_key)
            if not file_path.exists():
                raise FileNotFoundError(metadata.file_name)
            return file_path, metadata.mime_type, metadata.file_name

        asset = self._require_spreadsheet_rule(asset_id)
        requested_version = str(version or "").strip()
        if requested_version and requested_version != "current":
            resolved_version, metadata = self._resolve_spreadsheet_version_meta(
                asset, version=requested_version
            )
        else:
            resolved_version, metadata = self._resolve_current_spreadsheet_meta(asset)
        if validated_session is not None:
            self._require_matching_onlyoffice_document(
                validated_session,
                tenant_id=asset.tenant_id,
                resource_scope=asset.scope,
                document_version=resolved_version,
                metadata=metadata,
            )
        file_path = self.spreadsheet_manager.resolve_storage_path(metadata.storage_key)
        if not file_path.exists():
            raise FileNotFoundError(metadata.file_name)
        return file_path, metadata.mime_type, metadata.file_name

    def validate_rule_spreadsheet_access_token(
        self,
        asset_id: str,
        access_token: str,
        *,
        expected_scope: str = AGENT_ASSET_ONLYOFFICE_CONTENT_SCOPE,
    ) -> AgentAssetOnlyOfficeValidatedSession:
        return self._onlyoffice_session_service().validate(
            asset_id=asset_id,
            token=access_token,
            expected_scope=expected_scope,
        )

    def upload_rule_spreadsheet(
        self,
        asset_id: str,
        *,
        filename: str,
        content: bytes,
        actor: str,
        request_id: str | None = None,
        change_note: str | None = None,
        source: str = "upload",
    ) -> AgentAssetRead:
        self._ensure_ready()
        asset = self._require_spreadsheet_rule(asset_id)
        normalized_name = Path(str(filename or "").strip()).name.strip()
        if not normalized_name:
            raise ValueError("规则表文件名不能为空。")
        if Path(normalized_name).suffix.lower() != ".xlsx":
            raise ValueError("当前仅支持上传 .xlsx 格式的规则表。")
        if not content:
            raise ValueError("规则表文件内容不能为空。")

        _, current_metadata = self._resolve_current_spreadsheet_meta(asset)
        file_name = current_metadata.file_name or self._resolve_default_spreadsheet_file_name(asset)
        sheet_changes, cell_changes = self._collect_workbook_changes_from_content(
            current_metadata,
            content,
        )
        changed_sheet_count = self._count_changed_sheets(sheet_changes, cell_changes)
        changed_cell_count = len(cell_changes)

        metadata = self._store_current_rule_spreadsheet(
            asset,
            file_name=file_name,
            content=content,
            actor=actor,
            source=source,
        )
        summary = self._build_spreadsheet_change_summary(
            sheet_changes,
            cell_changes,
        )
        self.audit_service.log_action(
            actor=actor,
            action="edit_rule_spreadsheet",
            resource_type=asset.asset_type,
            resource_id=asset.id,
            before_json={"storage_key": current_metadata.storage_key},
            after_json={
                "summary": summary,
                "changed_sheet_count": changed_sheet_count,
                "changed_cell_count": changed_cell_count,
                "sheet_changes": [item.model_dump() for item in sheet_changes],
                "cell_changes": [item.model_dump() for item in cell_changes[:500]],
                "storage_key": metadata.storage_key,
            },
            request_id=request_id,
        )
        return self.get_asset(asset.id)  # type: ignore[return-value]

    def import_rule_spreadsheet_content(
        self,
        asset_id: str,
        *,
        filename: str,
        content: bytes,
        actor: str,
        request_id: str | None = None,
    ) -> AgentAssetRead:
        self._ensure_ready()
        asset = self._require_spreadsheet_rule(asset_id)
        normalized_name = Path(str(filename or "").strip()).name.strip()
        if not normalized_name:
            raise ValueError("待导入表格文件名不能为空。")
        if Path(normalized_name).suffix.lower() != ".xlsx":
            raise ValueError("当前仅支持导入 .xlsx 格式的规则表。")

        _, current_metadata = self._resolve_current_spreadsheet_meta(asset)
        imported_content = self.spreadsheet_manager.rebuild_from_uploaded_content(content)
        return self.upload_rule_spreadsheet(
            asset.id,
            filename=current_metadata.file_name,
            content=imported_content,
            actor=actor,
            request_id=request_id,
            change_note=f"导入 Excel 表格内容：{normalized_name}",
            source="content-import",
        )

    def handle_rule_spreadsheet_onlyoffice_callback(
        self,
        asset_id: str,
        *,
        version: str | None = None,
        payload: dict[str, Any],
        callback_token: str,
    ) -> None:
        self._ensure_ready()
        callback = self._parse_onlyoffice_callback(payload)
        session_service = self._onlyoffice_session_service()
        if callback.status not in {2, 6}:
            session_service.validate(
                asset_id=asset_id,
                token=callback_token,
                expected_scope=AGENT_ASSET_ONLYOFFICE_CALLBACK_SCOPE,
            )
            return
        if not callback.download_url:
            raise ValueError("ONLYOFFICE 回写回调缺少下载 URL。")

        claimed = session_service.claim_callback(
            asset_id=asset_id,
            token=callback_token,
            payload_document_key=callback.document_key,
        )
        if version and str(version).strip() not in {"current", claimed.document_version}:
            session_service.finish_callback(
                claimed.jti,
                succeeded=False,
                failure_reason="legacy_version_mismatch",
            )
            raise AgentAssetOnlyOfficeSecurityError(
                "ONLYOFFICE 回调版本与编辑会话不一致。"
            )
        try:
            if asset_id == PREVIEW_RULE_ASSET_ID:
                self._save_preview_rule_spreadsheet_callback(
                    claimed=claimed,
                    download_url=callback.download_url,
                )
            else:
                self._save_current_rule_spreadsheet_callback(
                    claimed=claimed,
                    download_url=callback.download_url,
                )
        except Exception as exc:
            session_service.finish_callback(
                claimed.jti,
                succeeded=False,
                failure_reason=type(exc).__name__,
            )
            raise
        session_service.finish_callback(claimed.jti, succeeded=True)


    @staticmethod
    def _can_edit_current_spreadsheet(current_user: CurrentUserContext) -> bool:
        role_codes = {str(item).strip() for item in current_user.role_codes}
        return current_user.is_admin or "manager" in role_codes or "finance" in role_codes

    @staticmethod
    def _build_onlyoffice_document_key(
        asset_id: str,
        metadata: RuleSpreadsheetMeta,
    ) -> str:
        fingerprint = metadata.checksum or metadata.updated_at or metadata.file_name
        raw_key = f"{asset_id}-{fingerprint}"
        return "".join(
            character if character.isalnum() or character in {"-", "_", ".", "="} else "_"
            for character in raw_key
        )

    @staticmethod
    def _parse_onlyoffice_callback(payload: dict[str, Any]) -> OnlyOfficeCallbackPayload:
        return OnlyOfficeCallbackPayload(
            status=int(payload.get("status") or 0),
            download_url=str(payload.get("url") or "").strip(),
            document_key=str(payload.get("key") or "").strip(),
        )

    def _build_onlyoffice_spreadsheet_config(
        self,
        *,
        asset_id: str,
        tenant_id: str,
        resource_scope: str,
        document_version: str,
        current_user: CurrentUserContext,
        metadata: RuleSpreadsheetMeta,
        editable: bool,
    ) -> AgentAssetOnlyOfficeConfigRead:
        onlyoffice_settings = self._resolve_onlyoffice_settings()
        settings = get_settings()
        if not onlyoffice_settings.enabled:
            raise ValueError("ONLYOFFICE 预览未启用。")
        if not onlyoffice_settings.public_url or not onlyoffice_settings.backend_url:
            raise ValueError("ONLYOFFICE 地址配置不完整。")
        if not onlyoffice_settings.jwt_secret:
            raise ValueError("ONLYOFFICE JWT 密钥未配置。")

        backend_base_url = onlyoffice_settings.backend_url.rstrip("/")
        public_url = onlyoffice_settings.public_url.rstrip("/")
        actor = stable_user_principal(current_user)
        document_key = self._build_onlyoffice_document_key(asset_id, metadata)
        tokens = self._onlyoffice_session_service().issue(
            tenant_id=tenant_id,
            resource_scope=resource_scope,
            asset_id=asset_id,
            document_key=document_key,
            document_version=document_version,
            document_fingerprint=self._onlyoffice_document_fingerprint(metadata),
            actor=actor,
            writable=editable,
        )
        document_url = (
            f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/content"
            f"?access_token={tokens.content_token}"
        )
        callback_url = (
            f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/callback"
            f"?access_token={tokens.callback_token}"
        )

        config: dict[str, Any] = {
            "documentType": "cell",
            "document": {
                "fileType": Path(metadata.file_name).suffix.lstrip(".").lower() or "xlsx",
                "key": document_key,
                "title": metadata.file_name,
                "url": document_url,
                "permissions": {
                    "download": True,
                    "edit": editable,
                    "print": True,
                    "copy": True,
                },
            },
            "editorConfig": {
                "mode": "edit" if editable else "view",
                "lang": "zh-CN",
                "callbackUrl": callback_url,
                "user": {
                    "id": current_user.username,
                    "name": current_user.name,
                },
                "customization": {
                    "compactHeader": True,
                    "compactToolbar": False,
                    "toolbarNoTabs": False,
                    "autosave": False,
                    "forcesave": editable,
                },
            },
            "width": "100%",
            "height": "100%",
        }
        config["token"] = jwt.encode(config, onlyoffice_settings.jwt_secret, algorithm="HS256")
        return AgentAssetOnlyOfficeConfigRead(documentServerUrl=public_url, config=config)

    def _ensure_preview_rule_spreadsheet(
        self,
        *,
        version: str | None = None,
    ) -> tuple[str, RuleSpreadsheetMeta]:
        resolved_version = str(version or PREVIEW_RULE_CURRENT_VERSION).strip()
        if resolved_version not in PREVIEW_RULE_VERSION_FILENAMES:
            raise LookupError(f"版本 {resolved_version} 不存在")

        file_name = PREVIEW_RULE_VERSION_FILENAMES[resolved_version]
        storage_key = (
            Path("rules")
            / FINANCE_RULES_LIBRARY
            / ".versions"
            / PREVIEW_RULE_ASSET_ID
            / resolved_version
            / file_name
        ).as_posix()
        try:
            file_path = self.spreadsheet_manager.resolve_storage_path(storage_key)
        except FileNotFoundError:
            file_path = None

        if file_path is not None and file_path.exists():
            content = file_path.read_bytes()
            updated_at = datetime.fromtimestamp(file_path.stat().st_mtime, UTC).isoformat()
            return resolved_version, RuleSpreadsheetMeta(
                file_name=file_name,
                storage_key=storage_key,
                mime_type=SPREADSHEET_MIME_TYPE,
                size_bytes=file_path.stat().st_size,
                checksum=self._hash_bytes(content),
                updated_at=updated_at,
                updated_by="ONLYOFFICE 预览",
                source="preview",
            )

        metadata = self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
            library=FINANCE_RULES_LIBRARY,
            asset_id=PREVIEW_RULE_ASSET_ID,
            version=resolved_version,
            file_name=file_name,
            content=AgentAssetSpreadsheetManager.build_company_travel_rule_template(),
            actor_name="ONLYOFFICE 预览",
            source="preview",
        )
        return resolved_version, metadata

    def _save_current_rule_spreadsheet_callback(
        self,
        *,
        claimed: AgentAssetOnlyOfficeValidatedSession,
        download_url: str,
    ) -> None:
        asset = self._require_spreadsheet_rule(claimed.asset_id)
        resolved_version, metadata = self._resolve_current_spreadsheet_meta(asset)
        self._require_matching_onlyoffice_document(
            claimed,
            tenant_id=asset.tenant_id,
            resource_scope=asset.scope,
            document_version=resolved_version,
            metadata=metadata,
        )
        content = download_onlyoffice_document(
            download_url,
            expected_filename=metadata.file_name,
        )
        if metadata.checksum and metadata.checksum == self._hash_bytes(content):
            return
        self.upload_rule_spreadsheet(
            asset.id,
            filename=metadata.file_name,
            content=content,
            actor=claimed.actor,
            source="onlyoffice",
        )

    def _save_preview_rule_spreadsheet_callback(
        self,
        *,
        claimed: AgentAssetOnlyOfficeValidatedSession,
        download_url: str,
    ) -> None:
        resolved_version, metadata = self._ensure_preview_rule_spreadsheet(
            version=claimed.document_version
        )
        self._require_matching_onlyoffice_document(
            claimed,
            tenant_id="platform",
            resource_scope="platform",
            document_version=resolved_version,
            metadata=metadata,
        )
        content = download_onlyoffice_document(
            download_url,
            expected_filename=metadata.file_name,
        )
        if metadata.checksum and metadata.checksum == self._hash_bytes(content):
            return
        self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
            library=FINANCE_RULES_LIBRARY,
            asset_id=PREVIEW_RULE_ASSET_ID,
            version=resolved_version,
            file_name=metadata.file_name,
            content=content,
            actor_name=claimed.actor,
            source="onlyoffice-preview",
        )

    def _onlyoffice_session_service(self) -> AgentAssetOnlyOfficeSessionService:
        settings = self._resolve_onlyoffice_settings()
        return AgentAssetOnlyOfficeSessionService(
            self.db,
            jwt_secret=settings.jwt_secret,
        )

    @staticmethod
    def _onlyoffice_document_fingerprint(metadata: RuleSpreadsheetMeta) -> str:
        return str(metadata.checksum or metadata.updated_at or metadata.file_name)

    def _require_matching_onlyoffice_document(
        self,
        session: AgentAssetOnlyOfficeValidatedSession,
        *,
        tenant_id: str,
        resource_scope: str,
        document_version: str,
        metadata: RuleSpreadsheetMeta,
    ) -> None:
        comparisons = (
            session.tenant_id == tenant_id,
            session.resource_scope == resource_scope,
            session.document_version == document_version,
            session.document_key
            == self._build_onlyoffice_document_key(session.asset_id, metadata),
            session.document_fingerprint
            == self._onlyoffice_document_fingerprint(metadata),
        )
        if not all(comparisons):
            raise AgentAssetOnlyOfficeSecurityError(
                "ONLYOFFICE 编辑会话绑定的规则表版本已失效。"
            )

    @staticmethod
    def _read_current_rule_document_meta(asset: AgentAsset) -> RuleSpreadsheetMeta | None:
        payload = (asset.config_json or {}).get("rule_document")
        if not isinstance(payload, dict):
            return None

        return RuleSpreadsheetMeta(
            file_name=str(payload.get("file_name") or "").strip(),
            storage_key=str(payload.get("storage_key") or "").strip(),
            mime_type=(
                str(payload.get("mime_type") or "").strip()
                or "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            ),
            size_bytes=int(payload.get("size_bytes") or 0),
            checksum=str(payload.get("checksum") or "").strip(),
            updated_at=str(payload.get("updated_at") or "").strip(),
            updated_by=str(payload.get("updated_by") or "system").strip() or "system",
            source=str(payload.get("source") or "upload").strip() or "upload",
        )
