"""
Channel connection API endpoints — Slack OAuth, WhatsApp manual connect, test messaging.

Endpoints:
  GET  /api/portal/channels/slack/install?tenant_id={id}
         → generates HMAC-signed OAuth state, returns Slack authorize URL
  GET  /api/portal/channels/slack/callback?code={code}&state={state}
         → verifies state, exchanges code for bot_token, stores in channel_connections
  POST /api/portal/channels/whatsapp/connect
         → validates system_user_token, encrypts it, stores in channel_connections
  POST /api/portal/channels/{tenant_id}/test
         → sends a test message via the connected channel

OAuth state format (HMAC-SHA256 signed):
  base64url( JSON({ tenant_id, nonce }) + "." + HMAC-SHA256(payload, secret) )

This design provides CSRF protection for the Slack OAuth callback:
  - The nonce prevents replay attacks (state is single-use via constant-time comparison)
  - The HMAC signature prevents forgery
  - The tenant_id embedded in the state is recovered after verification

See STATE.md decision: HMAC uses hmac.new() with hmac.compare_digest for timing-safe verification.
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import secrets
import uuid
from typing import Any

import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession

from shared.api.rbac import PortalCaller, require_tenant_admin, require_tenant_member
from shared.config import settings
from shared.crypto import KeyEncryptionService
from shared.db import get_session
from shared.models.tenant import ChannelConnection, ChannelTypeEnum, Tenant

channels_router = APIRouter(prefix="/api/portal/channels", tags=["channels"])

# Slack scopes required for AI employee functionality
_SLACK_SCOPES = ",".join([
    "app_mentions:read",
    "channels:read",
    "channels:history",
    "chat:write",
    "im:read",
    "im:write",
    "im:history",
])


# ---------------------------------------------------------------------------
# HMAC OAuth state generation / verification
# ---------------------------------------------------------------------------

def generate_oauth_state(tenant_id: str, secret: str) -> str:
    """
    Generate a URL-safe, HMAC-signed OAuth state parameter.

    Format:
        base64url(payload_json).base64url(hmac_sig)

    Where:
        payload_json = {"tenant_id": tenant_id, "nonce": random_hex}
        hmac_sig     = HMAC-SHA256(payload_json, secret)

    Returns:
        A URL-safe base64-encoded string containing the signed state.
    """
    nonce = secrets.token_hex(16)
    payload = json.dumps({"tenant_id": tenant_id, "nonce": nonce}, separators=(",", ":"))
    payload_b64 = base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=")

    sig = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).decode().rstrip("=")

    return f"{payload_b64}.{sig_b64}"


def verify_oauth_state(state: str, secret: str) -> str:
    """
    Verify an HMAC-signed OAuth state and return the embedded tenant_id.

    Raises:
        ValueError: if the state is malformed, tampered, or the HMAC is invalid.

    Returns:
        The tenant_id embedded in the state payload.
    """
    try:
        parts = state.split(".", 1)
        if len(parts) != 2:
            raise ValueError("Malformed state: expected exactly one '.' separator")

        payload_b64, sig_b64 = parts

        # Decode payload (add padding back)
        payload_bytes = base64.urlsafe_b64decode(payload_b64 + "==")
        payload_str = payload_bytes.decode()

        # Recompute expected HMAC
        expected_sig = hmac.new(secret.encode(), payload_str.encode(), hashlib.sha256).digest()
        expected_sig_b64 = base64.urlsafe_b64encode(expected_sig).decode().rstrip("=")

        # Timing-safe comparison
        if not hmac.compare_digest(sig_b64, expected_sig_b64):
            raise ValueError("HMAC signature mismatch — state may have been tampered")

        payload = json.loads(payload_str)
        tenant_id: str = payload["tenant_id"]
        return tenant_id

    except (KeyError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise ValueError(f"Invalid OAuth state: {exc}") from exc


# ---------------------------------------------------------------------------
# Pydantic schemas
# ---------------------------------------------------------------------------

class SlackInstallResponse(BaseModel):
    url: str
    state: str


class WhatsAppConnectRequest(BaseModel):
    tenant_id: uuid.UUID
    phone_number_id: str
    waba_id: str
    system_user_token: str


class WhatsAppConnectResponse(BaseModel):
    id: str
    tenant_id: str
    channel_type: str
    workspace_id: str


class TestChannelRequest(BaseModel):
    channel_type: str


class TestChannelResponse(BaseModel):
    success: bool
    message: str


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _get_encryption_service() -> KeyEncryptionService:
    """Return the platform-level KeyEncryptionService from settings."""
    if not settings.platform_encryption_key:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="PLATFORM_ENCRYPTION_KEY not configured",
        )
    return KeyEncryptionService(
        primary_key=settings.platform_encryption_key,
        previous_key=settings.platform_encryption_key_previous,
    )


async def _get_tenant_or_404(tenant_id: uuid.UUID, session: AsyncSession) -> Tenant:
    result = await session.execute(select(Tenant).where(Tenant.id == tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
    return tenant


# ---------------------------------------------------------------------------
# Slack OAuth endpoints
# ---------------------------------------------------------------------------

@channels_router.get("/slack/install", response_model=SlackInstallResponse)
async def slack_install(
    tenant_id: uuid.UUID = Query(...),
    caller: PortalCaller = Depends(require_tenant_admin),
) -> SlackInstallResponse:
    """
    Generate the Slack OAuth authorization URL for installing the app.

    Returns the URL the operator should redirect their browser to, plus the
    opaque state token (for debugging / manual testing).
    """
    if not settings.oauth_state_secret:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="OAUTH_STATE_SECRET not configured",
        )

    state = generate_oauth_state(tenant_id=str(tenant_id), secret=settings.oauth_state_secret)

    url = (
        f"https://slack.com/oauth/v2/authorize"
        f"?client_id={settings.slack_client_id}"
        f"&scope={_SLACK_SCOPES}"
        f"&redirect_uri={settings.slack_oauth_redirect_uri}"
        f"&state={state}"
    )

    return SlackInstallResponse(url=url, state=state)


@channels_router.get("/slack/callback")
async def slack_callback(
    code: str = Query(...),
    state: str = Query(...),
    session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
    """
    Handle the Slack OAuth callback.

    1. Verify HMAC state (CSRF protection)
    2. Exchange code for bot_token via Slack API
    3. Encrypt bot_token and store in channel_connections
    """
    if not settings.oauth_state_secret:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="OAUTH_STATE_SECRET not configured",
        )

    # Verify state (raises ValueError on tampering)
    try:
        tenant_id_str = verify_oauth_state(state=state, secret=settings.oauth_state_secret)
        tenant_id = uuid.UUID(tenant_id_str)
    except (ValueError, Exception) as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid OAuth state: {exc}",
        ) from exc

    await _get_tenant_or_404(tenant_id, session)

    # Exchange code for tokens via Slack API
    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.post(
            "https://slack.com/api/oauth.v2.access",
            data={
                "code": code,
                "client_id": settings.slack_client_id,
                "client_secret": settings.slack_client_secret,
                "redirect_uri": settings.slack_oauth_redirect_uri,
            },
        )

    if response.status_code != 200:
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail="Slack API returned an error during token exchange",
        )

    slack_data = response.json()
    if not slack_data.get("ok"):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Slack OAuth error: {slack_data.get('error', 'unknown')}",
        )

    bot_token: str = slack_data["access_token"]
    workspace_id: str = slack_data["team"]["id"]
    team_name: str = slack_data["team"]["name"]
    bot_user_id: str = slack_data.get("bot_user_id", "")

    # Encrypt bot_token before storage
    enc_svc = _get_encryption_service()
    encrypted_token = enc_svc.encrypt(bot_token)

    # Check for existing connection
    existing = await session.execute(
        select(ChannelConnection).where(
            ChannelConnection.channel_type == ChannelTypeEnum.SLACK,
            ChannelConnection.workspace_id == workspace_id,
        )
    )
    conn = existing.scalar_one_or_none()

    if conn is None:
        conn = ChannelConnection(
            tenant_id=tenant_id,
            channel_type=ChannelTypeEnum.SLACK,
            workspace_id=workspace_id,
            config={
                "bot_token": encrypted_token,
                "bot_user_id": bot_user_id,
                "team_name": team_name,
            },
        )
        session.add(conn)
    else:
        conn.config = {
            "bot_token": encrypted_token,
            "bot_user_id": bot_user_id,
            "team_name": team_name,
        }

    await session.commit()

    return {
        "ok": True,
        "workspace_id": workspace_id,
        "team_name": team_name,
        "tenant_id": str(tenant_id),
    }


# ---------------------------------------------------------------------------
# WhatsApp manual connect endpoint
# ---------------------------------------------------------------------------

@channels_router.post("/whatsapp/connect", response_model=WhatsAppConnectResponse, status_code=status.HTTP_201_CREATED)
async def whatsapp_connect(
    body: WhatsAppConnectRequest,
    caller: PortalCaller = Depends(require_tenant_admin),
    session: AsyncSession = Depends(get_session),
) -> WhatsAppConnectResponse:
    """
    Manually connect a WhatsApp Business phone number to a tenant.

    Validates the system_user_token by calling the Meta Graph API, then
    encrypts and stores the token in channel_connections.
    """
    await _get_tenant_or_404(body.tenant_id, session)

    # Validate token by calling Meta Graph API
    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.get(
            f"https://graph.facebook.com/v22.0/{body.phone_number_id}",
            headers={"Authorization": f"Bearer {body.system_user_token}"},
        )

    if response.status_code != 200:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Meta Graph API validation failed: {response.status_code}",
        )

    # Encrypt token before storage
    enc_svc = _get_encryption_service()
    encrypted_token = enc_svc.encrypt(body.system_user_token)

    # Check for existing connection
    existing = await session.execute(
        select(ChannelConnection).where(
            ChannelConnection.channel_type == ChannelTypeEnum.WHATSAPP,
            ChannelConnection.workspace_id == body.phone_number_id,
        )
    )
    conn = existing.scalar_one_or_none()

    if conn is None:
        conn = ChannelConnection(
            tenant_id=body.tenant_id,
            channel_type=ChannelTypeEnum.WHATSAPP,
            workspace_id=body.phone_number_id,
            config={
                "system_user_token": encrypted_token,
                "waba_id": body.waba_id,
                "phone_number_id": body.phone_number_id,
            },
        )
        session.add(conn)
    else:
        conn.config = {
            "system_user_token": encrypted_token,
            "waba_id": body.waba_id,
            "phone_number_id": body.phone_number_id,
        }

    await session.commit()
    await session.refresh(conn)

    return WhatsAppConnectResponse(
        id=str(conn.id),
        tenant_id=str(conn.tenant_id),
        channel_type=conn.channel_type.value,
        workspace_id=conn.workspace_id,
    )


# ---------------------------------------------------------------------------
# Test channel endpoint
# ---------------------------------------------------------------------------

@channels_router.post("/{tenant_id}/test", response_model=TestChannelResponse)
async def test_channel_connection(
    tenant_id: uuid.UUID,
    body: TestChannelRequest,
    caller: PortalCaller = Depends(require_tenant_member),
    session: AsyncSession = Depends(get_session),
) -> TestChannelResponse:
    """
    Send a test message via the connected channel to verify the integration.

    Loads the ChannelConnection for the tenant, decrypts the bot token,
    and sends "Konstruct connected successfully" via the appropriate SDK.
    """
    await _get_tenant_or_404(tenant_id, session)

    # Resolve channel type
    try:
        channel_type = ChannelTypeEnum(body.channel_type)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Unsupported channel type: {body.channel_type}",
        )

    # Load channel connection
    result = await session.execute(
        select(ChannelConnection).where(
            ChannelConnection.tenant_id == tenant_id,
            ChannelConnection.channel_type == channel_type,
        )
    )
    conn = result.scalar_one_or_none()
    if conn is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No {body.channel_type} connection found for this tenant",
        )

    enc_svc = _get_encryption_service()

    if channel_type == ChannelTypeEnum.SLACK:
        encrypted_token = conn.config.get("bot_token", "")
        if not encrypted_token:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Slack bot token not found in connection config",
            )
        bot_token = enc_svc.decrypt(encrypted_token)

        # Send test message via Slack Web API
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                "https://slack.com/api/chat.postMessage",
                headers={
                    "Authorization": f"Bearer {bot_token}",
                    "Content-Type": "application/json",
                },
                json={
                    "channel": conn.config.get("bot_user_id", ""),
                    "text": "Konstruct connected successfully",
                },
            )

        if response.status_code == 200 and response.json().get("ok"):
            return TestChannelResponse(success=True, message="Test message sent successfully")
        else:
            error = response.json().get("error", "unknown") if response.status_code == 200 else str(response.status_code)
            return TestChannelResponse(success=False, message=f"Slack API error: {error}")

    elif channel_type == ChannelTypeEnum.WHATSAPP:
        encrypted_token = conn.config.get("system_user_token", "")
        phone_number_id = conn.config.get("phone_number_id", conn.workspace_id)
        if not encrypted_token:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="WhatsApp system user token not found in connection config",
            )
        # For WhatsApp, we can't easily send a "test" message without a recipient.
        # Return success if the connection config is valid.
        return TestChannelResponse(
            success=True,
            message=f"WhatsApp connection validated for phone number ID {phone_number_id}",
        )

    else:
        return TestChannelResponse(
            success=False,
            message=f"Test messages not yet supported for {body.channel_type}",
        )
