from __future__ import annotations

import base64
from datetime import datetime, timedelta, timezone
import hashlib
from html import escape
import json
import logging
import os
import secrets
from typing import Any, Dict, Optional
from urllib.parse import urlparse
from uuid import uuid4

import jwt
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, Security
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession

from ... import settings
from ...auth import get_current_account
from ...auth_core import get_current_identity
from ...auth_security import require_account
from ...db import get_session
from ...models import User
from ...services.roles import require_admin, require_viewer
from ...services.secret_storage import get_saas_secret_backend
from ...services.hubspot_oauth import (
    HubSpotOAuthError,
    build_hubspot_auth_url,
    disconnect_hubspot,
    exchange_code_for_tokens,
    fetch_token_identity,
    get_hubspot_metadata,
    get_hubspot_secret,
    get_hubspot_status,
    refresh_if_needed,
    sanitize_hubspot_error_text,
    store_hubspot_tokens,
)
from ...services import hubspot_write_control as write_control
from ...api.progress import get_redis

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v2/integrations/hubspot", tags=["hubspot"])

OAUTH_NONCE_TTL_SECONDS = 600
CLI_SESSION_TTL_SECONDS = 300


class HubSpotWriteIntent(BaseModel):
    method: str = Field(default="POST")
    path: str = Field(default="/")
    query: Dict[str, Any] = Field(default_factory=dict)
    body: Any = Field(default_factory=dict)


class HubSpotWriteGateIssueRequest(BaseModel):
    action_id: str
    command_id: Optional[str] = None
    intent: Optional[HubSpotWriteIntent] = None
    intent_fingerprint: Optional[str] = None


class HubSpotProxyJsonRequest(BaseModel):
    gate_token: str
    action_id: str
    method: str
    path: str
    query: Dict[str, Any] = Field(default_factory=dict)
    body: Any = Field(default_factory=dict)


_AGENT_LICENSE_PAID_TIERS = frozenset(
    {
        "agent_paid",
        "paid",
        "pro",
        "scale",
        "unleashed",
        "enterprise",
        "full_stack",
        "workspace_ops",
        "hubspot_admin",
    }
)
_AGENT_LICENSE_TRIAL_TIERS = frozenset({"agent_trial", "trial"})
_OPTIONAL_BEARER = HTTPBearer(auto_error=False)


def _parse_optional_iso8601_utc(value: Any) -> datetime | None:
    text = str(value or "").strip()
    if not text:
        return None
    try:
        parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
    except ValueError:
        return None
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _license_identity_write_entitled(identity: Optional[dict[str, Any]]) -> bool:
    if not isinstance(identity, dict):
        return False
    license_tier = str(identity.get("license_tier") or "").strip().lower()
    if not license_tier:
        return False
    if license_tier in _AGENT_LICENSE_PAID_TIERS:
        return True
    if license_tier in _AGENT_LICENSE_TRIAL_TIERS:
        trial_ends_at = _parse_optional_iso8601_utc(identity.get("license_trial_ends_at"))
        if trial_ends_at is None:
            return True
        return trial_ends_at > datetime.now(timezone.utc)
    return False


async def _optional_identity_from_bearer(
    credentials: Optional[HTTPAuthorizationCredentials] = Security(_OPTIONAL_BEARER),
) -> Optional[dict[str, Any]]:
    if credentials is None:
        return None
    try:
        return await get_current_identity(credentials)
    except HTTPException:
        return None


