from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Header, Query, Body
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import StaleDataError
from typing import Optional, List
from ..db import get_session
from ..auth_core import require_admin, get_current_account
from .schemas import (
    RouteRequest,
    RouteResponse,
    PoolCreate,
    PoolMemberUpsert,
    RuleUpsert,
    LeadIn,
)
from .models import (
    RoutingPool,
    RoutingPoolMember,
    RoutingRule,
    RoutingDecision,
    RoutingPoolState,
)
from .engine import route_lead
from .adapters.salesforce import SalesforceAdapter
from ..services.salesforce_gateway import SalesforceGateway
from .slack import post_decision
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import logging
import json
import uuid
import hashlib
import time
from ..models import Integration, IntegrationProvider
from sqlalchemy import select

router = APIRouter(prefix="/api/v2/routing", tags=["routing"])
admin_router = APIRouter(
    prefix="/api/v2/routing/admin",
    tags=["routing-admin"],
    dependencies=[Depends(require_admin)],
)
logger = logging.getLogger(__name__)


async def get_salesforce_gateway_for_tenant(
    db: Session, tenant_id: str
) -> Optional[SalesforceGateway]:
    """Get Salesforce gateway for the given tenant"""
    # Ensure an integration exists
    result = await db.execute(
        select(Integration).where(
            Integration.account_id == tenant_id,
            Integration.provider == IntegrationProvider.SALESFORCE,
        )
    )
    integration = result.scalar_one_or_none()
    if not integration:
        return None
    return await SalesforceGateway.for_tenant(tenant_id, db)


def validate_tenant_access(tenant_id: str, current_account: dict) -> bool:
    """Validate that the current account has access to the specified tenant"""
    # For multi-tenant systems, validate that the account belongs to the tenant
    # This is a simplified check - in production you'd validate against account permissions
    account_tenant = current_account.get("tenant_id") or current_account.get("id")
    if not account_tenant:
        return False
    # Allow access if account tenant matches or if account is a super admin
    return account_tenant == tenant_id or current_account.get("is_super_admin", False)


