"""Slack interactivity (buttons, modals) endpoint.

This handles Block Kit button clicks and shortcut submissions from Slack.
"""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass
from typing import Any, Optional

from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse

from ... import settings
from ...db import AsyncSessionLocal
from ...services.salesforce_gateway import get_salesforce_gateway, SalesforceGateway
from ...services.slack_verify import verify_slack_signature

# Import existing job handlers to keep behavior consistent
from .l2a import job_approve as l2a_job_approve
from .l2a import job_undo as l2a_job_undo
from .l2a import ApproveBody as L2AApproveBody

from .c2a import c2a_job_approve
from .c2a import c2a_job_undo
from .c2a import c2a_run, RunBody as C2ARunBody
from .c2a import c2a_safe_apply


router = APIRouter(prefix="/api/v2/slack", tags=["slack"])
log = logging.getLogger(__name__)


@dataclass
class SlackActor:
    slack_user_id: str
    user_id: str
    account_id: str
    role: str  # 'admin' | 'manager' | 'steward' | 'viewer'


async def _map_slack_user(slack_user_id: str) -> Optional[SlackActor]:
    """Map Slack user -> app user and role via email.

    - In development, returns a permissive dev mapping.
    - In other environments, attempts to call Slack API users.info to obtain email,
      then looks up User by email to determine account_id and role.
    """
    try:
        # Dev mode: default to admin of DEV_ACCOUNT_ID
        if (settings.ENV or "").lower() in ("dev", "development"):
            return SlackActor(
                slack_user_id=slack_user_id,
                user_id="dev-user",
                account_id=settings.DEV_ACCOUNT_ID,
                role="admin",
            )

        # Production lookup path
        token = settings.SLACK_BOT_TOKEN
        if not token:
            log.warning("Slack bot token not configured; cannot map Slack user")
            return None

        # Call Slack users.info to fetch email
        import httpx

        async with httpx.AsyncClient(timeout=10) as client:
            resp = await client.get(
                "https://slack.com/api/users.info",
                params={"user": slack_user_id},
                headers={"Authorization": f"Bearer {token}"},
            )
            data = resp.json()
        if not data.get("ok"):
            log.warning("Slack users.info failed: %s", data)
            return None
        profile = ((data or {}).get("user") or {}).get("profile") or {}
        email = (profile.get("email") or "").strip().lower()
        if not email:
            return None

        # Find User by email
        from sqlalchemy import select
        from ...db import AsyncSessionLocal
        from ...models import User

        async with AsyncSessionLocal() as db:
            row = (
                await db.execute(select(User).where(User.email == email))
            ).scalar_one_or_none()
            if not row:
                return None
            role = getattr(row, "role", None)
            role_str = (
                str(role.value) if hasattr(role, "value") else str(role or "")
            ).lower()
            return SlackActor(
                slack_user_id=slack_user_id,
                user_id=str(row.id),
                account_id=str(row.account_id or ""),
                role=role_str or "viewer",
            )
    except Exception:
        log.exception("Failed to map Slack user")
        return None


