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

import jwt
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession

from ... import settings
from ...api.progress import get_redis
from ...auth import get_current_account
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.zoominfo_oauth import (
    ZoomInfoOAuthError,
    build_zoominfo_auth_url,
    disconnect_zoominfo,
    exchange_code_for_tokens,
    fetch_zoominfo_identity,
    get_zoominfo_metadata,
    get_zoominfo_secret,
    get_zoominfo_status,
    refresh_if_needed,
    sanitize_zoominfo_error_text,
    store_zoominfo_tokens,
)

logger = logging.getLogger(__name__)

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

OAUTH_NONCE_TTL_SECONDS = 600
CLI_SESSION_TTL_SECONDS = 300


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("ZoomInfo OAuth state secret missing; using dev fallback in %s environment", env or "unknown")
        return "dev-secret"
    raise RuntimeError("ZoomInfo 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]]:
    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"zoominfo_cli_session:{session_id}",
        CLI_SESSION_TTL_SECONDS,
        json.dumps(payload),
    )


def _error_html(message: str) -> HTMLResponse:
    safe_message = escape(message or "Unexpected ZoomInfo OAuth failure.")
    html = f"""
    <html>
      <head><title>ZoomInfo 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;">ZoomInfo 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>ZoomInfo 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;">ZoomInfo Connected</h2>
          <p style="line-height:1.5;">Your ZoomInfo 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: 'zoominfo_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.ZOOMINFO_OAUTH_REDIRECT_URI:
        return settings.ZOOMINFO_OAUTH_REDIRECT_URI
    return str(request.url_for("zoominfo_oauth_callback"))


@router.get("/connect")
async def start_zoominfo_connect(
    request: Request,
    redirect: bool = Query(False, description="If true, issue a 302 redirect to ZoomInfo auth URL."),
    account_id=Depends(get_current_account),
    redis=Depends(get_redis),
    _user: User = Depends(require_viewer),
):
    if (
        not settings.ZOOMINFO_CLIENT_ID
        or not settings.ZOOMINFO_CLIENT_SECRET
        or not (settings.ZOOMINFO_OAUTH_AUTHORIZE_URL or "").strip()
        or not (settings.ZOOMINFO_OAUTH_TOKEN_URL or "").strip()
    ):
        raise HTTPException(status_code=503, detail="ZoomInfo 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"zoominfo_oauth_nonce:{nonce}",
        OAUTH_NONCE_TTL_SECONDS,
        json.dumps({"account_id": str(account_id), "code_verifier": verifier}),
    )
    try:
        auth_url = build_zoominfo_auth_url(
            state=state_token,
            redirect_uri=_resolve_redirect_uri(request),
            code_challenge=challenge,
        )
    except ZoomInfoOAuthError as exc:
        raise HTTPException(status_code=exc.status_code, detail=sanitize_zoominfo_error_text(str(exc)))
    if redirect:
        return RedirectResponse(auth_url)
    return {"auth_url": auth_url}


@router.get("/callback", name="zoominfo_oauth_callback")
async def zoominfo_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("ZoomInfo 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"zoominfo_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"ZoomInfo 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_zoominfo_identity(access_token)
        metadata_overrides = {
            "org_id": identity.get("org_id"),
            "org_name": identity.get("org_name"),
            "user_id": identity.get("user_id"),
            "email": identity.get("email"),
            "name": identity.get("name"),
        }
        await store_zoominfo_tokens(
            db,
            account_id=account_id,
            tokens=token_payload,
            created_by="zoominfo_oauth",
            metadata_overrides=metadata_overrides,
        )
        await db.commit()
    except ZoomInfoOAuthError as exc:
        await db.rollback()
        safe_error = sanitize_zoominfo_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 ZoomInfo OAuth callback failure")
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="error",
            message="Unexpected ZoomInfo OAuth failure.",
            account_id=account_id,
        )
        return _error_html("Unexpected ZoomInfo 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?zoominfo_connected=1")


@router.get("/status")
async def zoominfo_status(
    db: AsyncSession = Depends(get_session),
    account_id=Depends(require_account),
):
    account_id_str = str(account_id)
    status_payload = await get_zoominfo_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_zoominfo_status(db, account_id_str)
        except ZoomInfoOAuthError as exc:
            metadata = await get_zoominfo_metadata(db, account_id_str)
            safe_message = sanitize_zoominfo_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="zoominfo",
                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_zoominfo_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_zoominfo(
    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 ZoomInfoOAuthError as exc:
        await db.rollback()
        raise HTTPException(
            status_code=exc.status_code,
            detail={
                "code": exc.code,
                "message": sanitize_zoominfo_error_text(str(exc)),
            },
        )


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


@router.get("/cli/authorize")
async def zoominfo_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.ZOOMINFO_CLIENT_ID
        or not settings.ZOOMINFO_CLIENT_SECRET
        or not (settings.ZOOMINFO_OAUTH_AUTHORIZE_URL or "").strip()
        or not (settings.ZOOMINFO_OAUTH_TOKEN_URL or "").strip()
    ):
        raise HTTPException(status_code=503, detail="ZoomInfo 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"zoominfo_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),
    )
    try:
        auth_url = build_zoominfo_auth_url(
            state=state_token,
            redirect_uri=_resolve_redirect_uri(request),
            code_challenge=challenge,
        )
    except ZoomInfoOAuthError as exc:
        raise HTTPException(status_code=exc.status_code, detail=sanitize_zoominfo_error_text(str(exc)))
    return {"auth_url": auth_url, "workspace_id": target_workspace_id}


@router.get("/cli/poll")
async def zoominfo_cli_poll(
    session_id: str = Query(...),
    account_id: str = Depends(require_account),
    redis=Depends(get_redis),
):
    payload = await _redis_get_json(redis, f"zoominfo_cli_session:{session_id}")
    if not payload:
        return {"status": "pending"}
    payload_account_id = str(payload.get("account_id") or "")
    if payload_account_id and payload_account_id != str(account_id):
        raise HTTPException(status_code=403, detail="Workspace mismatch")
    return payload


@router.get("/cli/token")
async def zoominfo_cli_token(
    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_redis),
):
    caller_account_id = str(account_id)
    session_workspace_id = ""
    if session_id:
        payload = await _redis_get_json(redis, f"zoominfo_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 ZoomInfoOAuthError as exc:
        await db.rollback()
        raise HTTPException(
            status_code=exc.status_code,
            detail={
                "code": exc.code,
                "message": sanitize_zoominfo_error_text(str(exc)),
            },
        )
    access_token = await get_zoominfo_secret(db, target_workspace_id, "access_token")
    if not access_token:
        raise HTTPException(status_code=404, detail={"code": "zoominfo_not_connected", "message": "ZoomInfo is not connected."})
    return {
        "access_token": access_token,
        "org_id": metadata.get("org_id"),
        "org_name": metadata.get("org_name"),
        "expires_at": metadata.get("expires_at"),
        "scope": metadata.get("scope"),
    }
