from __future__ import annotations

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

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

from .. import settings as saas_settings
from ..models import (
    LicenseGrant,
    LicenseGrantProjection,
    LicenseGrantStatus,
    LicenseGrantType,
    LicenseProjectionSyncStatus,
)
from .admin_surface_signals import normalize_product_surface

log = logging.getLogger(__name__)

GREMLIN_GATEWAY_KEY = "gremlin_site"
GREMLIN_SYNC_PATH = "/api/v1/internal/license-manager/gremlin"


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


def _coerce_datetime(value: object) -> Optional[datetime]:
    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_state_value(value: object) -> object:
    if isinstance(value, bool) or value is None:
        return value
    if isinstance(value, datetime):
        return _iso_or_none(value)
    if isinstance(value, str):
        parsed = _coerce_datetime(value)
        if parsed is not None:
            return parsed.isoformat()
        return value.strip()
    return value


def normalize_license_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", "paid", "trial"}:
        return LicenseGrantStatus.ACTIVE.value

    return LicenseGrantStatus.ACTIVE.value


def resolve_plan_key(
    *,
    product_surface: str,
    local_tier: Optional[str],
    grant_status: str,
) -> str:
    normalized_surface = normalize_product_surface(product_surface)
    normalized_tier = (local_tier or "").strip().lower()
    normalized_status = normalize_license_status(grant_status)

    if normalized_surface == "gremlin":
        if normalized_tier in {"workspace_ops", "hubspot_admin", "full_stack"}:
            return normalized_tier
        if normalized_status == LicenseGrantStatus.ACTIVE.value:
            return "full_stack"
        return "local_free"

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

    return normalized_tier or "free"


def resolve_grant_type(
    *,
    action: Optional[str],
    product_surface: str,
    local_tier: Optional[str],
    ends_at: Optional[datetime],
) -> str:
    normalized_action = (action or "").strip().lower()
    normalized_tier = (local_tier or "").strip().lower()
    parsed_ends_at = _coerce_datetime(ends_at)
    normalized_surface = normalize_product_surface(product_surface)

    if normalized_action in {"upgrade", "revoke"}:
        return LicenseGrantType.MANUAL_OVERRIDE.value
    if normalized_action == "extend":
        return LicenseGrantType.TRIAL.value
    if parsed_ends_at is not None:
        return LicenseGrantType.TRIAL.value
    if normalized_surface == "gremlin":
        return LicenseGrantType.MANUAL_OVERRIDE.value
    if normalized_tier and normalized_tier != "free":
        return LicenseGrantType.PAID.value
    return LicenseGrantType.TRIAL.value


def projection_gateway_key(product_surface: str) -> str:
    normalized_surface = normalize_product_surface(product_surface)
    if normalized_surface == "gremlin":
        return GREMLIN_GATEWAY_KEY
    return f"backend_{normalized_surface}"


def serialize_license_projection(projection: LicenseGrantProjection) -> dict[str, Any]:
    return {
        "id": projection.id,
        "grant_id": projection.grant_id,
        "gateway_key": projection.gateway_key,
        "sync_status": projection.sync_status,
        "external_ref": projection.external_ref,
        "error_message": projection.error_message,
        "projection_payload": projection.projection_payload or {},
        "synced_at": _iso_or_none(projection.synced_at),
        "updated_at": _iso_or_none(projection.updated_at),
    }


def serialize_license_grant(
    grant: LicenseGrant,
    *,
    projections: Optional[list[LicenseGrantProjection]] = None,
) -> dict[str, Any]:
    return {
        "id": grant.id,
        "account_id": grant.account_id,
        "user_id": grant.user_id,
        "clerk_user_id": grant.clerk_user_id,
        "primary_email": grant.primary_email,
        "product_surface": grant.product_surface,
        "grant_type": grant.grant_type,
        "plan_key": grant.plan_key,
        "status": grant.status,
        "source": grant.source,
        "starts_at": _iso_or_none(grant.starts_at),
        "ends_at": _iso_or_none(grant.ends_at),
        "attributes": grant.attributes or {},
        "updated_at": _iso_or_none(grant.updated_at),
        "projections": [
            serialize_license_projection(projection)
            for projection in (projections or [])
        ],
    }


