# src/fmatch/saas/api/v2/slack.py
"""Slack integration API endpoints."""

from fastapi import APIRouter, HTTPException, Depends, Query, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import Dict, Optional, List
from datetime import datetime, timedelta
import os
import json
import httpx
import logging

logger = logging.getLogger(__name__)

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

# Mock storage for Slack configuration (in production, use database)
SLACK_CONFIG = {
    "connected": False,
    "team": None,
    "team_id": None,
    "channel": None,
    "access_token": None,
    "bot_user_id": None,
    "connected_at": None,
    "needs_reconnect": False,
}

SLACK_ROUTES = {
    "default_channel": "#general",
    "routes": {
        "match_run_complete": "",
        "duplicates_found": "",
        "data_quality_alert": "",
        "enrichment_opportunity": "",
    },
}


class SlackRoutes(BaseModel):
    default_channel: Optional[str] = ""
    routes: Optional[Dict[str, str]] = {}


class TestNotification(BaseModel):
    route_key: Optional[str] = "match_run_complete"
    channel: Optional[str] = None
    text: Optional[str] = None


async def send_to_slack(channel: str, text: str):
    """Actually send a message to Slack using the bot token."""
    if not SLACK_CONFIG.get("access_token"):
        logger.error("No Slack access token available")
        return False

    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://slack.com/api/chat.postMessage",
                headers={
                    "Authorization": f"Bearer {SLACK_CONFIG['access_token']}",
                    "Content-Type": "application/json",
                },
                json={"channel": channel, "text": text},
            )

            data = response.json()
            if data.get("ok"):
                logger.info(f"Message sent to Slack channel {channel}")
                return True
            else:
                logger.error(f"Slack API error: {data.get('error')}")
                return False

    except Exception as e:
        logger.error(f"Failed to send to Slack: {e}")
        return False


@router.get("/status")
async def get_slack_status():
    """Get current Slack connection status."""
    return SLACK_CONFIG


@router.get("/install")
async def start_slack_install():
    """Generate Slack OAuth URL for installation."""
    client_id = os.getenv("SLACK_CLIENT_ID")
    if not client_id or client_id == "your_actual_slack_client_id_here":
        logger.warning("SLACK_CLIENT_ID not configured properly in .env file")
        raise HTTPException(
            status_code=500,
            detail="Slack OAuth not configured. Please set SLACK_CLIENT_ID in .env file",
        )

    # Get redirect URI - if it's ngrok, append the callback path
    redirect_uri_base = os.getenv("SLACK_REDIRECT_URI", "http://localhost:8000")
    if not redirect_uri_base.endswith("/api/v2/slack/oauth/callback"):
        redirect_uri = f"{redirect_uri_base}/api/v2/slack/oauth/callback"
    else:
        redirect_uri = redirect_uri_base

    # REQUIRED: Must specify scopes for OAuth v2
    # Use only basic scopes that are commonly available
    auth_url = (
        f"https://slack.com/oauth/v2/authorize?"
        f"client_id={client_id}&"
        f"scope=chat:write,channels:read&"  # Minimal required scopes
        f"redirect_uri={redirect_uri}"
    )

    logger.info(f"Generated Slack OAuth URL with redirect: {redirect_uri}")
    return {"auth_url": auth_url}


@router.get("/routes")
async def get_slack_routes():
    """Get configured Slack channel routing."""
    return SLACK_ROUTES


@router.post("/routes")
async def save_slack_routes(request: Request):
    """Save Slack channel routing configuration - debug version."""
    global SLACK_ROUTES

    # Get the raw body and log it
    body = await request.body()
    logger.info(f"Raw request body: {body.decode('utf-8')}")

    try:
        # Parse the JSON
        data = json.loads(body)
        logger.info(f"Parsed JSON data: {data}")
        logger.info(
            f"Data type: {type(data)}, Keys: {data.keys() if isinstance(data, dict) else 'Not a dict'}"
        )

        # Extract the routes - be very flexible about the structure
        if isinstance(data, dict):
            # Direct structure or wrapped
            SLACK_ROUTES = {
                "default_channel": data.get("default_channel", "") or "",
                "routes": data.get("routes", {}) or {},
            }
        else:
            # Fallback to empty
            logger.warning(f"Unexpected data type: {type(data)}")
            SLACK_ROUTES = {"default_channel": "", "routes": {}}

        logger.info(f"Saved Slack routes: {SLACK_ROUTES}")

        return {
            "status": "saved",
            "message": "Routes saved successfully",
            "routes": SLACK_ROUTES,
        }

    except json.JSONDecodeError as e:
        logger.error(f"JSON decode error: {e}")
        logger.error(f"Body that failed to parse: {body}")
        raise HTTPException(status_code=422, detail=f"Invalid JSON: {str(e)}")
    except Exception as e:
        logger.error(f"Unexpected error in save_slack_routes: {e}")
        logger.error(f"Exception type: {type(e).__name__}")
        import traceback

        logger.error(f"Traceback: {traceback.format_exc()}")
        raise HTTPException(status_code=500, detail=str(e))


# COMMENTED OUT - Duplicate route, using more secure version on line 1305
# @router.post("/test")
# async def test_slack_notification(request: Request):
#     """Send a test notification to Slack - flexible version."""
#     # Get raw body for debugging
#     body = await request.body()
#     logger.info(f"Test notification raw body: {body.decode('utf-8')}")
#
#     try:
#         data = json.loads(body)
#         logger.info(f"Test notification data: {data}")