async def _resolve_hubspot_write_context(
    db: AsyncSession,
    account_id: str,
) -> dict[str, Any]:
    from .gremlin import get_gremlin_tier

    tier = "unknown"
    try:
        tier = str(await get_gremlin_tier(account_id, db) or "unknown").strip().lower()
    except Exception:
        logger.exception("Failed to resolve Gremlin tier for HubSpot write gate account=%s", account_id)
        raise HTTPException(
            status_code=503,
            detail={
                "error": "TENANT_RATE_LIMITED",
                "message": "Write control is temporarily unavailable. Retry shortly.",
                "next_step": "RETRY_LATER",
            },
        )

    status_payload = await get_hubspot_status(db, account_id)
    if status_payload.get("connected") and status_payload.get("needs_refresh"):
        try:
            await refresh_if_needed(db, account_id=account_id, force=False, created_by="system")
            await db.commit()
            status_payload = await get_hubspot_status(db, account_id)
        except HubSpotOAuthError:
            await db.rollback()
            status_payload = {"connected": False, "status": "disconnected"}
        except Exception:
            await db.rollback()
            logger.exception("Failed to refresh HubSpot token during write gate context check")
            raise HTTPException(
                status_code=503,
                detail={
                    "error": "TENANT_RATE_LIMITED",
                    "message": "Write control is temporarily unavailable. Retry shortly.",
                    "next_step": "RETRY_LATER",
                },
            )

    provider_tenant_id = str(status_payload.get("portal_id") or "").strip()
    return {
        "tier": tier,
        "entitled": tier not in {"free", "local_free"},
        "connected": bool(status_payload.get("connected")),
        "provider_tenant_id": provider_tenant_id,
    }


def _write_control_http_exception(
    exc: write_control.WriteControlDenied,
    *,
    correlation_id: str | None = None,
) -> HTTPException:
    detail = exc.to_detail()["detail"]
    if correlation_id:
        detail["correlation_id"] = correlation_id
    return HTTPException(status_code=exc.status_code, detail=detail)


def _write_control_upgrade_contract(request: Request) -> write_control.UpgradeContract:
    cli_version = write_control.parse_cli_version(
        cli_version_header=request.headers.get("X-Gremlin-CLI-Version"),
        user_agent=request.headers.get("User-Agent"),
    )
    return write_control.build_upgrade_contract(cli_version)


def _upgrade_required_exception(contract: write_control.UpgradeContract) -> HTTPException:
    denied = write_control.WriteControlDenied(
        status_code=426,
        error="UPGRADE_REQUIRED",
        message=contract.upgrade_message or "Upgrade required for HubSpot writes.",
        next_step="UPGRADE",
        upgrade_url=contract.upgrade_url,
    )
    detail = denied.to_detail()["detail"]
    detail.update(contract.as_dict())
    return HTTPException(status_code=426, detail=detail)


def _attach_upgrade_contract(payload: dict[str, Any], contract: write_control.UpgradeContract) -> dict[str, Any]:
    payload.update(contract.as_dict())
    return payload


def _state_secret() -> str:
    secret = settings.ENCRYPTION_KEY or settings.JWT_SECRET_KEY
    if secret:
        return secret
    env = (settings.ENV or "").strip().lower()
    is_dev_like = env in {"dev", "development", "test"} or os.getenv("FM_TEST_MODE") == "1"
    if is_dev_like:
        logger.warning("HubSpot OAuth state secret missing; using dev fallback in %s environment", env or "unknown")
        return "dev-secret"
    raise RuntimeError("HubSpot OAuth state secret is not configured.")


def _pkce_verifier() -> str:
    # RFC 7636: high-entropy verifier in [43, 128] chars from unreserved set.
    return secrets.token_urlsafe(64)


def _pkce_challenge(verifier: str) -> str:
    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")


def _encode_state(nonce: str, flow: str) -> str:
    payload = {
        "nonce": nonce,
        "flow": flow,
        "exp": int((datetime.now(timezone.utc) + timedelta(seconds=OAUTH_NONCE_TTL_SECONDS)).timestamp()),
    }
    return jwt.encode(payload, _state_secret(), algorithm="HS256")


def _decode_state(state_token: str) -> Dict[str, Any]:
    return jwt.decode(state_token, _state_secret(), algorithms=["HS256"])


async def _redis_get_json(redis, key: str) -> Optional[Dict[str, Any]]:
    if redis is None:
        return None
    raw = await redis.get(key)
    if not raw:
        return None
    if isinstance(raw, bytes):
        raw = raw.decode("utf-8", errors="ignore")
    try:
        parsed = json.loads(raw)
    except json.JSONDecodeError:
        return None
    return parsed if isinstance(parsed, dict) else None