@router.post("/interact")
async def slack_interact(request: Request):
    """Slack interactivity handler (buttons, shortcuts, modals)."""
    raw = await request.body()
    verify_slack_signature(
        getattr(settings, "SLACK_SIGNING_SECRET", None), request.headers, raw
    )

    form = await request.form()
    payload_raw = form.get("payload") or "{}"
    try:
        payload = json.loads(payload_raw)
    except Exception:
        payload = {}

    user = payload.get("user", {}) or {}
    slack_user_id = user.get("id") or ""
    channel = (payload.get("channel") or {}).get("id") or None
    actions = payload.get("actions") or []
    action = actions[0] if actions else {}
    action_id = action.get("action_id") or ""

    # Map actor and enforce minimal RBAC server-side
    actor = await _map_slack_user(slack_user_id)
    if not actor:
        return JSONResponse({"text": "⚠️ Unable to map Slack user to app user"})

    # Route based on action
    value = action.get("value") or "{}"
    meta: dict[str, Any]
    try:
        meta = json.loads(value)
    except Exception:
        meta = {}

    kind = (meta.get("type") or "").lower()  # 'l2a' | 'c2a' | 'dedupe'
    job_id = meta.get("job_id") or meta.get("id") or ""
    op = (meta.get("op") or action_id or "").lower()

    # Approvals require elevated roles
    if op in {"approve", "undo", "approve_safe"} and actor.role not in {
        "admin",
        "manager",
    }:
        return JSONResponse(
            {"response_action": "errors", "errors": {"_": "Insufficient role"}}
        )

    try:
        # Provide a DB session for downstream calls
        async with AsyncSessionLocal() as db:
            sf: SalesforceGateway = await get_salesforce_gateway()

            # Idempotency guard (60s) per user/action (memory + optional Redis)
            key = _idem_key(kind, job_id or "-", op, actor.user_id)
            if not _idem_check(kind, job_id or "-", op, actor.user_id):
                return JSONResponse({"text": "⏳ Already processing that action…"})
            if not await _idem_check_dist(key, 60):
                return JSONResponse({"text": "⏳ Already processing that action…"})

            # Guard account scope for any job-scoped action
            if job_id and kind in {"l2a", "c2a"}:
                job_acc = await _resolve_job_account(kind, job_id, db)
                if not job_acc or str(job_acc) != str(actor.account_id):
                    return JSONResponse({"text": "⚠️ Wrong workspace for this job"})

            if kind == "l2a" and op == "approve" and job_id:
                res = await l2a_job_approve(
                    job_id=job_id,
                    body=L2AApproveBody(approve=True),
                    db=db,
                    account_id=actor.account_id,
                    user_id=actor.user_id,
                )
                # Audit: stamp approved_via if column exists
                try:
                    from ...model_defs.l2a_models import L2ABulkJob

                    row = await db.get(L2ABulkJob, job_id)
                    if row:
                        try:
                            setattr(row, "approved_via", "slack")
                            db.add(row)
                            await db.commit()
                        except Exception:
                            await db.rollback()
                except Exception:
                    pass
                return JSONResponse(
                    {"text": "✅ L2A job queued under org concurrency limits"}
                )

            if kind == "l2a" and op == "undo" and job_id:
                res = await l2a_job_undo(
                    job_id=job_id, db=db, account_id=actor.account_id, sf=sf
                )
                return JSONResponse(
                    {"text": f"↩️ L2A undo completed: {res.get('undone', 0)}"}
                )

            if kind == "c2a" and op == "approve" and job_id:
                res = await c2a_job_approve(
                    job_id=job_id, db=db, account_id=actor.account_id
                )
                # Audit: stamp approved metadata
                try:
                    from ...model_defs.c2a_models import C2AJob

                    j = await db.get(C2AJob, job_id)
                    if j:
                        try:
                            setattr(j, "approved_by", str(actor.user_id))
                            setattr(j, "approved_via", "slack")
                            db.add(j)
                            await db.commit()
                        except Exception:
                            await db.rollback()
                except Exception:
                    pass
                return JSONResponse(
                    {"text": "✅ C2A job queued under org concurrency limits"}
                )

            if kind == "c2a" and op == "undo" and job_id:
                res = await c2a_job_undo(
                    job_id=job_id, db=db, account_id=actor.account_id, sf=sf
                )
                return JSONResponse(
                    {"text": f"↩️ C2A undo completed: {res.get('undone', 0)}"}
                )

            # Report duplicate / bad data (lightweight triage)
            if action_id == "report_dupe" or op == "report_dupe":
                sf_id = meta.get("sf_id") or ""
                obj_type = meta.get("type") or ""
                notes = (payload.get("message") or {}).get("text") or ""
                await _create_data_issue(
                    account_id=actor.account_id,
                    user_id=actor.user_id,
                    source="slack",
                    sf_url=None,
                    sf_id=sf_id,
                    object_type=obj_type,
                    reason="duplicate",
                    notes=notes,
                )
                return JSONResponse(
                    {"text": "📝 Added to Ops Inbox. Thanks for reporting!"}
                )

            if action_id == "add_to_c2a" or op == "add_to_c2a":
                sf_id = meta.get("sf_id") or ""
                if not sf_id or not sf_id.startswith("003"):
                    return JSONResponse(
                        {"text": "⚠️ Please select a Contact to add to C2A."}
                    )
                # De-dupe micro-run per contact across processes if Redis available
                if not await _idem_check_dist(
                    _idem_key("c2a", sf_id, "micro", actor.user_id), 60
                ):
                    return JSONResponse({"text": "⏳ Already queued for review"})
                # Micro-run for single Contact via c2a_run(contact_ids=[sf_id])
                body = C2ARunBody(
                    threshold=0.7,
                    b2c_skip=True,
                    mode="assign",
                    sample_limit=None,
                    contact_ids=[sf_id],
                )
                _ = await c2a_run(
                    body=body, db=db, account_id=actor.account_id, user_id=actor.user_id
                )
                return JSONResponse({"text": "📌 Contact queued for C2A review"})

            # Pre-handle C2A safe-apply to also post a thread summary
            if op == "approve_safe" and kind == "c2a":
                min_score = float(
                    meta.get("min_score")
                    or getattr(settings, "SAFE_APPROVE_DEFAULT", 0.80)
                )
                res = await c2a_safe_apply(
                    min_score=min_score, db=db, account_id=actor.account_id, sf=sf
                )
                applied = int(res.get("applied", 0))
                errors = int(res.get("errors", 0))
                try:
                    channel = (payload.get("channel") or {}).get("id")
                    ts = (payload.get("message") or {}).get("ts") or (
                        payload.get("container") or {}
                    ).get("message_ts")
                    if channel and ts:
                        from .slack import _slack_api

                        await _slack_api(
                            "chat.postMessage",
                            {
                                "channel": channel,
                                "thread_ts": ts,
                                "text": f"✅ Safe-apply complete (C2A): *{applied}* applied, *{errors}* errors.",
                            },
                        )
                except Exception:
                    pass
                return JSONResponse(
                    {
                        "text": f"✅ Safe-apply complete for C2A: applied {applied}, errors {errors}."
                    }
                )

            if op == "approve_safe":
                min_score = float(
                    meta.get("min_score")
                    or getattr(settings, "SAFE_APPROVE_DEFAULT", 0.80)
                )
                if kind == "c2a":
                    res = await c2a_safe_apply(
                        min_score=min_score, db=db, account_id=actor.account_id, sf=sf
                    )
                    return JSONResponse(
                        {
                            "text": f"✅ Safe-apply complete for C2A: applied {res.get('applied',0)}, errors {res.get('errors',0)} (≥ {int(min_score*100)}%)."
                        }
                    )
                if kind == "l2a":
                    return JSONResponse({"text": "ℹ️ L2A safe-approve coming soon."})

    except Exception as e:
        # Log structured error and send ephemeral feedback when possible
        log.exception("Slack interaction failed: %s", str(e))
        # Attempt ephemeral post
        try:
            if channel and slack_user_id:
                await _post_ephemeral(
                    channel,
                    slack_user_id,
                    "⚠️ Something went wrong. Please try again or contact support.",
                )
        except Exception:
            pass
        return JSONResponse({"text": "⚠️ Request failed."})

    return JSONResponse({"text": "🤖 Unhandled action"})