#         # Extract fields with defaults
#         route_key = data.get("route_key", "match_run_complete")
#         channel = data.get("channel")  # Can be None
#         text = data.get("text")  # Can be None
#
#         # Check if Slack is connected
#         if not SLACK_CONFIG.get("connected", False):
#             logger.warning("Slack is not connected, simulating test")
#             return {
#                 "message": "Slack not connected - test simulated",
#                 "status": "simulated",
#                 "route_key": route_key,
#                 "channel": channel or "#general",
#             }
#
#         # Determine the actual channel to use
#         if not channel:
#             # Use the route-specific channel or default
#             channel = SLACK_ROUTES.get("routes", {}).get(route_key)
#             if not channel:
#                 channel = SLACK_ROUTES.get("default_channel", "#general")
#
#         # Create test message based on route_key
#         messages = {
#             "match_run_complete": "✅ Test: Match run completed successfully",
#             "duplicates_found": "🔄 Test: Duplicates found in your data",
#             "data_quality_alert": "⚠️ Test: Data quality alert triggered",
#             "enrichment_opportunity": "💡 Test: Enrichment opportunity detected",
#         }
#
#         text_to_send = text or messages.get(
#             route_key, f"📧 Test notification for: {route_key}"
#         )
#
#         # Format the message with additional info
#         full_text = f"{text_to_send}\n\n_This is a test notification from FoundryMatch_"
#
#         # Actually send to Slack
#         success = await send_to_slack(channel, full_text)
#
#         if success:
#             return {
#                 "message": f"Test notification sent to {channel}",
#                 "status": "sent",
#                 "route_key": route_key,
#                 "channel": channel,
#             }
#         else:
#             # If sending failed, still return partial success but with warning
#             return {
#                 "message": f"Could not send to {channel}. Check if bot is invited to the channel.",
#                 "status": "partial",
#                 "route_key": route_key,
#                 "channel": channel,
#                 "error": "Bot may not have access to channel or token is invalid",
#             }
#
#     except json.JSONDecodeError as e:
#         logger.error(f"Invalid JSON in test notification: {e}")
#         raise HTTPException(status_code=422, detail=f"Invalid JSON: {str(e)}")
#     except Exception as e:
#         logger.error(f"Test notification error: {e}")
#         import traceback
#
#         logger.error(f"Traceback: {traceback.format_exc()}")
#         raise HTTPException(status_code=500, detail=str(e))


@router.get("/debug")
async def debug_slack_config():
    """Debug endpoint to check Slack configuration."""
    has_token = bool(SLACK_CONFIG.get("access_token"))
    token_prefix = SLACK_CONFIG.get("access_token", "")[:5] if has_token else "None"

    # Check if token looks valid
    token_type = "unknown"
    if token_prefix:
        if token_prefix.startswith("xoxb-"):
            token_type = "bot_token"
        elif token_prefix.startswith("xoxp-"):
            token_type = "user_token"
        elif token_prefix == "xoxb-":
            token_type = "demo_token"

    return {
        "connected": SLACK_CONFIG.get("connected", False),
        "has_token": has_token,
        "token_prefix": token_prefix,  # Should be "xoxb-" for bot token
        "token_type": token_type,
        "team": SLACK_CONFIG.get("team"),
        "team_id": SLACK_CONFIG.get("team_id"),
        "bot_user_id": SLACK_CONFIG.get("bot_user_id"),
        "connected_at": SLACK_CONFIG.get("connected_at"),
        "default_channel": SLACK_ROUTES.get("default_channel"),
        "routes": SLACK_ROUTES.get("routes", {}),
        "needs_reconnect": SLACK_CONFIG.get("needs_reconnect", False),
    }


@router.get("/test-connection")
async def test_slack_connection():
    """Test if the Slack API connection is working."""
    if not SLACK_CONFIG.get("access_token"):
        return {
            "status": "error",
            "message": "No access token available. Please connect to Slack first.",
            "connected": False,
        }

    try:
        # Test the connection using auth.test
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://slack.com/api/auth.test",
                headers={"Authorization": f"Bearer {SLACK_CONFIG['access_token']}"},
            )

            data = response.json()
            if data.get("ok"):
                return {
                    "status": "success",
                    "message": "Slack connection is working",
                    "connected": True,
                    "team": data.get("team"),
                    "user": data.get("user"),
                    "bot_id": data.get("bot_id"),
                    "url": data.get("url"),
                }
            else:
                return {
                    "status": "error",
                    "message": f"Slack API error: {data.get('error')}",
                    "connected": False,
                    "error": data.get("error"),
                }

    except Exception as e:
        logger.error(f"Failed to test Slack connection: {e}")
        return {
            "status": "error",
            "message": f"Connection test failed: {str(e)}",
            "connected": False,
        }