async def _set_cli_session_status(
    redis,
    *,
    session_id: Optional[str],
    status: str,
    message: Optional[str] = None,
    workspace_id: Optional[str] = None,
    account_id: Optional[str] = None,
) -> None:
    if not session_id:
        return
    payload: Dict[str, Any] = {"status": status}
    if message:
        payload["message"] = message
    if workspace_id:
        payload["workspace_id"] = workspace_id
    if account_id:
        payload["account_id"] = account_id
    await redis.setex(
        f"hubspot_cli_session:{session_id}",
        CLI_SESSION_TTL_SECONDS,
        json.dumps(payload),
    )


async def _get_optional_redis():
    try:
        return await get_redis()
    except HTTPException:
        return None


def _error_html(message: str) -> HTMLResponse:
    safe_message = escape(message or "Unexpected HubSpot OAuth failure.")
    html = f"""
    <html>
      <head><title>HubSpot OAuth Failed</title></head>
      <body style="font-family:Arial, sans-serif; background:#f6f7fb; color:#111;">
        <div style="max-width:560px;margin:10vh auto;background:#fff;padding:28px;border-radius:12px;">
          <h2 style="margin:0 0 8px;color:#b00020;">HubSpot OAuth Failed</h2>
          <p style="line-height:1.5;">{safe_message}</p>
          <p>You can close this window.</p>
        </div>
      </body>
    </html>
    """
    return HTMLResponse(content=html, status_code=200)