@router.post("/route/preview", response_model=RouteResponse)
async def preview_route(
    payload: RouteRequest,
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
) -> RouteResponse:
    # Validate tenant access
    if not validate_tenant_access(payload.tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")

    # Basic input validation (fast fail)
    if not (payload.lead.email or payload.lead.company or payload.lead.website):
        raise HTTPException(400, "Lead must include email, company, or website")
    sf = await get_salesforce_gateway_for_tenant(db, tenant_id=payload.tenant_id)
    adapter = SalesforceAdapter(sf) if sf else SalesforceAdapter(None)
    decision = await route_lead(
        db, payload.lead, payload.tenant_id, adapter, preview=True
    )
    # metrics/log
    logger.info(
        "route_preview tenant=%s decision=%s context=%s",
        payload.tenant_id,
        decision.get("decision"),
        json.dumps(decision.get("context") or {})[:500],
    )
    return RouteResponse(**decision)


@router.post("/route", response_model=RouteResponse)
async def commit_route(
    payload: RouteRequest,
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
    x_idempotency_key: Optional[str] = Header(None),
) -> RouteResponse:
    # Validate tenant access
    if not validate_tenant_access(payload.tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")

    if not (payload.lead.email or payload.lead.company or payload.lead.website):
        raise HTTPException(400, "Lead must include email, company, or website")

    # Check for idempotency key in header or payload
    idempotency_key = x_idempotency_key or getattr(payload, "idempotency_key", None)

    # If idempotency key provided, check for existing decision
    if idempotency_key:
        existing = (
            db.query(RoutingDecision)
            .filter_by(tenant_id=payload.tenant_id, idempotency_key=idempotency_key)
            .first()
        )

        if existing:
            # Return existing decision
            logger.info(
                "route_commit_idempotent tenant=%s idempotency_key=%s decision_id=%s",
                payload.tenant_id,
                idempotency_key,
                existing.id,
            )
            return RouteResponse(
                decision=existing.decision,
                owner_id=existing.owner_id,
                pool_id=existing.pool_id,
                rule_id=existing.rule_id,
                reason=existing.reason,
                context=existing.context or {},
            )

    # Generate idempotency key if not provided (for deduplication)
    if not idempotency_key:
        # Create hash from lead data for automatic dedup
        lead_data = f"{payload.tenant_id}:{payload.lead.email}:{payload.lead.company}:{payload.lead.website}"
        idempotency_key = hashlib.sha256(lead_data.encode()).hexdigest()[:32]

    sf = await get_salesforce_gateway_for_tenant(db, tenant_id=payload.tenant_id)
    adapter = SalesforceAdapter(sf) if sf else SalesforceAdapter(None)
    decision = await route_lead(
        db, payload.lead, payload.tenant_id, adapter, preview=False
    )

    # persist audit with idempotency key
    rd = RoutingDecision(
        id=str(uuid.uuid4()),
        tenant_id=payload.tenant_id,
        lead_id=payload.lead.lead_id or "unknown",
        decision=decision["decision"],
        owner_id=decision.get("owner_id"),
        pool_id=decision.get("pool_id"),
        rule_id=decision.get("rule_id"),
        reason=decision.get("reason"),
        context=decision.get("context") or {},
        created_at=datetime.utcnow(),
        applied_at=datetime.utcnow(),  # Mark as applied
        idempotency_key=idempotency_key,
    )

    try:
        db.add(rd)
        db.commit()
    except IntegrityError:
        # Race condition - another request with same idempotency key completed
        db.rollback()
        existing = (
            db.query(RoutingDecision)
            .filter_by(tenant_id=payload.tenant_id, idempotency_key=idempotency_key)
            .first()
        )
        if existing:
            return RouteResponse(
                decision=existing.decision,
                owner_id=existing.owner_id,
                pool_id=existing.pool_id,
                rule_id=existing.rule_id,
                reason=existing.reason,
                context=existing.context or {},
            )
        raise

    # optional notify
    await post_decision(decision)
    logger.info(
        "route_commit tenant=%s decision=%s idempotency_key=%s context=%s",
        payload.tenant_id,
        decision.get("decision"),
        idempotency_key,
        json.dumps(decision.get("context") or {})[:500],
    )
    return RouteResponse(**decision)


# ----------------------- Admin Endpoints -----------------------


@admin_router.get("/pools")
def list_pools(
    tenant_id: str,
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    pools = db.query(RoutingPool).filter_by(tenant_id=tenant_id).all()
    members = db.query(RoutingPoolMember).filter_by(tenant_id=tenant_id).all()
    by_pool = {}
    for m in members:
        by_pool.setdefault(m.pool_id, []).append(
            {"user_id": m.user_id, "weight": m.weight}
        )
    return [
        {
            "id": p.id,
            "name": p.name,
            "algo": p.algo,
            "daily_cap": p.daily_cap,
            "members": by_pool.get(p.id, []),
        }
        for p in pools
    ]


@admin_router.post("/pools")
def upsert_pool(
    body: PoolCreate,
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(body.tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    p = (
        db.query(RoutingPool)
        .filter_by(tenant_id=body.tenant_id, id=body.pool_id)
        .one_or_none()
    )
    if not p:
        p = RoutingPool(
            id=body.pool_id,
            tenant_id=body.tenant_id,
            name=body.name,
            algo=body.algo,
            daily_cap=body.daily_cap,
        )
        db.add(p)
    else:
        p.name, p.algo, p.daily_cap = body.name, body.algo, body.daily_cap
    db.commit()
    return {"status": "ok", "pool_id": p.id}


@admin_router.post("/pools/members")
def upsert_pool_member(
    body: PoolMemberUpsert,
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(body.tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    pid = f"{body.pool_id}:{body.user_id}"
    m = db.query(RoutingPoolMember).filter_by(id=pid).one_or_none()
    if not m:
        m = RoutingPoolMember(
            id=pid,
            pool_id=body.pool_id,
            tenant_id=body.tenant_id,
            user_id=body.user_id,
            weight=body.weight or 1,
        )
        db.add(m)
    else:
        m.weight = body.weight or 1
    db.commit()
    return {"status": "ok", "member": {"user_id": body.user_id, "weight": m.weight}}


@admin_router.post("/rules")
def upsert_rule(
    body: RuleUpsert,
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(body.tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    r = (
        db.query(RoutingRule)
        .filter_by(tenant_id=body.tenant_id, id=body.rule_id)
        .one_or_none()
    )
    if not r:
        r = RoutingRule(
            id=body.rule_id,
            tenant_id=body.tenant_id,
            name=body.name,
            priority=body.priority,
            active=body.active,
            condition=body.condition,
            action=body.action,
        )
        db.add(r)
    else:
        r.name, r.priority, r.active, r.condition, r.action = (
            body.name,
            body.priority,
            body.active,
            body.condition,
            body.action,
        )
    db.commit()
    return {"status": "ok", "rule_id": r.id}


# ----------------------- Enhanced Admin Endpoints -----------------------


@admin_router.post("/route/dry-run-batch")
async def dry_run_batch(
    tenant_id: str = Body(...),
    leads: Optional[List[LeadIn]] = Body(None),
    limit: int = Body(100),
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    """Preview routing for multiple leads without committing decisions"""

    # If no leads provided, generate sample leads for demo
    if not leads:
        # In production, you would fetch unrouted leads from your lead table
        # For now, create sample leads for testing
        sample_companies = [
            ("john@acme.com", "Acme Corp", "acme.com"),
            ("jane@widget.io", "Widget Inc", "widget.io"),
            ("bob@startup.tech", "Startup Tech", "startup.tech"),
            ("alice@enterprise.com", "Enterprise Solutions", "enterprise.com"),
            ("mike@sales.biz", "Sales Pro", "sales.biz"),
        ]

        leads = []
        for i, (email, company, website) in enumerate(
            sample_companies[: min(limit, len(sample_companies))]
        ):
            leads.append(
                LeadIn(
                    lead_id=f"sample_{i+1}",
                    email=email,
                    company=company,
                    website=website,
                )
            )

    # Limit the number of leads to process
    leads = leads[:limit]

    # Get Salesforce adapter for routing
    try:
        sf = await get_salesforce_gateway_for_tenant(db, tenant_id=tenant_id)
        adapter = SalesforceAdapter(sf)
    except Exception as e:
        logger.warning("Failed to get Salesforce adapter for dry-run batch: %s", str(e))
        # Continue without adapter for demo purposes
        adapter = None

    # Process each lead through routing engine
    results = []
    warnings = []
    summary = {"assigned_owner": 0, "assigned_pool": 0, "no_match": 0}

    for lead in leads:
        try:
            # Route with preview=True to avoid persisting decisions
            decision = await route_lead(
                db, lead, tenant_id=tenant_id, crm_adapter=adapter, preview=True
            )

            # Add lead_id to result
            result = {"lead_id": lead.lead_id or "unknown", **decision}
            results.append(result)

            # Update summary counts
            if decision["decision"] == "assign_owner":
                summary["assigned_owner"] += 1
            elif decision["decision"] == "assign_pool":
                summary["assigned_pool"] += 1
            else:
                summary["no_match"] += 1

        except Exception as e:
            warning = {"lead_id": lead.lead_id or "unknown", "error": str(e)}
            warnings.append(warning)
            logger.warning("Error routing lead %s in dry-run: %s", lead.lead_id, str(e))

    return {
        "tenant_id": tenant_id,
        "total_processed": len(results),
        "results": results,
        "warnings": warnings,
        "summary": summary,
    }


@admin_router.get("/decisions")
async def list_decisions(
    tenant_id: str = Query(...),
    limit: int = Query(50),
    include_rolled_back: bool = Query(False),
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    """Get recent routing decisions with filtering"""
    query = db.query(RoutingDecision).filter_by(tenant_id=tenant_id)

    if not include_rolled_back:
        query = query.filter(RoutingDecision.rolled_back_at.is_(None))

    decisions = query.order_by(RoutingDecision.created_at.desc()).limit(limit).all()

    return [
        {
            "id": d.id,
            "lead_id": d.lead_id,
            "decision": d.decision,
            "owner_id": d.owner_id,
            "pool_id": d.pool_id,
            "rule_id": d.rule_id,
            "reason": d.reason,
            "context": d.context,
            "created_at": d.created_at.isoformat() if d.created_at else None,
            "applied_at": d.applied_at.isoformat() if d.applied_at else None,
            "rolled_back_at": d.rolled_back_at.isoformat()
            if d.rolled_back_at
            else None,
        }
        for d in decisions
    ]


@admin_router.post("/decisions/{decision_id}/rollback")
async def rollback_decision(
    decision_id: str,
    tenant_id: str = Body(...),
    reason: str = Body(None),
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    """Rollback a routing decision"""
    decision = (
        db.query(RoutingDecision).filter_by(id=decision_id, tenant_id=tenant_id).first()
    )

    if not decision:
        raise HTTPException(404, "Decision not found")

    if decision.rolled_back_at:
        raise HTTPException(400, "Decision already rolled back")

    # Mark as rolled back
    decision.rolled_back_at = datetime.utcnow()

    # If it was a pool assignment, decrement the counter with retry logic
    if decision.pool_id and decision.applied_at and decision.owner_id:
        max_retries = 3
        retry_count = 0

        while retry_count < max_retries:
            try:
                pool_state = (
                    db.query(RoutingPoolState)
                    .filter_by(id=decision.pool_id, tenant_id=tenant_id)
                    .with_for_update()
                    .first()
                )

                if pool_state:
                    assigned = pool_state.assigned_today or {}
                    if decision.owner_id in assigned:
                        assigned[decision.owner_id] = max(
                            0, assigned[decision.owner_id] - 1
                        )
                        pool_state.assigned_today = assigned
                        pool_state.updated_at = datetime.utcnow()
                        # version will be incremented automatically by SQLAlchemy

                db.commit()
                break

            except StaleDataError:
                # Concurrent modification detected, retry
                db.rollback()
                retry_count += 1
                if retry_count < max_retries:
                    time.sleep(0.1 * retry_count)  # Exponential backoff
                    logger.warning(
                        "Optimistic lock retry %d for rollback decision %s",
                        retry_count,
                        decision_id,
                    )
                else:
                    logger.error(
                        "Failed to rollback decision %s after %d retries",
                        decision_id,
                        max_retries,
                    )
                    raise HTTPException(
                        409, "Concurrent modification conflict, please retry"
                    )
    else:
        # No pool state to update, just commit the decision rollback
        db.commit()

    logger.info(
        "routing_rollback tenant=%s decision=%s reason=%s",
        tenant_id,
        decision_id,
        (reason or ""),
    )

    return {
        "status": "ok",
        "decision_id": decision_id,
        "rolled_back_at": decision.rolled_back_at.isoformat(),
    }


@admin_router.get("/pools/{pool_id}/health")
async def pool_health(
    pool_id: str,
    tenant_id: str = Query(...),
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    """Get pool health metrics"""
    pool = db.query(RoutingPool).filter_by(id=pool_id, tenant_id=tenant_id).first()

    if not pool:
        raise HTTPException(404, "Pool not found")

    members = (
        db.query(RoutingPoolMember)
        .filter_by(pool_id=pool_id, tenant_id=tenant_id)
        .all()
    )

    state = (
        db.query(RoutingPoolState).filter_by(id=pool_id, tenant_id=tenant_id).first()
    )

    assigned_today = (state.assigned_today if state else {}) or {}

    # Calculate health metrics
    members_at_cap = 0
    total_assigned = 0
    member_stats = []

    for member in members:
        assigned = assigned_today.get(member.user_id, 0)
        total_assigned += assigned

        at_cap = False
        if pool.daily_cap and assigned >= pool.daily_cap:
            members_at_cap += 1
            at_cap = True

        member_stats.append(
            {
                "user_id": member.user_id,
                "weight": member.weight,
                "assigned_today": assigned,
                "at_capacity": at_cap,
                "utilization": (assigned / pool.daily_cap * 100)
                if pool.daily_cap
                else 0,
            }
        )

    # Calculate reset time
    reset_time = None
    if state and state.reset_at:
        # Calculate midnight in tenant's timezone
        tz = ZoneInfo(state.tenant_timezone or "UTC")
        now_tz = datetime.now(tz)
        tomorrow_local_midnight = (now_tz + timedelta(days=1)).replace(
            hour=0, minute=0, second=0, microsecond=0
        )
        # Convert to UTC for consistent storage/display
        reset_time = tomorrow_local_midnight.astimezone(ZoneInfo("UTC")).isoformat()

    # Calculate actual capacity utilization
    cap_util = 0
    if pool.daily_cap and members:
        cap_util = (total_assigned / (pool.daily_cap * len(members))) * 100

    return {
        "pool_id": pool_id,
        "name": pool.name,
        "algorithm": pool.algo,
        "daily_cap": pool.daily_cap,
        "member_count": len(members),
        "members_at_capacity": members_at_cap,
        "capacity_utilization": cap_util,
        "total_assigned_today": total_assigned,
        "average_assignments": total_assigned / len(members) if members else 0,
        "reset_time": reset_time,
        "member_stats": member_stats,
        "health_status": "healthy" if members_at_cap < len(members) else "at_capacity",
    }


@admin_router.post("/pools/{pool_id}/reset")
async def reset_pool_state(
    pool_id: str,
    tenant_id: str = Body(...),
    db: Session = Depends(get_session),
    current_account: dict = Depends(get_current_account),
):
    # Validate tenant access
    if not validate_tenant_access(tenant_id, current_account):
        raise HTTPException(403, "Access denied to this tenant")
    """Reset pool assignment counters"""
    max_retries = 3
    retry_count = 0

    while retry_count < max_retries:
        try:
            state = (
                db.query(RoutingPoolState)
                .filter_by(id=pool_id, tenant_id=tenant_id)
                .with_for_update()
                .first()
            )

            if not state:
                raise HTTPException(404, "Pool state not found")

            # Reset counters
            state.assigned_today = {}
            state.reset_at = datetime.utcnow()
            state.updated_at = datetime.utcnow()
            # version will be incremented automatically by SQLAlchemy

            db.commit()

            logger.info("pool_reset tenant=%s pool=%s", tenant_id, pool_id)

            return {
                "status": "ok",
                "pool_id": pool_id,
                "reset_at": state.reset_at.isoformat(),
            }

        except StaleDataError:
            # Concurrent modification detected, retry
            db.rollback()
            retry_count += 1
            if retry_count < max_retries:
                time.sleep(0.1 * retry_count)  # Exponential backoff
                logger.warning(
                    "Optimistic lock retry %d for reset pool %s", retry_count, pool_id
                )
            else:
                logger.error(
                    "Failed to reset pool %s after %d retries", pool_id, max_retries
                )
                raise HTTPException(
                    409, "Concurrent modification conflict, please retry"
                )