@router.get("/oauth/callback")
async def slack_oauth_callback(
    code: str = Query(..., description="OAuth authorization code from Slack"),
    state: Optional[str] = Query(None, description="OAuth state parameter"),
):
    """Handle Slack OAuth callback and exchange code for access token."""
    client_id = os.getenv("SLACK_CLIENT_ID")
    client_secret = os.getenv("SLACK_CLIENT_SECRET")

    # Get redirect URI - handle ngrok URLs properly
    redirect_uri_base = os.getenv("SLACK_REDIRECT_URI", "http://localhost:8000")
    if not redirect_uri_base.endswith("/api/v2/slack/oauth/callback"):
        redirect_uri = f"{redirect_uri_base}/api/v2/slack/oauth/callback"
    else:
        redirect_uri = redirect_uri_base

    if not client_id or not client_secret:
        logger.error("Slack OAuth credentials not configured")
        frontend_url = os.getenv("FRONTEND_ORIGIN", "http://localhost:3004")
        return HTMLResponse(
            content=f"""
            <html>
            <head>
                <title>Slack Configuration Error</title>
                <style>
                    body {{
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                        text-align: center;
                        padding: 50px;
                        background: #f5f5f5;
                    }}
                    .error {{
                        color: #d32f2f;
                        font-size: 20px;
                        margin: 20px 0;
                    }}
                </style>
            </head>
            <body>
                <h2 class="error">✗ OAuth Configuration Missing</h2>
                <p>Please configure Slack OAuth credentials in your .env file</p>
                <p>Redirecting...</p>
                <script>
                    setTimeout(() => {{
                        window.location.href = '{frontend_url}/settings/integrations?error=oauth_config_missing';
                    }}, 2000);
                </script>
            </body>
            </html>
        """
        )

    if client_id == "your_actual_slack_client_id_here":
        logger.warning("Using placeholder Slack credentials")
        # For development, just mark as connected with dummy data
        global SLACK_CONFIG
        SLACK_CONFIG = {
            "connected": True,
            "team": "Demo Team (Dev Mode)",
            "team_id": "T_DEMO",
            "channel": "#general",
            "access_token": "xoxb-demo-token",
            "bot_user_id": "U_DEMO",
            "connected_at": datetime.utcnow().isoformat(),
            "needs_reconnect": False,
        }
        frontend_url = os.getenv("FRONTEND_ORIGIN", "http://localhost:3004")
        return HTMLResponse(
            content=f"""
            <html>
            <head>
                <title>Slack Connected (Dev Mode)</title>
                <style>
                    body {{
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                        text-align: center;
                        padding: 50px;
                        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                        color: white;
                        height: 100vh;
                        margin: 0;
                        display: flex;
                        flex-direction: column;
                        justify-content: center;
                        align-items: center;
                    }}
                    .container {{
                        background: white;
                        border-radius: 12px;
                        padding: 40px;
                        box-shadow: 0 20px 40px rgba(0,0,0,0.1);
                        max-width: 400px;
                        color: #333;
                    }}
                    h2 {{
                        color: #FFA500;
                        margin: 0 0 10px 0;
                    }}
                    .button {{
                        display: inline-block;
                        margin-top: 30px;
                        padding: 12px 30px;
                        background: #FFA500;
                        color: white;
                        text-decoration: none;
                        border-radius: 6px;
                        font-weight: 600;
                    }}
                    .button:hover {{
                        background: #FF8C00;
                    }}
                </style>
            </head>
            <body>
                <div class="container" id="container">
                    <h2>⚡ Connected to Slack (Dev Mode)</h2>
                    <p><strong>Workspace:</strong> {SLACK_CONFIG['team']}</p>
                    <p style="color: #888; font-size: 14px;">Using placeholder credentials for development</p>
                    <p id="status" style="margin-top: 20px;">Closing window...</p>
                </div>
                <script>
                    // Check if this is a popup window
                    if (window.opener) {{
                        // Send message to parent window
                        try {{
                            window.opener.postMessage({{
                                type: 'slack_connected',
                                workspace: '{SLACK_CONFIG['team']}',
                                dev_mode: true
                            }}, '*');
                            console.log('Sent slack_connected (dev mode) message to opener');
                            document.getElementById('status').textContent = 'Success! You can close this window.';
                            setTimeout(() => {{
                                window.close();
                            }}, 2000);
                        }} catch (e) {{
                            console.error('Error sending message to opener:', e);
                            document.getElementById('status').style.display = 'none';
                            document.getElementById('container').innerHTML +=
                                '<a href="{frontend_url}/settings/integrations?slack_connected=true" class="button">Return to FoundryMatch</a>';
                        }}
                    }} else {{
                        // Not a popup - show return button
                        document.getElementById('status').style.display = 'none';
                        document.getElementById('container').innerHTML +=
                            '<a href="{frontend_url}/settings/integrations?slack_connected=true" class="button">Return to FoundryMatch</a>';
                    }}
                </script>
            </body>
            </html>
        """
        )

    try:
        # Exchange code for access token
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://slack.com/api/oauth.v2.access",
                data={
                    "client_id": client_id,
                    "client_secret": client_secret,
                    "code": code,
                    "redirect_uri": redirect_uri,
                },
            )
            data = response.json()

        if data.get("ok"):
            # Store the token and team info
            SLACK_CONFIG = {
                "connected": True,
                "team": data.get("team", {}).get("name", "Unknown Team"),
                "team_id": data.get("team", {}).get("id"),
                "channel": "#general",
                "access_token": data.get("access_token"),
                "bot_user_id": data.get("bot_user_id"),
                "connected_at": datetime.utcnow().isoformat(),
                "needs_reconnect": False,
            }
            logger.info(
                f"Successfully connected to Slack workspace: {SLACK_CONFIG['team']}"
            )

            # Use HTML response with postMessage for popup windows
            frontend_url = os.getenv("FRONTEND_ORIGIN", "http://localhost:3004")
            return HTMLResponse(
                content=f"""
                <html>
                <head>
                    <title>Slack Connected</title>
                    <style>
                        body {{
                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                            text-align: center;
                            padding: 50px;
                            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                            color: white;
                            height: 100vh;
                            margin: 0;
                            display: flex;
                            flex-direction: column;
                            justify-content: center;
                            align-items: center;
                        }}
                        .container {{
                            background: white;
                            border-radius: 12px;
                            padding: 40px;
                            box-shadow: 0 20px 40px rgba(0,0,0,0.1);
                            max-width: 400px;
                            color: #333;
                        }}
                        h2 {{
                            color: #4CAF50;
                            margin: 0 0 10px 0;
                            font-size: 24px;
                        }}
                        .workspace {{
                            font-size: 18px;
                            font-weight: 600;
                            color: #555;
                            margin: 15px 0;
                        }}
                        .status {{
                            color: #888;
                            font-size: 14px;
                            margin-top: 20px;
                        }}
                        .button {{
                            display: inline-block;
                            margin-top: 30px;
                            padding: 12px 30px;
                            background: #4CAF50;
                            color: white;
                            text-decoration: none;
                            border-radius: 6px;
                            font-weight: 600;
                            transition: background 0.3s;
                        }}
                        .button:hover {{
                            background: #45a049;
                        }}
                        .spinner {{
                            border: 3px solid #f3f3f3;
                            border-top: 3px solid #4CAF50;
                            border-radius: 50%;
                            width: 30px;
                            height: 30px;
                            animation: spin 1s linear infinite;
                            margin: 20px auto 0;
                        }}
                        @keyframes spin {{
                            0% {{ transform: rotate(0deg); }}
                            100% {{ transform: rotate(360deg); }}
                        }}
                    </style>
                </head>
                <body>
                    <div class="container" id="container">
                        <h2>✓ Connected to Slack!</h2>
                        <p class="workspace">Workspace: {SLACK_CONFIG['team']}</p>
                        <p class="status" id="status">Closing window...</p>
                        <div class="spinner" id="spinner"></div>
                    </div>
                    <script>
                        // Check if this is a popup window
                        if (window.opener) {{
                            // Send message to parent window
                            try {{
                                window.opener.postMessage({{
                                    type: 'slack_connected',
                                    workspace: '{SLACK_CONFIG['team']}',
                                    team_id: '{SLACK_CONFIG.get('team_id', '')}'
                                }}, '*');
                                console.log('Sent slack_connected message to opener');

                                // Update status
                                document.getElementById('status').textContent = 'Success! You can close this window.';

                                // Auto-close after 2 seconds
                                setTimeout(() => {{
                                    window.close();
                                }}, 2000);
                            }} catch (e) {{
                                console.error('Error sending message to opener:', e);
                                // Show fallback button if postMessage fails
                                document.getElementById('status').style.display = 'none';
                                document.getElementById('spinner').style.display = 'none';
                                document.getElementById('container').innerHTML +=
                                    '<a href="{frontend_url}/settings/integrations?slack_connected=true" class="button">Return to FoundryMatch</a>';
                            }}
                        }} else {{
                            // Not a popup - show return button
                            console.log('Not a popup window, showing return button');
                            document.getElementById('status').style.display = 'none';
                            document.getElementById('spinner').style.display = 'none';
                            document.getElementById('container').innerHTML +=
                                '<a href="{frontend_url}/settings/integrations?slack_connected=true" class="button">Return to FoundryMatch</a>';
                        }}
                    </script>
                </body>
                </html>
            """
            )
        else:
            error = data.get("error", "unknown_error")
            logger.error(f"Slack OAuth error: {error}")
            frontend_url = os.getenv("FRONTEND_ORIGIN", "http://localhost:3004")
            return HTMLResponse(
                content=f"""
                <html>
                <head>
                    <title>Slack Connection Error</title>
                    <style>
                        body {{
                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                            text-align: center;
                            padding: 50px;
                            background: #f5f5f5;
                        }}
                        .error {{
                            color: #d32f2f;
                            font-size: 20px;
                            margin: 20px 0;
                        }}
                        .error-detail {{
                            color: #666;
                            margin-top: 10px;
                        }}
                    </style>
                </head>
                <body>
                    <h2 class="error">✗ Slack Connection Failed</h2>
                    <p class="error-detail">Error: {error}</p>
                    <p>Redirecting...</p>
                    <script>
                        setTimeout(() => {{
                            window.location.href = '{frontend_url}/settings/integrations?error={error}';
                        }}, 2000);
                    </script>
                </body>
                </html>
            """
            )

    except Exception as e:
        logger.error(f"Error during Slack OAuth: {str(e)}")
        frontend_url = os.getenv("FRONTEND_ORIGIN", "http://localhost:3004")
        return HTMLResponse(
            content=f"""
            <html>
            <head>
                <title>Slack OAuth Failed</title>
                <style>
                    body {{
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                        text-align: center;
                        padding: 50px;
                        background: #f5f5f5;
                    }}
                    .error {{
                        color: #d32f2f;
                        font-size: 20px;
                        margin: 20px 0;
                    }}
                </style>
            </head>
            <body>
                <h2 class="error">✗ OAuth Failed</h2>
                <p>Failed to complete Slack authentication. Please try again.</p>
                <p>Redirecting...</p>
                <script>
                    setTimeout(() => {{
                        window.location.href = '{frontend_url}/settings/integrations?error=oauth_failed';
                    }}, 2000);
                </script>
            </body>
            </html>
        """
        )