def _origin_from_url(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme and parsed.netloc:
        return f"{parsed.scheme}://{parsed.netloc}"
    return url


def _success_html(frontend_url: str) -> HTMLResponse:
    safe_frontend_url = escape(frontend_url or "", quote=True)
    js_frontend_url = json.dumps(frontend_url or "")
    js_target_origin = json.dumps(_origin_from_url(frontend_url))
    html = f"""
    <html>
      <head><title>HubSpot Connected</title></head>
      <body style="font-family:Arial, sans-serif; background:#f6f7fb; color:#111;">
        <div style="max-width:560px;margin:10vh auto;background:#fff;padding:28px;border-radius:12px;">
          <h2 style="margin:0 0 8px;color:#0f8a3b;">HubSpot Connected</h2>
          <p style="line-height:1.5;">Your HubSpot account is connected. You can return to FoundryOps.</p>
          <p><a href="{safe_frontend_url}">Return to FoundryOps</a></p>
        </div>
        <script>
          const targetOrigin = {js_target_origin};
          const returnUrl = {js_frontend_url};
          try {{
            if (window.opener) {{
              window.opener.postMessage({{type: 'hubspot_connected'}}, targetOrigin);
              setTimeout(function() {{ try {{ window.close(); }} catch (e) {{}} }}, 300);
            }} else {{
              window.location.assign(returnUrl);
            }}
          }} catch (e) {{}}
        </script>
      </body>
    </html>
    """
    return HTMLResponse(content=html, status_code=200)


def _resolve_redirect_uri(request: Request) -> str:
    if settings.HUBSPOT_OAUTH_REDIRECT_URI:
        return settings.HUBSPOT_OAUTH_REDIRECT_URI
    return str(request.url_for("hubspot_oauth_callback"))


@router.get("/connect")
async def start_hubspot_connect(
    request: Request,
    redirect: bool = Query(False, description="If true, issue a 302 redirect to HubSpot auth URL."),
    account_id=Depends(get_current_account),
    redis=Depends(get_redis),
    _user: User = Depends(require_viewer),
):
    if not settings.HUBSPOT_CLIENT_ID or not settings.HUBSPOT_CLIENT_SECRET:
        raise HTTPException(status_code=503, detail="HubSpot OAuth not configured")

    nonce = secrets.token_urlsafe(32)
    try:
        verifier = _pkce_verifier()
        challenge = _pkce_challenge(verifier)
        state_token = _encode_state(nonce=nonce, flow="web")
    except RuntimeError as exc:
        raise HTTPException(status_code=503, detail=str(exc))

    await redis.setex(
        f"hubspot_oauth_nonce:{nonce}",
        OAUTH_NONCE_TTL_SECONDS,
        json.dumps({"account_id": str(account_id), "code_verifier": verifier}),
    )
    auth_url = build_hubspot_auth_url(
        state=state_token,
        redirect_uri=_resolve_redirect_uri(request),
        code_challenge=challenge,
    )
    if redirect:
        return RedirectResponse(auth_url)
    return {"auth_url": auth_url}


@router.get("/callback", name="hubspot_oauth_callback")
async def hubspot_oauth_callback(
    request: Request,
    code: Optional[str] = Query(None),
    state: Optional[str] = Query(None),
    error: Optional[str] = Query(None),
    error_description: Optional[str] = Query(None),
    db: AsyncSession = Depends(get_session),
    redis=Depends(get_redis),
):
    if not state:
        return _error_html("Missing OAuth state.")

    try:
        decoded = _decode_state(state)
        nonce = str(decoded.get("nonce") or "")
        flow = str(decoded.get("flow") or "")
    except RuntimeError:
        return _error_html("HubSpot OAuth state secret is not configured.")
    except jwt.ExpiredSignatureError:
        return _error_html("OAuth state expired. Please try again.")
    except jwt.InvalidTokenError:
        return _error_html("Invalid OAuth state.")

    if flow not in {"web", "cli"} or not nonce:
        return _error_html("Invalid OAuth session.")

    nonce_key = f"hubspot_oauth_nonce:{nonce}"
    nonce_data = await _redis_get_json(redis, nonce_key)
    if not nonce_data:
        return _error_html("OAuth session expired. Please retry.")
    await redis.delete(nonce_key)

    account_id = str(nonce_data.get("account_id") or "")
    session_id = str(nonce_data.get("cli_session_id") or "") if flow == "cli" else None
    code_verifier = str(nonce_data.get("code_verifier") or "")
    if not account_id:
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="error",
            message="Missing workspace context.",
        )
        return _error_html("Missing workspace context.")

    if error:
        message = error_description or error
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="error",
            message=message,
            account_id=account_id,
        )
        return _error_html(f"HubSpot authorization failed: {message}")
    if not code:
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="error",
            message="Missing authorization code.",
            account_id=account_id,
        )
        return _error_html("Missing authorization code.")

    try:
        token_payload = await exchange_code_for_tokens(
            code=code,
            redirect_uri=_resolve_redirect_uri(request),
            code_verifier=code_verifier or None,
        )
        access_token = str(token_payload.get("access_token") or "")
        identity = await fetch_token_identity(access_token)
        metadata_overrides = {
            "portal_id": identity.get("hub_id"),
            "hub_domain": identity.get("hub_domain"),
            "hubspot_user_id": identity.get("user"),
        }
        await store_hubspot_tokens(
            db,
            account_id=account_id,
            tokens=token_payload,
            created_by="hubspot_oauth",
            metadata_overrides=metadata_overrides,
        )
        await db.commit()
    except HubSpotOAuthError as exc:
        await db.rollback()
        safe_error = sanitize_hubspot_error_text(str(exc))
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="error",
            message=safe_error,
            account_id=account_id,
        )
        return _error_html(safe_error)
    except Exception:  # pragma: no cover - defensive
        await db.rollback()
        logger.exception("Unexpected HubSpot OAuth callback failure")
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="error",
            message="Unexpected HubSpot OAuth failure.",
            account_id=account_id,
        )
        return _error_html("Unexpected HubSpot OAuth failure.")

    if flow == "cli":
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="complete",
            workspace_id=account_id,
            account_id=account_id,
        )
        return _success_html(frontend_url=settings.FRONTEND_ORIGIN or "https://app.foundryops.io")

    frontend = settings.FRONTEND_ORIGIN or "https://app.foundryops.io"
    return _success_html(frontend_url=f"{frontend}/agent/quickstart?hubspot_connected=1")


