from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Any
from uuid import uuid4

import jwt
from sqlalchemy import update
from sqlalchemy.orm import Session

from app.models.agent_asset import AgentAssetOnlyOfficeSession

AGENT_ASSET_ONLYOFFICE_ISSUER = "x-financial"
AGENT_ASSET_ONLYOFFICE_AUDIENCE = "onlyoffice-document-server"
AGENT_ASSET_ONLYOFFICE_CONTENT_SCOPE = "agent-asset-spreadsheet-content"
AGENT_ASSET_ONLYOFFICE_CALLBACK_SCOPE = "agent-asset-spreadsheet-callback"
AGENT_ASSET_ONLYOFFICE_CONTENT_TTL_SECONDS = 900
AGENT_ASSET_ONLYOFFICE_CALLBACK_TTL_SECONDS = 4 * 60 * 60


class AgentAssetOnlyOfficeSecurityError(ValueError):
    pass


class AgentAssetOnlyOfficeReplayError(AgentAssetOnlyOfficeSecurityError):
    pass


@dataclass(frozen=True, slots=True)
class AgentAssetOnlyOfficeTokens:
    content_token: str
    callback_token: str
    expires_at: datetime


@dataclass(frozen=True, slots=True)
class AgentAssetOnlyOfficeValidatedSession:
    jti: str
    tenant_id: str
    resource_scope: str
    asset_id: str
    document_key: str
    document_version: str
    document_fingerprint: str
    writable: bool
    actor: str
    status: str