# ========== Slack Events (link unfurls) and Slash Commands ==========
from fastapi.responses import JSONResponse
from ...services.slack_verify import verify_slack_signature
from ...auth_security import require_account, require_user
from .l2a import make_role_dependency
from ...db import get_session
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete
from ...models import SlackSubscription
from ...models import SlackPrefs
from ...models import DataIssue

require_admin_or_manager = make_role_dependency("admin", "manager")


async def _slack_api(method: str, payload: dict) -> dict:
    """Call Slack Web API with the stored bot token."""
    token = SLACK_CONFIG.get("access_token") or os.getenv("SLACK_BOT_TOKEN")
    if not token:
        raise RuntimeError("Slack bot token not configured")
    import httpx

    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.post(
            f"https://slack.com/api/{method}",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            },
            json=payload,
        )
        data = r.json()
        return data


async def _chat_unfurl(channel: str, ts: str, unfurls: dict) -> None:
    try:
        data = await _slack_api(
            "chat.unfurl", {"channel": channel, "ts": ts, "unfurls": unfurls}
        )
        if not data.get("ok"):
            logger.warning("Slack chat.unfurl error: %s", data)
    except Exception:
        logger.exception("chat.unfurl failed")


import asyncio
import random

SLACK_METRICS = {"post_ok": 0, "post_429": 0, "post_err": 0}