@router.get("/status")
async def hubspot_status(
    db: AsyncSession = Depends(get_session),
    account_id=Depends(require_account),
):
    account_id_str = str(account_id)
    status_payload = await get_hubspot_status(db, account_id_str)

    if status_payload.get("connected") and status_payload.get("needs_refresh"):
        try:
            await refresh_if_needed(db, account_id=account_id_str, force=False, created_by="system")
            await db.commit()
            status_payload = await get_hubspot_status(db, account_id_str)
        except HubSpotOAuthError as exc:
            metadata = await get_hubspot_metadata(db, account_id_str)
            safe_message = sanitize_hubspot_error_text(str(exc))
            metadata.update(
                {
                    "last_refresh_status": "error",
                    "last_error_code": exc.code,
                    "last_error_message": safe_message,
                    "last_error_at": datetime.now(timezone.utc).isoformat(),
                }
            )
            secret_backend = get_saas_secret_backend()
            await secret_backend.set_secret(
                db=db,
                integration="hubspot",
                key="oauth_metadata",
                value=json.dumps(metadata, separators=(",", ":"), sort_keys=True),
                workspace_id=account_id_str,
                created_by="system",
            )
            await db.commit()
            status_payload = await get_hubspot_status(db, account_id_str)
            status_payload["status"] = "error"
            status_payload["error"] = {
                "code": exc.code,
                "message": safe_message,
            }
    return status_payload


@router.post("/refresh")
async def manual_refresh_hubspot(
    db: AsyncSession = Depends(get_session),
    account_id=Depends(get_current_account),
    _user: User = Depends(require_admin),
):
    try:
        metadata = await refresh_if_needed(
            db,
            account_id=str(account_id),
            force=True,
            created_by="system",
        )
        await db.commit()
        return {
            "ok": True,
            "refreshed_at": metadata.get("refreshed_at") or metadata.get("updated_at"),
            "expires_at": metadata.get("expires_at"),
        }
    except HubSpotOAuthError as exc:
        await db.rollback()
        raise HTTPException(
            status_code=exc.status_code,
            detail={
                "code": exc.code,
                "message": sanitize_hubspot_error_text(str(exc)),
            },
        )


@router.delete("/disconnect")
async def disconnect_hubspot_route(
    db: AsyncSession = Depends(get_session),
    account_id=Depends(get_current_account),
    _user: User = Depends(require_admin),
):
    await disconnect_hubspot(db, str(account_id))
    await db.commit()
    return {"ok": True, "message": "HubSpot disconnected"}


@router.get("/cli/authorize")
async def hubspot_cli_authorize(
    request: Request,
    session_id: str = Query(...),
    workspace_id: Optional[str] = Query(None),
    account_id: str = Depends(require_account),
    redis=Depends(get_redis),
):
    if not settings.HUBSPOT_CLIENT_ID or not settings.HUBSPOT_CLIENT_SECRET:
        raise HTTPException(status_code=503, detail="HubSpot OAuth not configured")
    target_workspace_id = str(workspace_id or account_id)
    # Optional explicit workspace override must still match caller auth context.
    if workspace_id and str(account_id) != target_workspace_id:
        raise HTTPException(status_code=403, detail="Workspace mismatch")

    nonce = secrets.token_urlsafe(32)
    try:
        verifier = _pkce_verifier()
        challenge = _pkce_challenge(verifier)
        state_token = _encode_state(nonce=nonce, flow="cli")
    except RuntimeError as exc:
        raise HTTPException(status_code=503, detail=str(exc))

    await redis.setex(
        f"hubspot_oauth_nonce:{nonce}",
        OAUTH_NONCE_TTL_SECONDS,
        json.dumps(
            {
                "account_id": target_workspace_id,
                "cli_session_id": session_id,
                "code_verifier": verifier,
            }
        ),
    )
    await _set_cli_session_status(
        redis,
        session_id=session_id,
        status="pending",
        workspace_id=target_workspace_id,
        account_id=str(account_id),
    )
    auth_url = build_hubspot_auth_url(
        state=state_token,
        redirect_uri=_resolve_redirect_uri(request),
        code_challenge=challenge,
    )
    return {"auth_url": auth_url, "workspace_id": target_workspace_id}


@router.get("/cli/poll")
async def hubspot_cli_poll(session_id: str = Query(...), redis=Depends(get_redis)):
    payload = await _redis_get_json(redis, f"hubspot_cli_session:{session_id}")
    if not payload:
        return {"status": "pending"}
    return payload