def _build_projection_payload(grant: LicenseGrant) -> dict[str, Any]:
    return {
        "grant_id": grant.id,
        "account_id": grant.account_id,
        "user_id": grant.user_id,
        "clerk_user_id": grant.clerk_user_id,
        "primary_email": grant.primary_email,
        "product_surface": grant.product_surface,
        "grant_type": grant.grant_type,
        "plan_key": grant.plan_key,
        "status": grant.status,
        "source": grant.source,
        "starts_at": _iso_or_none(grant.starts_at),
        "ends_at": _iso_or_none(grant.ends_at),
        "attributes": grant.attributes or {},
    }


def _build_trial_snapshot(
    *,
    trial_item: dict[str, Any],
    product_surface: str,
    local_tier: Optional[str],
    local_status: Optional[str],
    local_source: Optional[str],
    trial_started_at: object,
    trial_ends_at: object,
    action: Optional[str],
) -> dict[str, Any]:
    parsed_starts_at = _coerce_datetime(trial_started_at)
    parsed_ends_at = _coerce_datetime(trial_ends_at)
    normalized_surface = normalize_product_surface(product_surface)
    resolved_status = normalize_license_status(
        local_status or str(trial_item.get("status") or ""),
        ends_at=parsed_ends_at,
    )
    resolved_plan_key = resolve_plan_key(
        product_surface=normalized_surface,
        local_tier=local_tier or str(trial_item.get("tier") or ""),
        grant_status=resolved_status,
    )
    resolved_grant_type = resolve_grant_type(
        action=action,
        product_surface=normalized_surface,
        local_tier=local_tier or str(trial_item.get("tier") or ""),
        ends_at=parsed_ends_at,
    )

    identity = trial_item.get("identity") or {}
    attributes = {
        "local_tier": (local_tier or str(trial_item.get("tier") or "")).strip().lower()
        or None,
        "local_status": (local_status or str(trial_item.get("status") or "")).strip().lower()
        or None,
        "local_source": local_source or trial_item.get("sync_source"),
        "primary_surface": trial_item.get("primary_surface"),
        "active_surfaces": list(trial_item.get("active_surfaces") or []),
        "identity_kind": identity.get("kind"),
        "is_user_fallback": bool(identity.get("is_user_fallback")),
    }
    if normalized_surface == "gremlin":
        attributes["tier_after_expiry"] = "local_free"
    if action:
        attributes["admin_action"] = action

    return {
        "account_id": str(trial_item["account_id"]),
        "user_id": trial_item.get("primary_user_id") or None,
        "clerk_user_id": trial_item.get("primary_clerk_user_id") or None,
        "primary_email": trial_item.get("primary_email"),
        "product_surface": normalized_surface,
        "grant_type": resolved_grant_type,
        "plan_key": resolved_plan_key,
        "status": resolved_status,
        "source": local_source or str(trial_item.get("sync_source") or "license_manager_sync"),
        "starts_at": parsed_starts_at,
        "ends_at": parsed_ends_at,
        "attributes": attributes,
    }