async def _chat_post_message(channel: str, blocks: dict) -> None:
    tries = 0
    while True:
        tries += 1
        data = await _slack_api("chat.postMessage", {"channel": channel, **blocks})
        if data.get("ok"):
            SLACK_METRICS["post_ok"] += 1
            return
        # track metrics
        if data.get("error") in ("ratelimited", "rate_limited"):
            SLACK_METRICS["post_429"] += 1
        else:
            SLACK_METRICS["post_err"] += 1
        if tries >= 3:
            logger.warning("postMessage failed: %s", data)
            return
        await asyncio.sleep(min(4, 0.5 * tries + random.random()))


async def publish(
    topic: str, account_id: str, blocks: dict, db: AsyncSession | None = None
) -> None:
    """Publish a Block Kit message to all subscribed channels for a topic."""
    try:
        close_db = False
        if db is None:
            from ...db import AsyncSessionLocal

            db = AsyncSessionLocal()
            close_db = True
        team = SLACK_CONFIG.get("team_id")
        q = select(SlackSubscription).where(
            SlackSubscription.account_id == str(account_id),
            SlackSubscription.topic == str(topic),
        )
        if team:
            q = q.where(SlackSubscription.team_id == team)
        rows = (await db.execute(q)).scalars().all()
        for r in rows:
            try:
                await _chat_post_message(r.channel_id, blocks)
            except Exception:
                logger.exception("publish failed for channel=%s", r.channel_id)
        if close_db:
            await db.close()  # type: ignore
    except Exception:
        logger.exception("publish failed")


async def _build_unfurls(urls: list[str]) -> dict:
    from ...services.salesforce_gateway import get_salesforce_gateway
    import re

    def _parse_sf_id(u: str) -> tuple[Optional[str], Optional[str]]:
        try:
            # Accept any 15/18-char ID
            m = re.search(r"([a-zA-Z0-9]{15,18})", u)
            if not m:
                return None, None
            rid = m.group(1)
            if rid.startswith("00Q"):
                return rid, "Lead"
            if rid.startswith("003"):
                return rid, "Contact"
            if rid.startswith("001"):
                return rid, "Account"
            return rid, None
        except Exception:
            return None, None

    sf = await get_salesforce_gateway()
    out: dict = {}
    for u in urls or []:
        rid, rtype = _parse_sf_id(u)
        if not rid:
            continue
        title = None
        subtitle = None
        try:
            if rtype == "Lead":
                rec = await sf.get("Lead", rid)
                title = f"Lead: {(rec.get('Name') or '').strip()}"
                subtitle = (rec.get("Company") or rec.get("Email") or "").strip()
            elif rtype == "Contact":
                rec = await sf.get("Contact", rid)
                title = f"Contact: {(rec.get('FirstName') or '')} {(rec.get('LastName') or '')}".strip()
                subtitle = (rec.get("Email") or rec.get("AccountId") or "").strip()
            elif rtype == "Account":
                rec = await sf.get("Account", rid)
                title = f"Account: {(rec.get('Name') or '').strip()}"
                subtitle = (rec.get("Website") or rec.get("Industry") or "").strip()
            else:
                title = f"Salesforce Record {rid}"
        except Exception:
            title = f"Salesforce Record {rid}"
            subtitle = None

        blocks = [
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": f"*{title}* <{u}|Open>"},
            },
        ]
        if subtitle:
            blocks.append(
                {"type": "context", "elements": [{"type": "mrkdwn", "text": subtitle}]}
            )
        # Actions
        blocks.append(
            {
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "Report duplicate"},
                        "action_id": "report_dupe",
                        "value": json.dumps({"sf_id": rid, "type": rtype or "record"}),
                    },
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "Add to C2A review"},
                        "action_id": "add_to_c2a",
                        "value": json.dumps({"sf_id": rid}),
                    },
                ],
            }
        )
        out[u] = {"blocks": blocks}
    return out


