from __future__ import annotations

import asyncio
import hashlib
import logging
import os
from datetime import timedelta
from typing import Any, Dict, List, Optional

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from ..models import (
    Account,
    InsightTicketSeverity,
    SaaSEvent,
    SaaSEventStatus,
    SupportTicket,
    TelemetryInsightTicket,
    User,
    UserRole,
    utcnow,
)
from ..repos.telemetry_insight_repo import TelemetryInsightRepo
from .email_notifications import send_proactive_outreach_email

log = logging.getLogger(__name__)

MAX_EMAILS_PER_RUN = 10
MAX_CONCURRENT_SENDS = 3
OUTREACH_COOLDOWN_DAYS = 7
RECOVERY_WINDOW_MINUTES = 20
ABANDONMENT_WINDOW_MINUTES = 15


async def get_outreach_candidates(db: AsyncSession) -> List[TelemetryInsightTicket]:
    cutoff = utcnow() - timedelta(hours=2)
    stmt = (
        select(TelemetryInsightTicket)
        .where(
            TelemetryInsightTicket.severity.in_(
                [InsightTicketSeverity.CRITICAL, InsightTicketSeverity.HIGH]
            ),
            TelemetryInsightTicket.affected_paying_orgs >= 1,
            TelemetryInsightTicket.primary_org_id.is_not(None),
            TelemetryInsightTicket.last_seen_at >= cutoff,
            TelemetryInsightTicket.outreach_status.is_(None),
        )
        .order_by(TelemetryInsightTicket.last_seen_at.desc())
        .limit(50)
    )
    rows = await db.execute(stmt)
    return list(rows.scalars().all())


async def was_org_contacted_recently(
    db: AsyncSession, org_id: str, days: int = OUTREACH_COOLDOWN_DAYS
) -> bool:
    cutoff = utcnow() - timedelta(days=days)
    stmt = select(func.count()).select_from(TelemetryInsightTicket).where(
        TelemetryInsightTicket.primary_org_id == org_id,
        TelemetryInsightTicket.outreach_sent_at.is_not(None),
        TelemetryInsightTicket.outreach_sent_at >= cutoff,
    )
    result = await db.execute(stmt)
    return int(result.scalar() or 0) > 0


async def has_recent_support_ticket(
    db: AsyncSession, insight: TelemetryInsightTicket
) -> bool:
    if not insight.primary_org_id:
        return False
    stmt = (
        select(SupportTicket.id)
        .where(SupportTicket.account_id == insight.primary_org_id)
        .where(SupportTicket.created_at >= insight.first_seen_at)
        .limit(1)
    )
    result = await db.execute(stmt)
    return result.scalar() is not None


async def has_blocked_signal(
    insight: TelemetryInsightTicket, db: AsyncSession
) -> bool:
    context = insight.context_json or {}
    retry_count = int(context.get("retry_count") or 0)
    if retry_count >= 2:
        return True

    if not insight.action or not insight.primary_org_id:
        return False

    started_stmt = select(SaaSEvent).where(
        SaaSEvent.org_id == insight.primary_org_id,
        SaaSEvent.action == insight.action,
        SaaSEvent.status == SaaSEventStatus.STARTED,
        SaaSEvent.ts >= insight.first_seen_at - timedelta(minutes=5),
        SaaSEvent.ts <= insight.last_seen_at,
    )
    started_events = (await db.execute(started_stmt)).scalars().all()

    for started in started_events:
        completed_action = started.action.replace("_started", "_completed")
        completed_stmt = (
            select(SaaSEvent)
            .where(
                SaaSEvent.org_id == insight.primary_org_id,
                SaaSEvent.action == completed_action,
                SaaSEvent.status.in_(
                    [SaaSEventStatus.SUCCESS, SaaSEventStatus.COMPLETED]
                ),
                SaaSEvent.ts > started.ts,
                SaaSEvent.ts <= started.ts
                + timedelta(minutes=ABANDONMENT_WINDOW_MINUTES),
            )
            .limit(1)
        )
        completed = await db.execute(completed_stmt)
        if not completed.scalar():
            return True

    return False


async def has_subsequent_success(
    db: AsyncSession, insight: TelemetryInsightTicket
) -> bool:
    if not insight.primary_org_id or not insight.action:
        return False
    stmt = (
        select(SaaSEvent)
        .where(
            SaaSEvent.org_id == insight.primary_org_id,
            SaaSEvent.action == insight.action,
            SaaSEvent.status.in_(
                [SaaSEventStatus.SUCCESS, SaaSEventStatus.COMPLETED]
            ),
            SaaSEvent.ts > insight.last_seen_at,
            SaaSEvent.ts
            <= insight.last_seen_at + timedelta(minutes=RECOVERY_WINDOW_MINUTES),
        )
        .limit(1)
    )
    result = await db.execute(stmt)
    return result.scalar() is not None