@router.get("/cli/token")
async def hubspot_cli_token(
    request: Request,
    workspace_id: Optional[str] = Query(None),
    session_id: Optional[str] = Query(None),
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
    redis=Depends(_get_optional_redis),
):
    upgrade_contract = _write_control_upgrade_contract(request)
    if upgrade_contract.upgrade_required and write_control.is_426_cutoff_enabled():
        raise _upgrade_required_exception(upgrade_contract)

    caller_account_id = str(account_id)
    session_workspace_id = ""
    if session_id:
        if redis is None:
            raise HTTPException(status_code=503, detail="Cache service unavailable")
        payload = await _redis_get_json(redis, f"hubspot_cli_session:{session_id}") or {}
        session_account_id = str(payload.get("account_id") or "")
        session_workspace_id = str(payload.get("workspace_id") or "")
        if session_account_id and session_account_id != caller_account_id:
            raise HTTPException(status_code=403, detail="Workspace mismatch")

    target_workspace_id = str(workspace_id or session_workspace_id or caller_account_id)
    if workspace_id and caller_account_id != str(workspace_id):
        raise HTTPException(status_code=403, detail="Workspace mismatch")
    if session_workspace_id and target_workspace_id != session_workspace_id:
        raise HTTPException(status_code=403, detail="Workspace mismatch")
    try:
        metadata = await refresh_if_needed(
            db,
            account_id=target_workspace_id,
            force=False,
            created_by="system",
        )
        await db.commit()
    except HubSpotOAuthError as exc:
        await db.rollback()
        raise HTTPException(
            status_code=exc.status_code,
            detail={
                "code": exc.code,
                "message": sanitize_hubspot_error_text(str(exc)),
            },
        )
    access_token = await get_hubspot_secret(db, target_workspace_id, "access_token")
    if not access_token:
        raise HTTPException(status_code=404, detail={"code": "hubspot_not_connected", "message": "HubSpot is not connected."})
    payload = {
        "access_token": access_token,
        "api_version": "v3",
        "portal_id": metadata.get("portal_id"),
        "hub_domain": metadata.get("hub_domain"),
        "time_zone": metadata.get("time_zone"),
        "expires_at": metadata.get("expires_at"),
    }
    return _attach_upgrade_contract(payload, upgrade_contract)


@router.post("/write-gates/issue")
async def hubspot_write_gate_issue(
    payload: HubSpotWriteGateIssueRequest,
    request: Request,
    identity: Optional[Dict[str, Any]] = Depends(_optional_identity_from_bearer),
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
    redis=Depends(get_redis),
):
    mode = write_control.effective_mode()
    upgrade_contract = _write_control_upgrade_contract(request)
    if upgrade_contract.upgrade_required and write_control.is_426_cutoff_enabled():
        raise _upgrade_required_exception(upgrade_contract)

    workspace_id = str(account_id)
    context = await _resolve_hubspot_write_context(db, workspace_id)
    if not context.get("entitled") and _license_identity_write_entitled(identity):
        context = {**context, "entitled": True}

    would_have_blocked: list[str] = []
    if not context.get("entitled"):
        denied = write_control.WriteControlDenied(
            status_code=402,
            error="UPGRADE_REQUIRED",
            message="HubSpot write operations require a paid tier.",
            next_step="UPGRADE",
            upgrade_url=upgrade_contract.upgrade_url,
        )
        if mode == "enforce":
            raise _write_control_http_exception(denied)
        would_have_blocked.append(denied.error)

    if not context.get("connected"):
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="PROVIDER_NOT_CONNECTED",
            message="HubSpot is not connected for this workspace.",
            next_step="REAUTH",
        )
        if mode == "enforce":
            raise _write_control_http_exception(denied)
        would_have_blocked.append(denied.error)

    provider_tenant_id = str(context.get("provider_tenant_id") or "").strip()
    if not provider_tenant_id:
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="PROVIDER_NOT_CONNECTED",
            message="HubSpot tenant context is unavailable for this workspace.",
            next_step="REAUTH",
        )
        if mode == "enforce":
            raise _write_control_http_exception(denied)
        would_have_blocked.append(denied.error)

    try:
        await write_control.enforce_gate_issue_rate_limit(
            redis=redis,
            workspace_id=workspace_id,
        )
    except write_control.WriteControlDenied as exc:
        raise _write_control_http_exception(exc)

    intent = payload.intent or HubSpotWriteIntent()
    try:
        issued = await write_control.issue_gate(
            redis=redis,
            workspace_id=workspace_id,
            principal_id=workspace_id,
            provider_tenant_id=provider_tenant_id,
            action_id=payload.action_id,
            command_id=payload.command_id,
            method=intent.method,
            path=intent.path,
            query=intent.query,
            body=intent.body,
            explicit_intent_fingerprint=payload.intent_fingerprint,
        )
    except write_control.WriteControlDenied as exc:
        raise _write_control_http_exception(exc)
    except RuntimeError as exc:
        raise HTTPException(
            status_code=503,
            detail={
                "error": "TENANT_RATE_LIMITED",
                "message": str(exc),
                "next_step": "RETRY_LATER",
            },
        )

    response_payload = {
        **issued,
        "mode": mode,
    }
    if would_have_blocked:
        response_payload["would_have_blocked"] = would_have_blocked
    return _attach_upgrade_contract(response_payload, upgrade_contract)