@router.post("/events")
async def slack_events(request: Request):
    raw = await request.body()
    from ... import settings as saas_settings

    verify_slack_signature(
        getattr(saas_settings, "SLACK_SIGNING_SECRET", None), request.headers, raw
    )
    evt = await request.json()
    if (evt or {}).get("type") == "url_verification":
        return {"challenge": evt.get("challenge")}
    if (evt or {}).get("type") == "event_callback":
        e = evt.get("event", {}) or {}
        if e.get("type") == "link_shared":
            urls = [
                l.get("url")
                for l in (e.get("links") or [])
                if l.get("url") and _is_salesforce_url(l.get("url", ""))
            ]
            if urls:
                unfurls = await _build_unfurls(urls)
                await _chat_unfurl(
                    channel=str(e.get("channel")),
                    ts=str(e.get("message_ts")),
                    unfurls=unfurls,
                )
        elif e.get("type") == "app_home_opened":
            try:
                user = str(e.get("user"))
                from ...services.slack_usermap import map_slack_user

                actor = await map_slack_user(user)
                if actor and getattr(actor, "account_id", None):
                    view = await _build_home_view(str(actor.account_id))
                    await _slack_api("views.publish", {"user_id": user, "view": view})
            except Exception:
                logger.exception("home publish failed")
    return {"ok": True}


# Restrict unfurls to Salesforce domains
ALLOWED_SF_DOMAINS = ("salesforce.com", ".my.salesforce.com", ".lightning.force.com")


def _is_salesforce_url(url: str) -> bool:
    from urllib.parse import urlparse

    try:
        host = urlparse(url).netloc.lower()
        return any(host.endswith(d) for d in ALLOWED_SF_DOMAINS)
    except Exception:
        return False


async def _build_home_view(account_id: str) -> dict:
    from ...db import AsyncSessionLocal
    from ...model_defs.l2a_models import L2ABulkJob
    from ...model_defs.c2a_models import C2AJob, C2ASuggestion
    from ...models import User
    from ... import settings as saas_settings
    from sqlalchemy import select, func

    blocks = [
        {"type": "section", "text": {"type": "mrkdwn", "text": "*FoundryOps Home*"}},
        {"type": "divider"},
    ]

    # TTL cutoff for showing Undo for C2A jobs
    TTL_H = int(getattr(saas_settings, "C2A_UNDO_TTL_HOURS", 24))
    cutoff = datetime.utcnow() - timedelta(hours=TTL_H)

    def _undo_button(kind, j):
        return {
            "type": "button",
            "text": {"type": "plain_text", "text": "Undo"},
            "action_id": "undo",
            "value": json.dumps({"type": kind, "job_id": str(j.id), "op": "undo"}),
        }

    async def _email_for_user(db, user_id: str) -> str | None:
        try:
            row = (
                await db.execute(select(User).where(User.id == user_id))
            ).scalar_one_or_none()
            return getattr(row, "email", None)
        except Exception:
            return None

    async with AsyncSessionLocal() as db:
        # Waiting approvals (L2A + C2A)
        try:
            l2a_wait = (
                (
                    await db.execute(
                        select(L2ABulkJob)
                        .where(
                            L2ABulkJob.account_id == str(account_id),
                            L2ABulkJob.status == "waiting_approval",
                        )
                        .order_by(L2ABulkJob.created_at.desc())
                        .limit(3)
                    )
                )
                .scalars()
                .all()
            )
        except Exception:
            l2a_wait = []
        try:
            c2a_wait = (
                (
                    await db.execute(
                        select(C2AJob)
                        .where(
                            C2AJob.account_id == str(account_id),
                            C2AJob.status == "waiting_approval",
                        )
                        .order_by(C2AJob.created_at.desc())
                        .limit(3)
                    )
                )
                .scalars()
                .all()
            )
        except Exception:
            c2a_wait = []
        if l2a_wait or c2a_wait:
            blocks += [
                {"type": "section", "text": {"type": "mrkdwn", "text": "*Approvals*"}}
            ]
            for j in l2a_wait + c2a_wait:
                try:
                    kind = "l2a" if hasattr(j, "applied") else "c2a"
                    req_email = None
                    try:
                        req_email = await _email_for_user(
                            db, str(getattr(j, "requested_by", "") or "")
                        )
                    except Exception:
                        req_email = None
                    who = f" • requested by {req_email}" if req_email else ""
                    total = getattr(j, "total", None)
                    meta = (
                        f" • total {total}"
                        if isinstance(total, int) and total is not None and total >= 0
                        else ""
                    )
                    blocks += [
                        {
                            "type": "section",
                            "text": {
                                "type": "mrkdwn",
                                "text": f"`{str(j.id)[:8]}…` — {kind.upper()} waiting approval{meta}{who}",
                            },
                            "accessory": {
                                "type": "button",
                                "style": "primary",
                                "action_id": "approve",
                                "text": {"type": "plain_text", "text": "Approve"},
                                "value": json.dumps(
                                    {"type": kind, "job_id": str(j.id), "op": "approve"}
                                ),
                            },
                        }
                    ]
                except Exception:
                    continue
            blocks.append({"type": "divider"})

        # Recent jobs (last 3 across kinds)
        rows = []
        try:
            rows += (
                (
                    await db.execute(
                        select(L2ABulkJob)
                        .where(L2ABulkJob.account_id == str(account_id))
                        .order_by(L2ABulkJob.updated_at.desc())
                        .limit(3)
                    )
                )
                .scalars()
                .all()
            )
        except Exception:
            pass
        try:
            rows += (
                (
                    await db.execute(
                        select(C2AJob)
                        .where(C2AJob.account_id == str(account_id))
                        .order_by(C2AJob.updated_at.desc())
                        .limit(3)
                    )
                )
                .scalars()
                .all()
            )
        except Exception:
            pass
        if rows:
            blocks += [
                {"type": "section", "text": {"type": "mrkdwn", "text": "*Recent Jobs*"}}
            ]
            try:
                rows = sorted(
                    rows,
                    key=lambda r: getattr(r, "updated_at", None)
                    or getattr(r, "created_at", None),
                    reverse=True,
                )[:3]
            except Exception:
                rows = rows[:3]
            for j in rows:
                kind = "l2a" if hasattr(j, "applied") else "c2a"
                line = {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": f"`{str(j.id)[:8]}…` — {kind.upper()} • {getattr(j, 'status', '')}",
                    },
                }
                if kind == "c2a" and (getattr(j, "status", "") == "completed"):
                    linked = 0
                    try:
                        linked = (
                            await db.execute(
                                select(func.count())
                                .select_from(C2ASuggestion)
                                .where(
                                    C2ASuggestion.job_id == str(j.id),
                                    C2ASuggestion.status == "linked",
                                    C2ASuggestion.updated_at >= cutoff,
                                )
                            )
                        ).scalar_one()
                    except Exception:
                        linked = 0
                    if linked > 0:
                        line["accessory"] = _undo_button("c2a", j)
                blocks += [line]
            blocks.append({"type": "divider"})

        # Data issues (open count)
        open_count = 0
        try:
            open_count = (
                await db.execute(
                    select(func.count())
                    .select_from(DataIssue)
                    .where(
                        DataIssue.account_id == str(account_id),
                        DataIssue.status == "open",
                    )
                )
            ).scalar_one()
        except Exception:
            open_count = 0
        blocks += [
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": f"*Data Issues:* {open_count} open"},
            }
        ]
    return {"type": "home", "blocks": blocks}


