from __future__ import annotations
from typing import Optional, Dict, Any, Tuple, List
from datetime import datetime, timedelta
import logging
from sqlalchemy.orm import Session
from .schemas import LeadIn
from .models import RoutingPool, RoutingPoolMember, RoutingPoolState, RoutingRule
from ..services.domain_family import get_registry, DomainRelation, domain_normalize

logger = logging.getLogger(__name__)

# ------- Small in-process rate limiter (fallback if slowapi not in play) -------
from collections import defaultdict, deque

_rate = defaultdict(lambda: deque(maxlen=500))


def simple_rate_limit(key: str, per_minute: int = 120) -> None:
    now = datetime.utcnow()
    dq = _rate[key]
    # drop entries older than 60s
    while dq and (now - dq[0]) > timedelta(seconds=60):
        dq.popleft()
    if len(dq) >= per_minute:
        raise RuntimeError("rate_limit_exceeded")
    dq.append(now)


# ------------------------------- Core helpers ----------------------------------


def _confidence_for_relation(rel: DomainRelation) -> float:
    if rel == DomainRelation.EXACT:
        return 0.98
    if rel == DomainRelation.SUBDOMAIN:
        return 0.95
    if rel == DomainRelation.FAMILY:
        return 0.92
    return 0.50


def _auto_reset_if_new_day(state: RoutingPoolState) -> None:
    today = datetime.utcnow().date()
    if not state.reset_at or state.reset_at.date() < today:
        state.assigned_today = {}
        state.reset_at = datetime.utcnow()


def _fair_sort_key(
    member_user_id: str, assigned_today: Dict[str, int], last_user_id: Optional[str]
) -> tuple:
    used = assigned_today.get(member_user_id, 0)
    is_last = 1 if member_user_id == last_user_id else 0
    return (used, is_last)


def _pick_from_pool(
    db: Session, tenant_id: str, pool_id: str, preview: bool = False
) -> Optional[str]:
    """Pick user from pool with optimistic locking for concurrency safety"""
    pool = (
        db.query(RoutingPool).filter_by(tenant_id=tenant_id, id=pool_id).one_or_none()
    )
    if not pool:
        return None
    members: List[RoutingPoolMember] = (
        db.query(RoutingPoolMember)
        .filter_by(tenant_id=tenant_id, pool_id=pool_id)
        .all()
    )
    if not members:
        return None

    # Retry logic for optimistic locking
    max_retries = 3
    for retry in range(max_retries):
        state = (
            db.query(RoutingPoolState)
            .filter_by(tenant_id=tenant_id, id=pool_id)
            .one_or_none()
        )
        if not state:
            state = RoutingPoolState(
                id=pool_id,
                tenant_id=tenant_id,
                assigned_today={},
                last_assigned_user_id=None,
                version=0,
            )
            db.add(state)
            db.flush()

        current_version = state.version
        _auto_reset_if_new_day(state)

        assigned_today = state.assigned_today or {}
        # respect daily_cap if configured
        candidates = []
        for m in members:
            if pool.daily_cap:
                if assigned_today.get(m.user_id, 0) >= pool.daily_cap:
                    continue
            candidates.append(m.user_id)

        if not candidates:
            candidates = [m.user_id for m in members]  # if all capped, fallback

        # Fair RR: fewest assigned today, avoid last user if tie
        candidates.sort(
            key=lambda uid: _fair_sort_key(
                uid, assigned_today, state.last_assigned_user_id
            )
        )
        chosen = candidates[0]

        if not preview:
            # Try to update with optimistic locking
            assigned_today[chosen] = assigned_today.get(chosen, 0) + 1

            # Use raw SQL for atomic version check and update
            from sqlalchemy import text

            result = db.execute(
                text("""
                    UPDATE routing_pool_state
                    SET assigned_today = :assigned,
                        last_assigned_user_id = :last_user,
                        updated_at = :updated_at,
                        version = version + 1
                    WHERE id = :pool_id
                    AND tenant_id = :tenant_id
                    AND version = :current_version
                """),
                {
                    "assigned": str(assigned_today),  # Convert to JSON string
                    "last_user": chosen,
                    "updated_at": datetime.utcnow(),
                    "pool_id": pool_id,
                    "tenant_id": tenant_id,
                    "current_version": current_version,
                },
            )

            if result.rowcount > 0:
                # Success - commit and return
                db.flush()
                return chosen
            else:
                # Version mismatch - retry
                if retry < max_retries - 1:
                    logger.debug(
                        f"Pool assignment version conflict, retrying ({retry + 1}/{max_retries})"
                    )
                    db.rollback()  # Rollback this attempt
                    continue
                else:
                    logger.warning(
                        f"Pool assignment failed after {max_retries} retries"
                    )
                    return chosen  # Return anyway on last retry
        else:
            return chosen  # Preview mode, just return

    return chosen


async def _account_match(
    crm_adapter, lead: LeadIn
) -> Tuple[
    Optional[Dict[str, Any]],
    DomainRelation,
    Optional[str],
    Optional[str],
    Optional[str],
]:
    """Return (account, relation, family_id, lead_dom, acct_dom)."""
    reg = get_registry()
    lead_dom = domain_normalize(lead.email or lead.website)
    acct = None
    rel = DomainRelation.NONE
    family = None
    acct_dom = None
    if lead_dom:
        acct = await crm_adapter.find_account_by_domain(lead_dom)
        if acct:
            acct_dom = domain_normalize(acct.get("website"))
            r, fam = reg.relation(lead_dom, acct_dom)
            rel, family = r, fam
        else:
            # Try by company if no direct domain hit
            if lead.company:
                acct = await crm_adapter.find_account_by_name_like(lead.company)
                if acct:
                    acct_dom = domain_normalize(acct.get("website"))
                    r, fam = reg.relation(lead_dom, acct_dom)
                    rel, family = r, fam
    else:
        if lead.company:
            acct = await crm_adapter.find_account_by_name_like(lead.company)
            if acct:
                acct_dom = domain_normalize(acct.get("website"))
                rel, family = DomainRelation.NONE, None
    return acct, rel, family, lead_dom, acct_dom


