from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, Iterable, Optional, Sequence

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

from ..models import (
    BetaSubmission,
    GoogleSheetsIntegration,
    TrialEmailSend,
    TrialEmailTemplate,
    UsageLedger,
)

SUPPORTED_PRODUCT_SURFACES = ("gremlin", "sheets", "deal_desk")
DEFAULT_PRODUCT_SURFACE = "gremlin"

_SURFACE_ALIASES = {
    "gremlin": "gremlin",
    "cli": "gremlin",
    "agent_cli": "gremlin",
    "sheets": "sheets",
    "sheet": "sheets",
    "google_sheets": "sheets",
    "google-sheets": "sheets",
    "deal_desk": "deal_desk",
    "dealdesk": "deal_desk",
    "deal-desk": "deal_desk",
    "agent": "deal_desk",
}

_SURFACE_KEYWORDS: tuple[tuple[str, str], ...] = (
    ("agent_trial", "gremlin"),
    ("g-gremlin", "gremlin"),
    ("gremlin", "gremlin"),
    ("cli", "gremlin"),
    ("deal_desk", "deal_desk"),
    ("dealdesk", "deal_desk"),
    ("deal desk", "deal_desk"),
    ("google_sheets", "sheets"),
    ("google sheets", "sheets"),
    ("gsheets", "sheets"),
    ("sheet", "sheets"),
)

_USER_FALLBACK_NAME_PREFIXES = (
    "cli user ",
    "user ",
    "gremlin trial",
)


@dataclass
class SurfaceSignals:
    reasons: Dict[str, list[str]] = field(
        default_factory=lambda: {
            surface: [] for surface in SUPPORTED_PRODUCT_SURFACES
        }
    )

    def add(self, surface: str, reason: str) -> None:
        normalized = normalize_product_surface(surface)
        if normalized not in self.reasons:
            return
        if reason not in self.reasons[normalized]:
            self.reasons[normalized].append(reason)

    def merge(self, other: "SurfaceSignals") -> None:
        for surface, reasons in other.reasons.items():
            for reason in reasons:
                self.add(surface, reason)

    def flags(self) -> Dict[str, bool]:
        return {
            surface: bool(self.reasons.get(surface))
            for surface in SUPPORTED_PRODUCT_SURFACES
        }

    def as_dict(self) -> Dict[str, Dict[str, object]]:
        return {
            "flags": self.flags(),
            "reasons": {
                surface: list(self.reasons.get(surface, []))
                for surface in SUPPORTED_PRODUCT_SURFACES
            },
        }


def empty_surface_flags() -> Dict[str, bool]:
    return {surface: False for surface in SUPPORTED_PRODUCT_SURFACES}


def empty_surface_reasons() -> Dict[str, list[str]]:
    return {surface: [] for surface in SUPPORTED_PRODUCT_SURFACES}


def normalize_product_surface(
    value: Optional[str],
    *,
    default: str = DEFAULT_PRODUCT_SURFACE,
) -> str:
    raw = (value or "").strip().lower().replace(" ", "_")
    if not raw:
        return default
    return _SURFACE_ALIASES.get(raw, default)


def infer_product_surface(
    *,
    product_surface: Optional[str] = None,
    source: Optional[str] = None,
    default: str = DEFAULT_PRODUCT_SURFACE,
) -> str:
    normalized = normalize_product_surface(product_surface, default="")
    if normalized:
        return normalized
    for keyword, surface in _SURFACE_KEYWORDS:
        if keyword in (source or "").strip().lower():
            return surface
    return default


def add_keyword_surface_reasons(
    signals: SurfaceSignals,
    *,
    reason_prefix: str,
    values: Iterable[Optional[str]],
) -> None:
    for value in values:
        if not value:
            continue
        raw_value = str(value).strip()
        lowered = raw_value.lower()
        matched: set[str] = set()
        for keyword, surface in _SURFACE_KEYWORDS:
            if keyword in lowered and surface not in matched:
                signals.add(surface, f"{reason_prefix}:{raw_value}")
                matched.add(surface)