@router.post("/commands")
async def slack_commands(request: Request):
    raw = await request.body()
    from ... import settings as saas_settings

    verify_slack_signature(
        getattr(saas_settings, "SLACK_SIGNING_SECRET", None), request.headers, raw
    )
    form = await request.form()
    command = (form.get("command") or "").strip()
    text = (form.get("text") or "").strip()
    user_id = (form.get("user_id") or "").strip()
    channel_id = (form.get("channel_id") or "").strip()
    team_id = (form.get("team_id") or "").strip()

    # Single command namespace e.g., /foundry
    try:
        parts = text.split()
        sub = (parts[0] if parts else "").lower()
        if sub == "approve" and len(parts) >= 2:
            job_id = parts[1]
            kind = (parts[2] if len(parts) >= 3 else "l2a").lower()
            await _slack_api(
                "chat.postEphemeral",
                {
                    "channel": channel_id,
                    "user": user_id,
                    "text": f"Queued approval for {kind} job {job_id[:8]}…",
                },
            )
            return JSONResponse({"response_type": "ephemeral", "text": "Processing…"})
        if sub in ("subscribe", "unsubscribe") and len(parts) >= 2:
            topic = parts[1].lower()
            # Tenant scoping: require a token; use require_account via a quick call path
            # Open a DB session for persistence
            from ...db import AsyncSessionLocal

            async with AsyncSessionLocal() as db:
                if sub == "subscribe":
                    rec = SlackSubscription(
                        account_id="dev",
                        channel_id=str(channel_id),
                        topic=str(topic),
                        team_id=team_id or SLACK_CONFIG.get("team_id"),
                    )
                    # Try to get real account via token-less path? For slash commands, we don't have app auth.
                    # Persist best-effort under unknown; real API usage should call /subscribe endpoint.
                    try:
                        db.add(rec)
                        await db.commit()
                    except Exception:
                        await db.rollback()
                    await _slack_api(
                        "chat.postEphemeral",
                        {
                            "channel": channel_id,
                            "user": user_id,
                            "text": f"Subscribed this channel to {topic}",
                        },
                    )
                    return JSONResponse(
                        {"response_type": "ephemeral", "text": f"Subscribed to {topic}"}
                    )
                else:
                    try:
                        await db.execute(
                            delete(SlackSubscription).where(
                                SlackSubscription.channel_id == str(channel_id),
                                SlackSubscription.topic == str(topic),
                            )
                        )
                        await db.commit()
                    except Exception:
                        await db.rollback()
                    await _slack_api(
                        "chat.postEphemeral",
                        {
                            "channel": channel_id,
                            "user": user_id,
                            "text": f"Unsubscribed this channel from {topic}",
                        },
                    )
                    return JSONResponse(
                        {
                            "response_type": "ephemeral",
                            "text": f"Unsubscribed from {topic}",
                        }
                    )
        if sub == "report":
            return JSONResponse(
                {
                    "response_type": "ephemeral",
                    "text": "Use the message shortcut to report bad data.",
                }
            )
    except Exception:
        logger.exception("/commands handler failed")
    return JSONResponse({"response_type": "ephemeral", "text": "Unknown command"})


@router.get("/metrics", dependencies=[Depends(require_admin_or_manager)])
async def slack_metrics():
    return dict(SLACK_METRICS)


