from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any, Optional

from sqlalchemy import select
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy.ext.asyncio import AsyncSession

from ..models import (
    Account,
    GremlinGatewayLicense,
    LicenseGrant,
    LicenseGrantStatus,
    LicenseGrantType,
    UsageQuotaModel,
    User,
)
from .usage_gateway import UsageGateway

log = logging.getLogger(__name__)

_FREE_GREMLIN_PLANS = {"", "free", "local_free"}
_DIRECT_RUNTIME_TIERS = {"free", "pro", "scale", "unleashed"}


def _now_utc() -> datetime:
    return datetime.now(timezone.utc)


def _coerce_datetime(value: object) -> Optional[datetime]:
    # Accept strings because drift checks and legacy snapshots still pass ISO text.
    if value is None:
        return None
    if isinstance(value, datetime):
        if value.tzinfo is None:
            return value.replace(tzinfo=timezone.utc)
        return value.astimezone(timezone.utc)
    if isinstance(value, str):
        candidate = value.strip()
        if not candidate:
            return None
        try:
            parsed = datetime.fromisoformat(candidate.replace("Z", "+00:00"))
        except ValueError:
            return None
        if parsed.tzinfo is None:
            return parsed.replace(tzinfo=timezone.utc)
        return parsed.astimezone(timezone.utc)
    return None


def _iso_or_none(value: Optional[datetime]) -> Optional[str]:
    parsed = _coerce_datetime(value)
    return parsed.isoformat() if parsed else None


def normalize_gremlin_status(
    raw_status: Optional[str],
    *,
    ends_at: Optional[datetime] = None,
) -> str:
    normalized = (raw_status or "").strip().lower()
    parsed_ends_at = _coerce_datetime(ends_at)

    if normalized == LicenseGrantStatus.REVOKED.value:
        return LicenseGrantStatus.REVOKED.value
    if normalized in {LicenseGrantStatus.CANCELED.value, "cancelled"}:
        return LicenseGrantStatus.CANCELED.value
    if normalized in {
        LicenseGrantStatus.PENDING.value,
        "invited",
        "provisioning",
    }:
        return LicenseGrantStatus.PENDING.value
    if normalized == LicenseGrantStatus.EXPIRED.value:
        return LicenseGrantStatus.EXPIRED.value
    if parsed_ends_at and parsed_ends_at <= _now_utc():
        return LicenseGrantStatus.EXPIRED.value
    if normalized in {"active", "trialing", "trial", "paid"}:
        return LicenseGrantStatus.ACTIVE.value
    return LicenseGrantStatus.ACTIVE.value


def canonical_gremlin_plan_to_runtime_tier(
    plan_key: Optional[str],
    *,
    status: Optional[str],
) -> str:
    normalized_status = normalize_gremlin_status(status)
    normalized_plan = (plan_key or "").strip().lower()

    if normalized_status in {
        LicenseGrantStatus.REVOKED.value,
        LicenseGrantStatus.CANCELED.value,
        LicenseGrantStatus.EXPIRED.value,
    }:
        return "free"
    if normalized_plan in _DIRECT_RUNTIME_TIERS:
        return normalized_plan
    if normalized_plan in _FREE_GREMLIN_PLANS:
        return "free"
    return "unleashed"


def canonical_gremlin_status_to_subscription_status(
    *,
    license_type: str,
    status: str,
) -> str:
    normalized_status = normalize_gremlin_status(status)
    if normalized_status == LicenseGrantStatus.ACTIVE.value:
        return "trialing" if license_type == "trial" else "active"
    return normalized_status