@router.post("/proxy/validate-json")
async def hubspot_proxy_validate_json(
    payload: HubSpotProxyJsonRequest,
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    mode = write_control.effective_mode()
    workspace_id = str(account_id)
    context = await _resolve_hubspot_write_context(db, workspace_id)
    provider_tenant_id = str(context.get("provider_tenant_id") or "")
    if not context.get("connected"):
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="PROVIDER_NOT_CONNECTED",
            message="HubSpot is not connected for this workspace.",
            next_step="REAUTH",
        )
        if mode == "enforce":
            raise _write_control_http_exception(denied)
        return {
            "ok": True,
            "mode": mode,
            "would_have_blocked": True,
            "would_have_blocked_reason": denied.error,
        }
    if not provider_tenant_id:
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="PROVIDER_NOT_CONNECTED",
            message="HubSpot tenant context is unavailable for this workspace.",
            next_step="REAUTH",
        )
        if mode == "enforce":
            raise _write_control_http_exception(denied)
        return {
            "ok": True,
            "mode": mode,
            "would_have_blocked": True,
            "would_have_blocked_reason": denied.error,
        }

    try:
        claims = write_control.decode_gate_token(payload.gate_token)
        validation = write_control.validate_bound_request(
            gate_claims=claims,
            workspace_id=workspace_id,
            provider_tenant_id=provider_tenant_id,
            action_id=payload.action_id,
            method=payload.method,
            path=payload.path,
            query=payload.query,
            body=payload.body,
        )
    except write_control.WriteControlDenied as exc:
        if mode == "shadow":
            return {
                "ok": True,
                "mode": mode,
                "would_have_blocked": True,
                "would_have_blocked_reason": exc.error,
            }
        raise _write_control_http_exception(exc)

    return {
        "ok": True,
        "mode": mode,
        "action_id": payload.action_id,
        "operation_class": validation.operation_class,
        "payload_sha256": validation.payload_sha256,
    }