# ===== Subscriptions List =====
@router.get("/subscriptions")
async def slack_subscriptions_list(
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    rows = (
        (
            await db.execute(
                select(SlackSubscription).where(
                    SlackSubscription.account_id == str(account_id)
                )
            )
        )
        .scalars()
        .all()
    )
    out: List[dict] = []
    for r in rows:
        out.append(
            {
                "channel_id": r.channel_id,
                "topic": r.topic,
                "team_id": getattr(r, "team_id", None),
            }
        )
    return out


@router.get("/prefs")
async def slack_prefs_get(
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    row = (
        await db.execute(
            select(SlackPrefs).where(SlackPrefs.account_id == str(account_id)).limit(1)
        )
    ).scalar_one_or_none()
    if not row:
        return {
            "digest_enabled": True,
            "digest_day": "mon",
            "digest_hour_utc": 9,
            "c2a_safe_threshold": 0.80,
        }
    return {
        "digest_enabled": bool(getattr(row, "digest_enabled", True)),
        "digest_day": getattr(row, "digest_day", "mon"),
        "digest_hour_utc": int(getattr(row, "digest_hour_utc", 9)),
        "c2a_safe_threshold": float(getattr(row, "c2a_safe_threshold", 0.80) or 0.80),
    }


@router.post("/prefs", dependencies=[Depends(require_admin_or_manager)])
async def slack_prefs_set(
    body: dict,
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    # upsert preferences
    row = (
        await db.execute(
            select(SlackPrefs).where(SlackPrefs.account_id == str(account_id)).limit(1)
        )
    ).scalar_one_or_none()
    if not row:
        row = SlackPrefs(account_id=str(account_id))
    try:
        if isinstance(body, dict):
            if "digest_enabled" in body:
                row.digest_enabled = bool(body.get("digest_enabled"))
            if "digest_day" in body:
                row.digest_day = str(body.get("digest_day") or "mon")
            if "digest_hour_utc" in body:
                row.digest_hour_utc = int(body.get("digest_hour_utc") or 9)
            if "c2a_safe_threshold" in body:
                row.c2a_safe_threshold = float(body.get("c2a_safe_threshold") or 0.80)
        db.add(row)
        await db.commit()
        return {"ok": True}
    except Exception:
        await db.rollback()
        raise HTTPException(400, "Invalid prefs payload")


@router.post("/subscribe")
async def slack_subscribe(
    body: dict,
    account_id: str | None = Depends(require_account),
    user_id: str | None = Depends(require_user),
    db: AsyncSession | None = Depends(get_session),
):
    """Persist channel subscription for a topic."""
    try:
        channel_id = (body.get("channel_id") if isinstance(body, dict) else None) or ""
        topic = (body.get("topic") if isinstance(body, dict) else None) or ""
        if not (channel_id and topic):
            raise HTTPException(422, "channel_id and topic required")
        # Upsert unique (account_id, channel_id, topic)
        team_id = None
        try:
            team_id = (
                body.get("team_id") if isinstance(body, dict) else None
            ) or SLACK_CONFIG.get("team_id")
        except Exception:
            team_id = SLACK_CONFIG.get("team_id")
        rec = SlackSubscription(
            account_id=str(account_id),
            channel_id=str(channel_id),
            topic=str(topic),
            team_id=team_id,
        )
        db.add(rec)
        try:
            await db.commit()
        except Exception:
            await db.rollback()
            # Likely unique constraint - ignore as idempotent
        return {"ok": True}
    except HTTPException:
        raise
    except Exception as e:
        logger.exception("subscribe failed: %s", str(e))
        raise HTTPException(500, "subscribe failed")


@router.post("/unsubscribe")
async def slack_unsubscribe(
    body: dict,
    account_id: str | None = Depends(require_account),
    user_id: str | None = Depends(require_user),
    db: AsyncSession | None = Depends(get_session),
):
    try:
        channel_id = (body.get("channel_id") if isinstance(body, dict) else None) or ""
        topic = (body.get("topic") if isinstance(body, dict) else None) or ""
        if not (channel_id and topic):
            raise HTTPException(422, "channel_id and topic required")
        await db.execute(
            delete(SlackSubscription).where(
                SlackSubscription.account_id == str(account_id),
                SlackSubscription.channel_id == str(channel_id),
                SlackSubscription.topic == str(topic),
            )
        )
        await db.commit()
        return {"ok": True}
    except HTTPException:
        raise
    except Exception as e:
        logger.exception("unsubscribe failed: %s", str(e))
        raise HTTPException(500, "unsubscribe failed")


# ===== Test & Digest Preview (admin/manager) =====
@router.post("/test", dependencies=[Depends(require_admin_or_manager)])
async def slack_test(
    body: dict,
    account_id: str = Depends(require_account),
):
    channel_id = (body or {}).get("channel_id")
    text = (body or {}).get("text") or "✅ Test: FoundryMatch is connected."
    if not channel_id:
        raise HTTPException(422, "channel_id required")
    await _chat_post_message(channel_id, {"text": text})
    return {"ok": True}


@router.post("/digest/preview", dependencies=[Depends(require_admin_or_manager)])
async def slack_digest_preview(
    body: dict,
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    channel_id = (body or {}).get("channel_id")
    if not channel_id:
        raise HTTPException(422, "channel_id required")
    from ...scheduler import _build_digest_blocks

    now = datetime.utcnow()
    blocks = await _build_digest_blocks(db, str(account_id), now - timedelta(days=7))
    if not blocks:
        raise HTTPException(400, "No digest content for this tenant")
    await _chat_post_message(channel_id, blocks)
    return {"ok": True}