def build_gremlin_gateway_license_fields(
    *,
    grant_type: str,
    plan_key: str,
    status: str,
    starts_at: Optional[datetime],
    ends_at: Optional[datetime],
    attributes: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    normalized_status = normalize_gremlin_status(status, ends_at=ends_at)
    normalized_grant_type = (grant_type or "").strip().lower()
    attrs = attributes or {}

    license_type = (
        "paid" if normalized_grant_type == LicenseGrantType.PAID.value else "trial"
    )
    tier_after_expiry = attrs.get("tier_after_expiry")
    if license_type == "trial" and not tier_after_expiry:
        tier_after_expiry = "local_free"

    return {
        "license_type": license_type,
        "status": normalized_status,
        "tier": (plan_key or "").strip().lower() or "local_free",
        "tier_after_expiry": tier_after_expiry,
        "trial_ends_at": _coerce_datetime(ends_at) if license_type == "trial" else None,
        "current_period_start": _coerce_datetime(starts_at) if license_type == "paid" else None,
        "current_period_end": _coerce_datetime(ends_at) if license_type == "paid" else None,
    }


def serialize_gremlin_gateway_license(
    license_row: GremlinGatewayLicense,
) -> dict[str, Any]:
    return {
        "licenseId": license_row.account_id,
        "accountId": license_row.account_id,
        "userId": license_row.user_id,
        "clerkUserId": license_row.clerk_user_id,
        "primaryEmail": license_row.primary_email,
        "licenseType": license_row.license_type,
        "status": license_row.status,
        "tier": license_row.tier,
        "tierAfterExpiry": license_row.tier_after_expiry,
        "trialEndsAt": _iso_or_none(license_row.trial_ends_at),
        "currentPeriodStart": _iso_or_none(license_row.current_period_start),
        "currentPeriodEnd": _iso_or_none(license_row.current_period_end),
        "source": license_row.source,
        "syncedAt": _iso_or_none(license_row.synced_at),
    }


async def get_canonical_gremlin_runtime_tier(
    db: AsyncSession,
    *,
    account_id: str,
) -> Optional[str]:
    try:
        result = await db.execute(
            select(LicenseGrant).where(
                LicenseGrant.account_id == account_id,
                LicenseGrant.product_surface == "gremlin",
            )
        )
    except (OperationalError, ProgrammingError) as exc:
        log.debug("Canonical Gremlin grant lookup unavailable for %s: %s", account_id, exc)
        return None

    grant = result.scalar_one_or_none()
    if grant is None:
        return None

    normalized_status = normalize_gremlin_status(grant.status, ends_at=grant.ends_at)
    if normalized_status == LicenseGrantStatus.PENDING.value:
        return None

    return canonical_gremlin_plan_to_runtime_tier(
        grant.plan_key,
        status=normalized_status,
    )


async def apply_gremlin_gateway_projection(
    db: AsyncSession,
    *,
    account_id: str,
    user_id: Optional[str],
    clerk_user_id: Optional[str],
    primary_email: Optional[str],
    grant_type: str,
    plan_key: str,
    status: str,
    source: str,
    starts_at: Optional[datetime],
    ends_at: Optional[datetime],
    attributes: Optional[dict[str, Any]] = None,
) -> GremlinGatewayLicense:
    account = await db.get(Account, account_id)
    if account is None:
        raise LookupError(f"Account {account_id} not found for Gremlin projection")

    now = _now_utc()
    payload = build_gremlin_gateway_license_fields(
        grant_type=grant_type,
        plan_key=plan_key,
        status=status,
        starts_at=starts_at,
        ends_at=ends_at,
        attributes=attributes,
    )

    gateway_license = await db.get(GremlinGatewayLicense, account_id)
    if gateway_license is None:
        gateway_license = GremlinGatewayLicense(account_id=account_id, created_at=now)

    gateway_license.user_id = user_id
    gateway_license.clerk_user_id = (clerk_user_id or "").strip() or None
    gateway_license.primary_email = (primary_email or "").strip().lower() or None
    gateway_license.license_type = str(payload["license_type"])
    gateway_license.status = str(payload["status"])
    gateway_license.tier = str(payload["tier"])
    gateway_license.tier_after_expiry = (
        str(payload["tier_after_expiry"]) if payload["tier_after_expiry"] else None
    )
    gateway_license.trial_ends_at = _coerce_datetime(payload["trial_ends_at"])
    gateway_license.current_period_start = _coerce_datetime(payload["current_period_start"])
    gateway_license.current_period_end = _coerce_datetime(payload["current_period_end"])
    gateway_license.source = source
    gateway_license.synced_at = now
    gateway_license.updated_at = now
    db.add(gateway_license)

    runtime_tier = canonical_gremlin_plan_to_runtime_tier(
        gateway_license.tier,
        status=gateway_license.status,
    )
    quota = await db.get(UsageQuotaModel, account_id)
    quota_status = canonical_gremlin_status_to_subscription_status(
        license_type=gateway_license.license_type,
        status=gateway_license.status,
    )
    quota_source = (source or "license_manager")[:32]
    if quota is None:
        quota = UsageQuotaModel(
            account_id=account_id,
            tier=runtime_tier,
            subscription_status=quota_status,
            tier_synced_at=now,
            tier_sync_source=quota_source,
        )
    else:
        quota.tier = runtime_tier
        quota.subscription_status = quota_status
        quota.tier_synced_at = now
        quota.tier_sync_source = quota_source
    db.add(quota)

    entitlements = await UsageGateway(db).sync_from_stripe(
        account_id=account_id,
        tier=runtime_tier,
        addons=[],
        source=(source or "license_manager")[:50],
        expires_at=(
            gateway_license.trial_ends_at
            if gateway_license.license_type == "trial"
            else None
        ),
        commit=False,
    )
    entitlements.trial_started_at = (
        _coerce_datetime(starts_at) if gateway_license.license_type == "trial" else None
    )
    entitlements.expires_at = (
        gateway_license.trial_ends_at if gateway_license.license_type == "trial" else None
    )
    db.add(entitlements)
    await db.commit()
    await db.refresh(gateway_license)

    return gateway_license


async def fetch_gremlin_gateway_license(
    db: AsyncSession,
    *,
    account_id: Optional[str] = None,
    clerk_user_id: Optional[str] = None,
    primary_email: Optional[str] = None,
) -> Optional[GremlinGatewayLicense]:
    normalized_account_id = (account_id or "").strip() or None
    normalized_clerk_user_id = (clerk_user_id or "").strip() or None
    normalized_email = (primary_email or "").strip().lower() or None

    if normalized_account_id:
        row = await db.get(GremlinGatewayLicense, normalized_account_id)
        if row is not None:
            return row

    if normalized_clerk_user_id:
        result = await db.execute(
            select(GremlinGatewayLicense)
            .where(GremlinGatewayLicense.clerk_user_id == normalized_clerk_user_id)
            .order_by(GremlinGatewayLicense.updated_at.desc())
        )
        row = result.scalars().first()
        if row is not None:
            return row

    if normalized_email:
        result = await db.execute(
            select(GremlinGatewayLicense)
            .where(GremlinGatewayLicense.primary_email == normalized_email)
            .order_by(GremlinGatewayLicense.updated_at.desc())
        )
        row = result.scalars().first()
        if row is not None:
            return row

    if normalized_clerk_user_id:
        user_result = await db.execute(select(User).where(User.clerk_user_id == normalized_clerk_user_id))
        user = user_result.scalar_one_or_none()
        if user is not None:
            return await db.get(GremlinGatewayLicense, user.account_id)

    if normalized_email:
        user_result = await db.execute(select(User).where(User.email == normalized_email))
        user = user_result.scalar_one_or_none()
        if user is not None:
            return await db.get(GremlinGatewayLicense, user.account_id)

    return None


async def get_gremlin_gateway_runtime_tier(
    db: AsyncSession,
    *,
    account_id: str,
) -> Optional[str]:
    row = await db.get(GremlinGatewayLicense, account_id)
    if row is None:
        return None
    return canonical_gremlin_plan_to_runtime_tier(row.tier, status=row.status)