def classify_account_identity(
    *,
    account_id: str,
    account_name: Optional[str],
    clerk_org_id: Optional[str],
    primary_user_id: Optional[str],
    primary_clerk_user_id: Optional[str],
    user_count: int = 0,
) -> Dict[str, object]:
    normalized_name = (account_name or "").strip().lower()

    if clerk_org_id:
        kind = "clerk_org"
        label = "Clerk org"
        reason = "account has clerk_org_id"
    elif primary_clerk_user_id and primary_clerk_user_id == account_id:
        kind = "user_fallback"
        label = "User fallback"
        reason = "account_id matches primary Clerk user id"
    elif primary_user_id and primary_user_id == account_id:
        kind = "user_fallback"
        label = "User fallback"
        reason = "account_id matches primary user row id"
    elif any(normalized_name.startswith(prefix) for prefix in _USER_FALLBACK_NAME_PREFIXES):
        kind = "user_fallback"
        label = "User fallback"
        reason = f"account name suggests fallback ({account_name})"
    else:
        kind = "account_only"
        label = "Standalone account"
        reason = None

    return {
        "kind": kind,
        "label": label,
        "reason": reason,
        "is_user_fallback": kind == "user_fallback",
        "clerk_org_id": clerk_org_id,
        "primary_user_id": primary_user_id,
        "primary_clerk_user_id": primary_clerk_user_id,
        "user_count": int(user_count or 0),
    }


async def collect_account_surface_signals(
    db: AsyncSession,
    *,
    account_ids: Sequence[str],
    primary_emails_by_account: Dict[str, Optional[str]],
) -> Dict[str, SurfaceSignals]:
    signals = {account_id: SurfaceSignals() for account_id in account_ids}
    if not account_ids:
        return signals

    sheet_result = await db.execute(
        select(GoogleSheetsIntegration.account_id, GoogleSheetsIntegration.status).where(
            GoogleSheetsIntegration.account_id.in_(list(account_ids))
        )
    )
    for account_id, status in sheet_result.all():
        surface_signals = signals.get(str(account_id))
        if surface_signals is None:
            continue
        status_value = getattr(status, "value", status)
        surface_signals.add("sheets", f"google_sheets_integration:{status_value}")

    send_result = await db.execute(
        select(
            TrialEmailSend.account_id,
            TrialEmailTemplate.product_surface,
            TrialEmailTemplate.slug,
        )
        .join(TrialEmailTemplate, TrialEmailTemplate.id == TrialEmailSend.template_id)
        .where(TrialEmailSend.account_id.in_(list(account_ids)))
    )
    for account_id, product_surface, slug in send_result.all():
        surface_signals = signals.get(str(account_id))
        if surface_signals is None:
            continue
        surface_signals.add(
            normalize_product_surface(product_surface),
            f"trial_email:{slug}",
        )

    email_to_accounts: Dict[str, list[str]] = {}
    for account_id, email in primary_emails_by_account.items():
        normalized_email = (email or "").strip().lower()
        if normalized_email:
            email_to_accounts.setdefault(normalized_email, []).append(account_id)

    if email_to_accounts:
        beta_result = await db.execute(
            select(
                BetaSubmission.email,
                BetaSubmission.source,
                BetaSubmission.product_surface,
            ).where(BetaSubmission.email.in_(list(email_to_accounts.keys())))
        )
        for email, source, product_surface in beta_result.all():
            surface = infer_product_surface(
                product_surface=product_surface,
                source=source,
            )
            reason = f"beta_submission:{source or surface}"
            for account_id in email_to_accounts.get((email or "").strip().lower(), []):
                signals[account_id].add(surface, reason)

    ledger_result = await db.execute(
        select(UsageLedger.account_id, UsageLedger.resource, UsageLedger.source).where(
            UsageLedger.account_id.in_(list(account_ids))
        )
    )
    for account_id, resource, source in ledger_result.all():
        surface_signals = signals.get(str(account_id))
        if surface_signals is None:
            continue
        add_keyword_surface_reasons(
            surface_signals,
            reason_prefix="usage_ledger",
            values=(resource, source),
        )

    return signals
