from __future__ import annotations

from datetime import datetime, timedelta, timezone
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, JSONResponse, 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.outreach_oauth import (
    OutreachOAuthError,
    build_outreach_auth_url,
    disconnect_outreach,
    exchange_code_for_tokens,
    fetch_outreach_identity,
    has_outreach_oauth_credentials,
    get_outreach_metadata,
    get_outreach_secret,
    get_outreach_status,
    refresh_if_needed,
    resolve_outreach_oauth_client,
    resolve_outreach_oauth_client_source,
    resolve_scope_profile,
    sanitize_outreach_error_text,
    store_outreach_tokens,
)
from ...services.roles import require_admin, require_viewer
from ...services.secret_storage import get_saas_secret_backend

logger = logging.getLogger(__name__)

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

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("Outreach OAuth state secret missing; using dev fallback in %s environment", env or "unknown")
        return "dev-secret"
    raise RuntimeError("Outreach OAuth state secret is not configured.")


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"])


def _decode_state_unverified(state_token: str) -> Dict[str, Any]:
    return jwt.decode(
        state_token,
        options={"verify_signature": False, "verify_exp": False},
        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


def _oauth_nonce_key(nonce: str) -> str:
    return f"outreach_oauth_nonce:{nonce}"


def _oauth_state_key(state_token: str) -> str:
    return f"outreach_oauth_state:{state_token}"


async def _store_oauth_session(
    redis,
    *,
    nonce: str,
    state_token: str,
    flow: str,
    account_id: str,
    scope_profile: str,
    cli_session_id: Optional[str] = None,
) -> None:
    payload: Dict[str, Any] = {
        "nonce": nonce,
        "state": state_token,
        "flow": flow,
        "account_id": account_id,
        "scope_profile": scope_profile,
    }
    if cli_session_id:
        payload["cli_session_id"] = cli_session_id
    serialized = json.dumps(payload)
    await redis.setex(_oauth_nonce_key(nonce), OAUTH_NONCE_TTL_SECONDS, serialized)
    await redis.setex(_oauth_state_key(state_token), OAUTH_NONCE_TTL_SECONDS, serialized)


async def _consume_oauth_session(
    redis,
    *,
    nonce: Optional[str] = None,
    state_token: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
    keys = []
    if nonce:
        keys.append(_oauth_nonce_key(nonce))
    if state_token:
        keys.append(_oauth_state_key(state_token))

    payload: Optional[Dict[str, Any]] = None
    for key in keys:
        payload = await _redis_get_json(redis, key)
        if payload:
            break
    if not payload:
        return None

    stored_nonce = str(payload.get("nonce") or nonce or "")
    stored_state = str(payload.get("state") or state_token or "")
    delete_keys = set(keys)
    if stored_nonce:
        delete_keys.add(_oauth_nonce_key(stored_nonce))
    if stored_state:
        delete_keys.add(_oauth_state_key(stored_state))
    for key in delete_keys:
        await redis.delete(key)
    return payload


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"outreach_cli_session:{session_id}",
        CLI_SESSION_TTL_SECONDS,
        json.dumps(payload),
    )


def _error_html(message: str) -> HTMLResponse:
    safe_message = escape(message or "Unexpected Outreach OAuth failure.")
    html = f"""
    <html>
      <head><title>Outreach 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;">Outreach 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 _is_dev_env() -> bool:
    return (getattr(settings, "ENV", "") or "").strip().lower() in {
        "dev",
        "development",
        "local",
        "test",
    }


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>Outreach 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;">Outreach Connected</h2>
          <p style="line-height:1.5;">Your Outreach 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: 'outreach_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:
    configured = (settings.OUTREACH_OAUTH_REDIRECT_URI or "").strip()
    if configured:
        if _is_dev_env():
            try:
                env_host = (urlparse(configured).hostname or "").strip().lower()
                req_host = ((request.url.hostname or "") if request and request.url else "").strip().lower()
                if env_host and req_host and env_host != req_host:
                    logger.warning(
                        "Ignoring OUTREACH_OAUTH_REDIRECT_URI in development due host mismatch: env_host=%s request_host=%s",
                        env_host,
                        req_host,
                    )
                    return str(request.url_for("outreach_oauth_callback"))
            except Exception:
                logger.exception("Failed to compare Outreach OAuth redirect hosts")
        return configured
    return str(request.url_for("outreach_oauth_callback"))


@router.get("/connect")
async def start_outreach_connect(
    request: Request,
    scope_profile: str = Query("default"),
    redirect: bool = Query(False, description="If true, issue a 302 redirect to Outreach auth URL."),
    account_id=Depends(get_current_account),
    redis=Depends(get_redis),
    _user: User = Depends(require_viewer),
):
    redirect_uri = _resolve_redirect_uri(request)
    if not has_outreach_oauth_credentials(redirect_uri=redirect_uri):
        raise HTTPException(status_code=503, detail="Outreach OAuth not configured")
    nonce = secrets.token_urlsafe(32)
    resolved_profile = resolve_scope_profile(scope_profile)
    try:
        state_token = _encode_state(nonce=nonce, flow="web")
    except RuntimeError as exc:
        raise HTTPException(status_code=503, detail=str(exc))

    await _store_oauth_session(
        redis,
        nonce=nonce,
        state_token=state_token,
        flow="web",
        account_id=str(account_id),
        scope_profile=resolved_profile,
    )
    client_source = resolve_outreach_oauth_client_source(redirect_uri=redirect_uri)
    logger.info("Outreach OAuth connect using redirect_uri=%s client_source=%s", redirect_uri, client_source)
    auth_url = build_outreach_auth_url(
        state=state_token,
        redirect_uri=redirect_uri,
        scope_profile=resolved_profile,
    )
    if redirect:
        return RedirectResponse(auth_url)
    return JSONResponse(
        content={"auth_url": auth_url},
        headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
    )


@router.get("/callback", name="outreach_oauth_callback")
async def outreach_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.")

    nonce = ""
    flow = ""
    nonce_data: Optional[Dict[str, Any]] = None
    try:
        decoded = _decode_state(state)
        nonce = str(decoded.get("nonce") or "")
        flow = str(decoded.get("flow") or "")
    except RuntimeError:
        return _error_html("Outreach OAuth state secret is not configured.")
    except jwt.ExpiredSignatureError:
        return _error_html("OAuth state expired. Please try again.")
    except jwt.InvalidTokenError:
        nonce_data = await _consume_oauth_session(redis, state_token=state)
        if not nonce_data:
            try:
                decoded = _decode_state_unverified(state)
            except jwt.InvalidTokenError:
                return _error_html("Invalid OAuth state.")
            nonce = str(decoded.get("nonce") or "")
            flow = str(decoded.get("flow") or "")
            if flow not in {"web", "cli"} or not nonce:
                return _error_html("Invalid OAuth state.")
            # Signing-key drift can invalidate the JWT while the nonce record is
            # still authoritative in Redis. Requiring that record preserves CSRF
            # protection for older sessions that predate raw-state storage.
            nonce_data = await _consume_oauth_session(redis, nonce=nonce, state_token=state)
            if not nonce_data:
                return _error_html("Invalid OAuth state.")

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

    if not nonce_data:
        nonce_data = await _consume_oauth_session(redis, nonce=nonce, state_token=state)
    if not nonce_data:
        return _error_html("OAuth session expired. Please retry.")

    account_id = str(nonce_data.get("account_id") or "")
    scope_profile = resolve_scope_profile(str(nonce_data.get("scope_profile") or "default"))
    session_id = str(nonce_data.get("cli_session_id") or "") if flow == "cli" else None

    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"Outreach 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:
        redirect_uri = _resolve_redirect_uri(request)
        oauth_client_id, _oauth_client_secret = resolve_outreach_oauth_client(redirect_uri=redirect_uri)
        oauth_client_source = resolve_outreach_oauth_client_source(redirect_uri=redirect_uri)
        token_payload = await exchange_code_for_tokens(
            code=code,
            redirect_uri=redirect_uri,
        )
        access_token = str(token_payload.get("access_token") or "")
        identity = await fetch_outreach_identity(access_token)
        metadata_overrides = {
            "scope_profile": scope_profile,
            "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"),
            "oauth_client_id": oauth_client_id,
            "oauth_client_source": oauth_client_source,
        }
        await store_outreach_tokens(
            db,
            account_id=account_id,
            tokens=token_payload,
            created_by="outreach_oauth",
            metadata_overrides=metadata_overrides,
        )
        await db.commit()
    except OutreachOAuthError as exc:
        await db.rollback()
        safe_error = sanitize_outreach_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 Outreach OAuth callback failure")
        await _set_cli_session_status(
            redis,
            session_id=session_id,
            status="error",
            message="Unexpected Outreach OAuth failure.",
            account_id=account_id,
        )
        return _error_html("Unexpected Outreach 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?outreach_connected=1")


@router.get("/status")
async def outreach_status(
    db: AsyncSession = Depends(get_session),
    account_id=Depends(require_account),
):
    account_id_str = str(account_id)
    status_payload = await get_outreach_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_outreach_status(db, account_id_str)
        except OutreachOAuthError as exc:
            metadata = await get_outreach_metadata(db, account_id_str)
            safe_message = sanitize_outreach_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="outreach",
                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_outreach_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_outreach(
    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 OutreachOAuthError as exc:
        await db.rollback()
        raise HTTPException(
            status_code=exc.status_code,
            detail={
                "code": exc.code,
                "message": sanitize_outreach_error_text(str(exc)),
            },
        )


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


@router.get("/cli/authorize")
async def outreach_cli_authorize(
    request: Request,
    session_id: str = Query(...),
    scope_profile: str = Query("default"),
    workspace_id: Optional[str] = Query(None),
    account_id: str = Depends(require_account),
    redis=Depends(get_redis),
):
    redirect_uri = _resolve_redirect_uri(request)
    if not has_outreach_oauth_credentials(redirect_uri=redirect_uri):
        raise HTTPException(status_code=503, detail="Outreach OAuth not configured")
    target_workspace_id = str(workspace_id or account_id)
    if workspace_id and str(account_id) != target_workspace_id:
        raise HTTPException(status_code=403, detail="Workspace mismatch")

    resolved_profile = resolve_scope_profile(scope_profile)
    nonce = secrets.token_urlsafe(32)
    try:
        state_token = _encode_state(nonce=nonce, flow="cli")
    except RuntimeError as exc:
        raise HTTPException(status_code=503, detail=str(exc))

    await _store_oauth_session(
        redis,
        nonce=nonce,
        state_token=state_token,
        flow="cli",
        account_id=target_workspace_id,
        scope_profile=resolved_profile,
        cli_session_id=session_id,
    )
    await _set_cli_session_status(
        redis,
        session_id=session_id,
        status="pending",
        workspace_id=target_workspace_id,
        account_id=str(account_id),
    )

    client_source = resolve_outreach_oauth_client_source(redirect_uri=redirect_uri)
    logger.info("Outreach CLI authorize using redirect_uri=%s client_source=%s", redirect_uri, client_source)
    auth_url = build_outreach_auth_url(
        state=state_token,
        redirect_uri=redirect_uri,
        scope_profile=resolved_profile,
    )
    return {
        "auth_url": auth_url,
        "workspace_id": target_workspace_id,
        "scope_profile": resolved_profile,
    }


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


@router.get("/cli/token")
async def outreach_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"outreach_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 OutreachOAuthError as exc:
        await db.rollback()
        raise HTTPException(
            status_code=exc.status_code,
            detail={
                "code": exc.code,
                "message": sanitize_outreach_error_text(str(exc)),
            },
        )

    access_token = await get_outreach_secret(db, target_workspace_id, "access_token")
    if not access_token:
        raise HTTPException(
            status_code=404,
            detail={"code": "outreach_not_connected", "message": "Outreach is not connected."},
        )

    return {
        "access_token": access_token,
        "scope_profile": metadata.get("scope_profile") or "default",
        "org_id": metadata.get("org_id"),
        "org_name": metadata.get("org_name"),
        "expires_at": metadata.get("expires_at"),
    }