async def get_outreach_contact(db: AsyncSession, org_id: str) -> Optional[str]:
    """Get explicit admin contact for outreach. Returns None if not available."""
    account = await db.get(Account, org_id)
    if not account:
        return None

    admin_stmt = (
        select(User.email)
        .where(
            User.account_id == org_id,
            User.role.in_(UserRole.admin_roles()),
            User.seat_status == "active",
        )
        .order_by(User.created_at.asc())
        .limit(1)
    )
    admin_email = (await db.execute(admin_stmt)).scalar_one_or_none()
    if admin_email:
        return admin_email

    if account.stripe_customer_id:
        # Optionally fetch from Stripe if available
        return None

    return None


async def mark_outreach_sent(
    db: AsyncSession, insight: TelemetryInsightTicket, email_id: Optional[str]
) -> None:
    insight.outreach_status = "sent"
    insight.outreach_sent_at = utcnow()
    insight.outreach_email_id = email_id
    insight.outreach_skipped_reason = None
    db.add(insight)
    await db.commit()
    await db.refresh(insight)


async def mark_outreach_skipped(
    db: AsyncSession,
    insight: TelemetryInsightTicket,
    reason: str,
    *,
    commit: bool = True,
) -> None:
    insight.outreach_status = "skipped"
    insight.outreach_skipped_reason = reason
    db.add(insight)
    if commit:
        await db.commit()
        await db.refresh(insight)


async def evaluate_proactive_outreach(db: AsyncSession) -> None:
    """Run every 15 minutes to identify and send proactive outreach."""
    if not os.getenv("PROACTIVE_OUTREACH_ENABLED"):
        return

    candidates = await get_outreach_candidates(db)
    if not candidates:
        return

    repo = TelemetryInsightRepo(db)
    send_queue: List[Dict[str, Any]] = []
    skipped = False

    for insight in candidates:
        if len(send_queue) >= MAX_EMAILS_PER_RUN:
            break

        if not insight.primary_org_id:
            await mark_outreach_skipped(db, insight, "missing_org", commit=False)
            skipped = True
            continue

        account = await db.get(Account, insight.primary_org_id)
        if account and account.do_not_contact:
            await mark_outreach_skipped(db, insight, "do_not_contact", commit=False)
            skipped = True
            continue

        if await was_org_contacted_recently(db, insight.primary_org_id):
            await mark_outreach_skipped(db, insight, "rate_limited", commit=False)
            skipped = True
            continue

        if await has_recent_support_ticket(db, insight):
            await mark_outreach_skipped(db, insight, "support_ticket_exists", commit=False)
            skipped = True
            continue

        if not await has_blocked_signal(insight, db):
            await mark_outreach_skipped(db, insight, "no_blocked_signal", commit=False)
            skipped = True
            continue

        if await has_subsequent_success(db, insight):
            await mark_outreach_skipped(db, insight, "recovered", commit=False)
            skipped = True
            continue

        contact_email = await get_outreach_contact(db, insight.primary_org_id)
        if not contact_email:
            await mark_outreach_skipped(db, insight, "no_explicit_contact", commit=False)
            skipped = True
            continue

        send_queue.append(
            {
                "ticket_id": insight.id,
                "org_id": insight.primary_org_id,
                "email": contact_email,
            }
        )

    if skipped:
        await db.commit()

    if not send_queue:
        return

    semaphore = asyncio.Semaphore(MAX_CONCURRENT_SENDS)

    async def _send(payload: Dict[str, Any]) -> Dict[str, Any]:
        async with semaphore:
            try:
                email_id = await send_proactive_outreach_email(payload["email"])
                return {**payload, "email_id": email_id, "error": None}
            except Exception as exc:
                return {**payload, "email_id": None, "error": str(exc)}

    results = await asyncio.gather(*[_send(payload) for payload in send_queue])

    for result in results:
        insight = await repo.get_ticket(result["ticket_id"])
        if not insight:
            continue

        if result["error"]:
            await mark_outreach_skipped(db, insight, "send_failed")
            continue

        await mark_outreach_sent(db, insight, result["email_id"])

        recipient_hash = hashlib.sha256(result["email"].encode()).hexdigest()[:16]
        await repo.add_action(
            ticket_id=insight.id,
            action_type="proactive_email_sent",
            details={
                "recipient_hash": recipient_hash,
                "resend_id": result["email_id"],
            },
        )