class AgentAssetOnlyOfficeSessionService:
    def __init__(self, db: Session, *, jwt_secret: str) -> None:
        self.db = db
        self.jwt_secret = str(jwt_secret or "").strip()
        if not self.jwt_secret:
            raise AgentAssetOnlyOfficeSecurityError("ONLYOFFICE JWT 密钥未配置。")

    def issue(
        self,
        *,
        tenant_id: str,
        resource_scope: str,
        asset_id: str,
        document_key: str,
        document_version: str,
        document_fingerprint: str,
        writable: bool,
        actor: str,
    ) -> AgentAssetOnlyOfficeTokens:
        normalized_tenant_id = str(tenant_id or "").strip()
        normalized_scope = str(resource_scope or "").strip()
        if not normalized_tenant_id or normalized_scope not in {"platform", "tenant"}:
            raise AgentAssetOnlyOfficeSecurityError("ONLYOFFICE 会话租户范围不合法。")
        if (normalized_scope == "platform") != (normalized_tenant_id == "platform"):
            raise AgentAssetOnlyOfficeSecurityError("ONLYOFFICE 会话租户范围不一致。")

        now = datetime.now(UTC)
        content_expires_at = now + timedelta(
            seconds=AGENT_ASSET_ONLYOFFICE_CONTENT_TTL_SECONDS
        )
        expires_at = now + timedelta(
            seconds=AGENT_ASSET_ONLYOFFICE_CALLBACK_TTL_SECONDS
        )
        jti = str(uuid4())
        row = AgentAssetOnlyOfficeSession(
            jti=jti,
            tenant_id=normalized_tenant_id,
            resource_scope=normalized_scope,
            asset_id=str(asset_id),
            document_key=str(document_key),
            document_version=str(document_version),
            document_fingerprint=str(document_fingerprint),
            audience=AGENT_ASSET_ONLYOFFICE_AUDIENCE,
            writable=bool(writable),
            status="active",
            actor=str(actor),
            expires_at=expires_at,
            failure_reason="",
        )
        self.db.add(row)
        self.db.commit()

        common_claims = {
            "iss": AGENT_ASSET_ONLYOFFICE_ISSUER,
            "aud": AGENT_ASSET_ONLYOFFICE_AUDIENCE,
            "sub": str(asset_id),
            "jti": jti,
            "iat": int(now.timestamp()),
            "nbf": int(now.timestamp()),
            "tenant_id": normalized_tenant_id,
            "resource_scope": normalized_scope,
            "asset_id": str(asset_id),
            "document_key": str(document_key),
            "document_version": str(document_version),
            "document_fingerprint": str(document_fingerprint),
            "writable": bool(writable),
            "actor": str(actor),
        }
        return AgentAssetOnlyOfficeTokens(
            content_token=self._encode(
                {
                    **common_claims,
                    "scope": AGENT_ASSET_ONLYOFFICE_CONTENT_SCOPE,
                    "exp": int(content_expires_at.timestamp()),
                }
            ),
            callback_token=self._encode(
                {
                    **common_claims,
                    "scope": AGENT_ASSET_ONLYOFFICE_CALLBACK_SCOPE,
                    "exp": int(expires_at.timestamp()),
                }
            ),
            expires_at=expires_at,
        )

    def validate(
        self,
        *,
        asset_id: str,
        token: str,
        expected_scope: str,
    ) -> AgentAssetOnlyOfficeValidatedSession:
        try:
            claims = jwt.decode(
                token,
                self.jwt_secret,
                algorithms=["HS256"],
                audience=AGENT_ASSET_ONLYOFFICE_AUDIENCE,
                issuer=AGENT_ASSET_ONLYOFFICE_ISSUER,
                options={
                    "require": [
                        "iss",
                        "aud",
                        "sub",
                        "jti",
                        "iat",
                        "nbf",
                        "exp",
                        "scope",
                        "tenant_id",
                        "resource_scope",
                        "asset_id",
                        "document_key",
                        "document_version",
                        "document_fingerprint",
                        "writable",
                        "actor",
                    ]
                },
            )
        except jwt.PyJWTError as exc:
            raise AgentAssetOnlyOfficeSecurityError(
                "ONLYOFFICE 会话令牌无效或已过期。"
            ) from exc

        jti = str(claims.get("jti") or "").strip()
        row = self.db.get(AgentAssetOnlyOfficeSession, jti)
        if row is None:
            raise AgentAssetOnlyOfficeSecurityError("ONLYOFFICE 会话不存在。")
        now = datetime.now(UTC)
        comparisons = (
            claims.get("scope") == expected_scope,
            claims.get("sub") == asset_id,
            claims.get("asset_id") == asset_id == row.asset_id,
            claims.get("tenant_id") == row.tenant_id,
            claims.get("resource_scope") == row.resource_scope,
            claims.get("document_key") == row.document_key,
            claims.get("document_version") == row.document_version,
            claims.get("document_fingerprint") == row.document_fingerprint,
            bool(claims.get("writable")) == row.writable,
            claims.get("actor") == row.actor,
            row.audience == AGENT_ASSET_ONLYOFFICE_AUDIENCE,
            row.status == "active",
            _as_utc(row.expires_at) > now,
        )
        if not all(comparisons):
            if row.status != "active":
                raise AgentAssetOnlyOfficeReplayError(
                    "ONLYOFFICE 回调会话已被使用或已撤销。"
                )
            raise AgentAssetOnlyOfficeSecurityError(
                "ONLYOFFICE 会话与目标规则表不匹配。"
            )
        return self._validated(row)

    def claim_callback(
        self,
        *,
        asset_id: str,
        token: str,
        payload_document_key: str,
    ) -> AgentAssetOnlyOfficeValidatedSession:
        validated = self.validate(
            asset_id=asset_id,
            token=token,
            expected_scope=AGENT_ASSET_ONLYOFFICE_CALLBACK_SCOPE,
        )
        if not validated.writable:
            raise AgentAssetOnlyOfficeSecurityError("只读 ONLYOFFICE 会话禁止回写规则表。")
        if not payload_document_key or payload_document_key != validated.document_key:
            raise AgentAssetOnlyOfficeSecurityError(
                "ONLYOFFICE 回调文档 key 与编辑会话不一致。"
            )

        now = datetime.now(UTC)
        result = self.db.execute(
            update(AgentAssetOnlyOfficeSession)
            .where(
                AgentAssetOnlyOfficeSession.jti == validated.jti,
                AgentAssetOnlyOfficeSession.status == "active",
                AgentAssetOnlyOfficeSession.expires_at > now,
            )
            .values(status="processing", claimed_at=now)
        )
        if result.rowcount != 1:
            self.db.rollback()
            raise AgentAssetOnlyOfficeReplayError(
                "ONLYOFFICE 回调会话已被使用或已过期。"
            )
        self.db.commit()
        row = self.db.get(AgentAssetOnlyOfficeSession, validated.jti)
        if row is None:  # pragma: no cover - 数据库约束保证
            raise AgentAssetOnlyOfficeSecurityError("ONLYOFFICE 会话不存在。")
        return self._validated(row)

    def finish_callback(
        self,
        jti: str,
        *,
        succeeded: bool,
        failure_reason: str = "",
    ) -> None:
        row = self.db.get(AgentAssetOnlyOfficeSession, jti)
        if row is None or row.status != "processing":
            raise AgentAssetOnlyOfficeReplayError("ONLYOFFICE 回调会话状态不可更新。")
        if succeeded:
            row.status = "consumed"
            row.consumed_at = datetime.now(UTC)
            row.failure_reason = ""
        else:
            row.status = "failed"
            row.failure_reason = str(failure_reason or "callback_failed")[:1000]
        self.db.commit()

    def _encode(self, claims: dict[str, Any]) -> str:
        return jwt.encode(claims, self.jwt_secret, algorithm="HS256")

    @staticmethod
    def _validated(row: AgentAssetOnlyOfficeSession) -> AgentAssetOnlyOfficeValidatedSession:
        return AgentAssetOnlyOfficeValidatedSession(
            jti=row.jti,
            tenant_id=row.tenant_id,
            resource_scope=row.resource_scope,
            asset_id=row.asset_id,
            document_key=row.document_key,
            document_version=row.document_version,
            document_fingerprint=row.document_fingerprint,
            writable=row.writable,
            actor=row.actor,
            status=row.status,
        )


def _as_utc(value: datetime) -> datetime:
    if value.tzinfo is None:
        return value.replace(tzinfo=UTC)
    return value.astimezone(UTC)