async def _create_data_issue(
    *,
    account_id: str,
    user_id: str,
    source: str,
    sf_url: Optional[str],
    sf_id: Optional[str],
    object_type: Optional[str],
    reason: Optional[str],
    notes: Optional[str],
):
    from sqlalchemy import insert
    from datetime import datetime
    from ...db import AsyncSessionLocal
    from ...models import SQLModel

    # Write row using raw insert to avoid tight coupling if model evolves
    async with AsyncSessionLocal() as db:  # type: AsyncSession
        # If DataIssue model exists in models.py, prefer using it; else fallback to raw insert
        try:
            from ...models import DataIssue  # type: ignore

            row = DataIssue(
                account_id=account_id,
                user_id=user_id,
                source=source,
                sf_url=sf_url,
                sf_id=sf_id,
                object_type=object_type,
                reason=reason,
                notes=notes,
                status="open",
            )
            db.add(row)
            await db.commit()
            return
        except Exception:
            await db.rollback()
            # Fallback: try raw insert into data_issues if table exists
        try:
            await db.execute(
                insert(SQLModel.metadata.tables["data_issues"]).values(
                    account_id=account_id,
                    user_id=user_id,
                    source=source,
                    sf_url=sf_url,
                    sf_id=sf_id,
                    object_type=object_type,
                    reason=reason,
                    notes=notes,
                    status="open",
                    created_at=datetime.utcnow(),
                    updated_at=datetime.utcnow(),
                )
            )
            await db.commit()
        except Exception:
            log.exception("Failed to create data_issues row")


# ========== Helpers: idempotency, job account, ephemeral ==========
_IDEM: dict[str, float] = {}


def _idem_key(kind: str, job_id: str, op: str, user: str) -> str:
    return f"{kind}:{job_id}:{op}:{user}"


def _idem_check(kind: str, job_id: str, op: str, user: str, ttl: int = 60) -> bool:
    import time as _t

    now = _t.time()
    # purge old
    for kk, ts in list(_IDEM.items()):
        if now - ts > ttl:
            _IDEM.pop(kk, None)
    k = _idem_key(kind, job_id, op, user)
    if k in _IDEM:
        return False
    _IDEM[k] = now
    return True


async def _resolve_job_account(kind: str, job_id: str, db) -> Optional[str]:
    try:
        if kind == "l2a":
            from ...model_defs.l2a_models import L2ABulkJob

            row = await db.get(L2ABulkJob, job_id)
            return str(row.account_id) if row else None
        if kind == "c2a":
            from ...model_defs.c2a_models import C2AJob

            row = await db.get(C2AJob, job_id)
            return str(row.account_id) if row else None
    except Exception:
        return None
    return None


async def _post_ephemeral(channel: str, user: str, text: str) -> None:
    import httpx

    token = getattr(settings, "SLACK_BOT_TOKEN", None)
    if not token:
        return
    async with httpx.AsyncClient(timeout=10) as client:
        await client.post(
            "https://slack.com/api/chat.postEphemeral",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            },
            json={"channel": channel, "user": user, "text": text},
        )


async def _idem_check_dist(key: str, ttl: int = 60) -> bool:
    """Optional distributed idempotency via Redis. Returns True if acquired."""
    try:
        from ... import settings as saas_settings

        url = getattr(saas_settings, "REDIS_URL", None)
        if not url:
            return True
        import redis.asyncio as aioredis

        client = aioredis.from_url(url, encoding="utf-8", decode_responses=True)
        ok = await client.set(key, "1", nx=True, ex=int(ttl))
        return bool(ok)
    except Exception:
        return True