def _rule_matches(cond: Dict[str, Any], ctx: Dict[str, Any]) -> bool:
    # Minimal DSL: relation, family_id, abm_tier, country/state, employee_count thresholds
    if "relation" in cond:
        allowed = set(cond["relation"])
        if ctx.get("domain_relation") not in allowed:
            return False
    if "family_id" in cond:
        val = cond["family_id"]
        if isinstance(val, list):
            if ctx.get("family_id") not in val:
                return False
        else:
            if ctx.get("family_id") != val:
                return False
    if "abm_tier" in cond:
        # e.g. {"in":[1,2]}
        abm = ctx.get("abm_tier_account")
        rule = cond["abm_tier"]
        if "in" in rule and abm not in set(rule["in"]):
            return False
    if "country_in" in cond:
        if (ctx.get("lead_country") or "").upper() not in set(
            [c.upper() for c in cond["country_in"]]
        ):
            return False
    if "state_in" in cond:
        if (ctx.get("lead_state") or "").upper() not in set(
            [s.upper() for s in cond["state_in"]]
        ):
            return False
    if "employee_count" in cond:
        rule = cond["employee_count"]
        ec = ctx.get("employee_count") or 0
        if "min" in rule and ec < int(rule["min"]):
            return False
        if "max" in rule and ec > int(rule["max"]):
            return False
    return True


async def route_lead(
    db: Session, lead: LeadIn, tenant_id: str, crm_adapter, preview: bool = False
) -> Dict[str, Any]:
    # Basic input guardrails
    if not (lead.email or lead.company or lead.website):
        return {
            "decision": "no_match",
            "reason": "invalid_input",
            "context": {"error": "Lead must have email, company or website"},
            "warnings": ["Lead missing required fields: email, company, or website"],
        }

    # Optional simple rate limit by tenant (fallback)
    try:
        simple_rate_limit(f"route:{tenant_id}", per_minute=300)
    except RuntimeError:
        return {
            "decision": "no_match",
            "reason": "rate_limited",
            "context": {"tenant_id": tenant_id},
            "warnings": ["Rate limit exceeded for tenant"],
        }

    # 1) Account-based routing (your edge)
    account, rel, family_id, lead_dom, acct_dom = await _account_match(
        crm_adapter, lead
    )
    confidence = _confidence_for_relation(rel)
    ctx = {
        "lead_id": lead.lead_id,
        "domain_relation": rel.value if hasattr(rel, "value") else str(rel),
        "family_id": family_id,
        "lead_domain": lead_dom,
        "account_domain": acct_dom,
        "matched_account_id": (account or {}).get("id"),
        "account_name": (account or {}).get("name"),
        "abm_tier_account": (account or {}).get("abm_tier"),
        "employee_count": lead.employee_count,
        "lead_country": lead.country,
        "lead_state": lead.state,
        "confidence": confidence,
    }

    if (
        account
        and account.get("owner_id")
        and rel
        in {DomainRelation.EXACT, DomainRelation.SUBDOMAIN, DomainRelation.FAMILY}
    ):
        return {
            "decision": "assign_owner",
            "owner_id": account["owner_id"],
            "reason": "account_match_owner",
            "context": ctx,
            "warnings": [],
        }

    # 2) Evaluate routing rules (ordered)
    rules: List[RoutingRule] = (
        db.query(RoutingRule)
        .filter_by(tenant_id=tenant_id, active=True)
        .order_by(RoutingRule.priority.asc())
        .all()
    )
    for rule in rules:
        if _rule_matches(rule.condition or {}, ctx):
            act = rule.action or {}
            if act.get("type") == "assign_owner" and act.get("owner_id"):
                return {
                    "decision": "assign_owner",
                    "owner_id": act["owner_id"],
                    "reason": "rule_assign_owner",
                    "rule_id": rule.id,
                    "context": ctx,
                    "warnings": [],
                }
            if act.get("type") == "assign_pool" and act.get("pool_id"):
                chosen = _pick_from_pool(db, tenant_id, act["pool_id"], preview=preview)
                if chosen:
                    return {
                        "decision": "pool_assign",
                        "owner_id": chosen,
                        "pool_id": act["pool_id"],
                        "reason": "rule_pool_assign",
                        "rule_id": rule.id,
                        "context": ctx,
                        "warnings": [],
                    }

    # 3) Default: send to a default pool if exists
    default_pool = (
        db.query(RoutingPool)
        .filter_by(tenant_id=tenant_id)
        .order_by(RoutingPool.created_at.asc())
        .first()
    )
    if default_pool:
        chosen = _pick_from_pool(db, tenant_id, default_pool.id, preview=preview)
        if chosen:
            return {
                "decision": "pool_assign",
                "owner_id": chosen,
                "pool_id": default_pool.id,
                "reason": "default_pool_assign",
                "context": ctx,
                "warnings": [],
            }

    return {
        "decision": "no_match",
        "reason": "no_rule_no_pool",
        "context": ctx,
        "warnings": ["No matching rules or available pools for lead assignment"],
    }