def _build_expected_gateway_state(
    *,
    product_surface: str,
    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_surface = normalize_product_surface(product_surface)
    normalized_status = normalize_license_status(status, ends_at=ends_at)
    attrs = attributes or {}

    if normalized_surface == "gremlin":
        license_type = "paid" if grant_type == LicenseGrantType.PAID.value else "trial"
        tier = resolve_plan_key(
            product_surface="gremlin",
            local_tier=plan_key,
            grant_status=normalized_status,
        )
        tier_after_expiry = attrs.get("tier_after_expiry")
        if not tier_after_expiry:
            tier_after_expiry = "local_free" if grant_type != LicenseGrantType.PAID.value else None
        expected = {
            "license_type": license_type,
            "status": normalized_status,
            "tier": tier,
            "tier_after_expiry": tier_after_expiry,
            "trial_ends_at": _iso_or_none(ends_at) if license_type == "trial" else None,
            "current_period_start": _iso_or_none(starts_at) if license_type == "paid" else None,
            "current_period_end": _iso_or_none(ends_at) if license_type == "paid" else None,
        }
        return expected

    return {
        "account_id": None,
        "product_surface": normalized_surface,
        "grant_type": grant_type,
        "plan_key": plan_key,
        "status": normalized_status,
        "starts_at": _iso_or_none(starts_at),
        "ends_at": _iso_or_none(ends_at),
    }


def _compare_states(
    *,
    expected: dict[str, Any],
    actual: Optional[dict[str, Any]],
) -> dict[str, Any]:
    actual_state = actual or {}
    mismatches: dict[str, dict[str, Any]] = {}

    for key, expected_value in expected.items():
        actual_value = actual_state.get(key)
        if _normalize_state_value(expected_value) != _normalize_state_value(actual_value):
            mismatches[key] = {
                "expected": expected_value,
                "actual": actual_value,
            }

    return {
        "drift_detected": bool(mismatches),
        "expected": expected,
        "actual": actual_state,
        "mismatches": mismatches,
    }


def _projection_base_url() -> str:
    base_url = (saas_settings.GREMLIN_LICENSE_GATEWAY_URL or saas_settings.APP_URL or "").strip()
    if not base_url:
        raise RuntimeError(
            "GREMLIN_LICENSE_GATEWAY_URL or APP_URL must be configured for Gremlin projection"
        )
    return base_url.rstrip("/")


def _projection_secret() -> str:
    secret = (saas_settings.LICENSE_MANAGER_INTERNAL_SECRET or "").strip()
    if not secret:
        raise RuntimeError(
            "LICENSE_MANAGER_INTERNAL_SECRET must be configured for internal license projection"
        )
    return secret


async def _post_gremlin_projection(payload: dict[str, Any]) -> dict[str, Any]:
    target_url = f"{_projection_base_url()}{GREMLIN_SYNC_PATH}"
    headers = {
        "Content-Type": "application/json",
        "X-Internal-Api-Key": _projection_secret(),
    }

    async with httpx.AsyncClient(timeout=15.0) as client:
        response = await client.post(target_url, json=payload, headers=headers)
        response.raise_for_status()
        data = response.json()

    if not isinstance(data, dict):
        raise RuntimeError("Gremlin projection returned a non-object payload")
    return data


async def _fetch_gremlin_projection_state(
    *,
    account_id: Optional[str],
    clerk_user_id: Optional[str],
    primary_email: Optional[str],
) -> Optional[dict[str, Any]]:
    params = {}
    if account_id:
        params["account_id"] = account_id
    if clerk_user_id:
        params["clerk_user_id"] = clerk_user_id
    if primary_email:
        params["primary_email"] = primary_email
    if not params:
        return None

    target_url = f"{_projection_base_url()}{GREMLIN_SYNC_PATH}"
    headers = {"X-Internal-Api-Key": _projection_secret()}

    async with httpx.AsyncClient(timeout=15.0) as client:
        response = await client.get(target_url, params=params, headers=headers)
        if response.status_code == 404:
            return None
        response.raise_for_status()
        data = response.json()

    if not isinstance(data, dict):
        return None

    license_payload = data.get("license")
    if not isinstance(license_payload, dict):
        return None

    return {
        "license_type": license_payload.get("licenseType"),
        "status": license_payload.get("status"),
        "tier": license_payload.get("tier"),
        "tier_after_expiry": license_payload.get("tierAfterExpiry"),
        "trial_ends_at": license_payload.get("trialEndsAt"),
        "current_period_start": license_payload.get("currentPeriodStart"),
        "current_period_end": license_payload.get("currentPeriodEnd"),
    }


async def _load_projection(
    db: AsyncSession,
    *,
    grant_id: str,
    gateway_key: str,
) -> Optional[LicenseGrantProjection]:
    result = await db.execute(
        select(LicenseGrantProjection).where(
            LicenseGrantProjection.grant_id == grant_id,
            LicenseGrantProjection.gateway_key == gateway_key,
        )
    )
    return result.scalar_one_or_none()


async def _prepare_projection_record(
    db: AsyncSession,
    *,
    grant: LicenseGrant,
) -> tuple[LicenseGrantProjection, bool]:
    gateway_key = projection_gateway_key(grant.product_surface)
    projection = await _load_projection(db, grant_id=grant.id, gateway_key=gateway_key)
    if projection is None:
        projection = LicenseGrantProjection(
            grant_id=grant.id,
            gateway_key=gateway_key,
        )

    now = _now_utc()
    payload = _build_projection_payload(grant)
    projection.updated_at = now
    projection.projection_payload = payload

    if gateway_key != GREMLIN_GATEWAY_KEY:
        projection.sync_status = LicenseProjectionSyncStatus.SYNCED.value
        projection.external_ref = grant.account_id
        projection.error_message = None
        projection.synced_at = now
        db.add(projection)
        await db.flush()
        return projection, False

    if grant.status == LicenseGrantStatus.PENDING.value:
        projection.sync_status = LicenseProjectionSyncStatus.SKIPPED.value
        projection.external_ref = None
        projection.error_message = (
            "Pending Gremlin grants are not projected until the account is active."
        )
        projection.synced_at = now
        db.add(projection)
        await db.flush()
        return projection, False

    projection.sync_status = LicenseProjectionSyncStatus.PENDING.value
    projection.error_message = None
    db.add(projection)
    await db.flush()
    return projection, True


async def _finalize_projection_sync(
    db: AsyncSession,
    *,
    projection: LicenseGrantProjection,
    grant: LicenseGrant,
) -> LicenseGrantProjection:
    now = _now_utc()
    try:
        response_payload = await _post_gremlin_projection(_build_projection_payload(grant))
        projection.sync_status = LicenseProjectionSyncStatus.SYNCED.value
        projection.external_ref = str(
            response_payload.get("licenseId")
            or response_payload.get("license_id")
            or response_payload.get("userId")
            or response_payload.get("user_id")
            or grant.account_id
        )
        projection.error_message = None
        projection.projection_payload = response_payload
        projection.synced_at = now
    except Exception as exc:  # noqa: BLE001 - projection failures are recorded
        log.error(
            "Failed to project Gremlin license grant %s for account %s: %s",
            grant.id,
            grant.account_id,
            exc,
            exc_info=True,
        )
        projection.sync_status = LicenseProjectionSyncStatus.ERROR.value
        projection.external_ref = None
        projection.error_message = str(exc)
        projection.synced_at = now

    projection.updated_at = now
    db.add(projection)
    await db.commit()
    await db.refresh(projection)
    return projection


async def upsert_and_sync_license_grant(
    db: AsyncSession,
    *,
    account_id: str,
    product_surface: str,
    user_id: Optional[str] = None,
    clerk_user_id: Optional[str] = None,
    primary_email: Optional[str] = None,
    grant_type: str,
    plan_key: str,
    status: str,
    source: str,
    starts_at: Optional[datetime],
    ends_at: Optional[datetime],
    attributes: Optional[dict[str, Any]] = None,
) -> tuple[LicenseGrant, LicenseGrantProjection]:
    normalized_surface = normalize_product_surface(product_surface)
    normalized_status = normalize_license_status(status, ends_at=ends_at)
    now = _now_utc()

    result = await db.execute(
        select(LicenseGrant).where(
            LicenseGrant.account_id == account_id,
            LicenseGrant.product_surface == normalized_surface,
        )
    )
    grant = result.scalar_one_or_none()
    if grant is None:
        grant = LicenseGrant(
            account_id=account_id,
            product_surface=normalized_surface,
            created_at=now,
        )

    grant.user_id = user_id
    grant.clerk_user_id = clerk_user_id
    grant.primary_email = (primary_email or "").strip().lower() or None
    grant.grant_type = grant_type
    grant.plan_key = plan_key
    grant.status = normalized_status
    grant.source = source
    grant.starts_at = _coerce_datetime(starts_at)
    grant.ends_at = _coerce_datetime(ends_at)
    grant.attributes = attributes or {}
    grant.updated_at = now

    db.add(grant)
    await db.flush()
    projection, needs_remote_sync = await _prepare_projection_record(db, grant=grant)
    await db.commit()
    await db.refresh(grant)
    await db.refresh(projection)

    if needs_remote_sync:
        projection = await _finalize_projection_sync(db, projection=projection, grant=grant)

    return grant, projection


async def detect_license_grant_drift(
    db: AsyncSession,
    *,
    grant: LicenseGrant,
    projection: Optional[LicenseGrantProjection] = None,
) -> dict[str, Any]:
    gateway_key = projection_gateway_key(grant.product_surface)
    expected = _build_expected_gateway_state(
        product_surface=grant.product_surface,
        grant_type=grant.grant_type,
        plan_key=grant.plan_key,
        status=grant.status,
        starts_at=grant.starts_at,
        ends_at=grant.ends_at,
        attributes=grant.attributes or {},
    )

    if gateway_key == GREMLIN_GATEWAY_KEY:
        actual = await _fetch_gremlin_projection_state(
            account_id=grant.account_id,
            clerk_user_id=grant.clerk_user_id,
            primary_email=grant.primary_email,
        )
        result = _compare_states(expected=expected, actual=actual)
        result["gateway_key"] = gateway_key
        return result

    if projection is None:
        projection = await _load_projection(db, grant_id=grant.id, gateway_key=gateway_key)
    actual = projection.projection_payload if projection else None
    result = _compare_states(expected=expected, actual=actual)
    result["gateway_key"] = gateway_key
    return result


async def detect_trial_license_drift(
    db: AsyncSession,
    *,
    trial_item: dict[str, Any],
    product_surface: str,
    local_tier: Optional[str],
    local_status: Optional[str],
    local_source: Optional[str],
    trial_started_at: object,
    trial_ends_at: object,
    action: Optional[str] = None,
) -> dict[str, Any]:
    snapshot = _build_trial_snapshot(
        trial_item=trial_item,
        product_surface=product_surface,
        local_tier=local_tier,
        local_status=local_status,
        local_source=local_source,
        trial_started_at=trial_started_at,
        trial_ends_at=trial_ends_at,
        action=action,
    )

    expected = _build_expected_gateway_state(
        product_surface=snapshot["product_surface"],
        grant_type=snapshot["grant_type"],
        plan_key=snapshot["plan_key"],
        status=snapshot["status"],
        starts_at=snapshot["starts_at"],
        ends_at=snapshot["ends_at"],
        attributes=snapshot["attributes"],
    )
    gateway_key = projection_gateway_key(str(snapshot["product_surface"]))

    if gateway_key == GREMLIN_GATEWAY_KEY:
        actual = await _fetch_gremlin_projection_state(
            account_id=str(snapshot["account_id"]),
            clerk_user_id=snapshot["clerk_user_id"],
            primary_email=snapshot["primary_email"],
        )
    else:
        result = await db.execute(
            select(LicenseGrant)
            .where(LicenseGrant.account_id == snapshot["account_id"])
            .where(LicenseGrant.product_surface == snapshot["product_surface"])
        )
        existing_grant = result.scalar_one_or_none()
        actual = None
        if existing_grant:
            projection = await _load_projection(
                db,
                grant_id=existing_grant.id,
                gateway_key=gateway_key,
            )
            actual = projection.projection_payload if projection else None

    result = _compare_states(expected=expected, actual=actual)
    result["gateway_key"] = gateway_key
    return result


async def sync_trial_license_from_inventory(
    db: AsyncSession,
    *,
    trial_item: dict[str, Any],
    product_surface: str,
    local_tier: Optional[str],
    local_status: Optional[str],
    local_source: Optional[str],
    trial_started_at: object,
    trial_ends_at: object,
    action: Optional[str] = None,
) -> tuple[LicenseGrant, LicenseGrantProjection]:
    snapshot = _build_trial_snapshot(
        trial_item=trial_item,
        product_surface=product_surface,
        local_tier=local_tier,
        local_status=local_status,
        local_source=local_source,
        trial_started_at=trial_started_at,
        trial_ends_at=trial_ends_at,
        action=action,
    )

    return await upsert_and_sync_license_grant(
        db,
        account_id=snapshot["account_id"],
        product_surface=snapshot["product_surface"],
        user_id=snapshot["user_id"],
        clerk_user_id=snapshot["clerk_user_id"],
        primary_email=snapshot["primary_email"],
        grant_type=snapshot["grant_type"],
        plan_key=snapshot["plan_key"],
        status=snapshot["status"],
        source=snapshot["source"],
        starts_at=snapshot["starts_at"],
        ends_at=snapshot["ends_at"],
        attributes=snapshot["attributes"],
    )