@router.post("/proxy/execute-json")
async def hubspot_proxy_execute_json(
    payload: HubSpotProxyJsonRequest,
    idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
    redis=Depends(get_redis),
):
    mode = write_control.effective_mode()
    workspace_id = str(account_id)
    correlation_id = f"corr_{uuid4().hex}"
    context = await _resolve_hubspot_write_context(db, workspace_id)
    provider_tenant_id = str(context.get("provider_tenant_id") or "")

    if not context.get("connected"):
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="PROVIDER_NOT_CONNECTED",
            message="HubSpot is not connected for this workspace.",
            next_step="REAUTH",
        )
        raise _write_control_http_exception(denied, correlation_id=correlation_id)
    if not provider_tenant_id:
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="PROVIDER_NOT_CONNECTED",
            message="HubSpot tenant context is unavailable for this workspace.",
            next_step="REAUTH",
        )
        raise _write_control_http_exception(denied, correlation_id=correlation_id)

    shadow_reason: str | None = None
    claims = {}
    validation: write_control.ValidationResult | None = None
    try:
        claims = write_control.decode_gate_token(payload.gate_token)
        validation = write_control.validate_bound_request(
            gate_claims=claims,
            workspace_id=workspace_id,
            provider_tenant_id=provider_tenant_id,
            action_id=payload.action_id,
            method=payload.method,
            path=payload.path,
            query=payload.query,
            body=payload.body,
        )
    except write_control.WriteControlDenied as exc:
        if mode == "shadow":
            shadow_reason = exc.error
        else:
            raise _write_control_http_exception(exc, correlation_id=correlation_id)

    request_hash = validation.payload_sha256 if validation is not None else write_control.payload_sha256(payload.body)
    if idempotency_key:
        try:
            cached = await write_control.lookup_idempotency(
                redis=redis,
                workspace_id=workspace_id,
                provider_tenant_id=provider_tenant_id,
                action_id=payload.action_id,
                idempotency_key=idempotency_key,
                request_sha256=request_hash,
            )
        except write_control.WriteControlDenied as exc:
            raise _write_control_http_exception(exc, correlation_id=correlation_id)
        if cached:
            replay_payload = dict(cached)
            replay_payload.setdefault("correlation_id", correlation_id)
            replay_payload["mode"] = mode
            replay_payload["idempotency_replay"] = True
            return replay_payload

    if mode == "enforce":
        try:
            await write_control.consume_gate_jti(
                redis=redis,
                jti=str(claims.get("jti") or ""),
            )
        except write_control.WriteControlDenied as exc:
            raise _write_control_http_exception(exc, correlation_id=correlation_id)

    try:
        await refresh_if_needed(
            db,
            account_id=workspace_id,
            force=False,
            created_by="system",
        )
        await db.commit()
    except HubSpotOAuthError as exc:
        await db.rollback()
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="WRITE_APP_NOT_CONNECTED",
            message=sanitize_hubspot_error_text(str(exc)),
            next_step="REAUTH",
        )
        raise _write_control_http_exception(denied, correlation_id=correlation_id)
    except Exception:
        await db.rollback()
        logger.exception("HubSpot proxy execute failed while refreshing token")
        denied = write_control.WriteControlDenied(
            status_code=503,
            error="TENANT_RATE_LIMITED",
            message="HubSpot write proxy is temporarily unavailable.",
            next_step="RETRY_LATER",
        )
        raise _write_control_http_exception(denied, correlation_id=correlation_id)

    access_token = await get_hubspot_secret(db, workspace_id, "access_token")
    if not access_token:
        denied = write_control.WriteControlDenied(
            status_code=412,
            error="WRITE_APP_NOT_CONNECTED",
            message="HubSpot write app is not connected.",
            next_step="REAUTH",
        )
        raise _write_control_http_exception(denied, correlation_id=correlation_id)

    provider_result = await write_control.execute_hubspot_json(
        access_token=access_token,
        method=payload.method,
        path=payload.path,
        query=payload.query,
        body=payload.body,
    )
    execute_payload: dict[str, Any] = {
        "status_code": provider_result.get("status_code", 500),
        "provider_request_id": provider_result.get("provider_request_id"),
        "proxy_latency_ms": provider_result.get("proxy_latency_ms"),
        "correlation_id": correlation_id,
        "retry_after_ms": provider_result.get("retry_after_ms"),
        "provider_headers": provider_result.get("provider_headers") or {},
        "body": provider_result.get("body") or {},
        "mode": mode,
    }
    if validation is not None:
        execute_payload["operation_class"] = validation.operation_class
    elif claims:
        execute_payload["operation_class"] = claims.get("operation_class")
    if shadow_reason:
        execute_payload["shadow_would_have_blocked"] = True
        execute_payload["shadow_reason"] = shadow_reason

    if idempotency_key:
        try:
            await write_control.store_idempotency(
                redis=redis,
                workspace_id=workspace_id,
                provider_tenant_id=provider_tenant_id,
                action_id=payload.action_id,
                idempotency_key=idempotency_key,
                request_sha256=request_hash,
                response_payload=execute_payload,
            )
        except Exception:
            logger.exception("Failed to store HubSpot write idempotency entry")
    return execute_payload
