"""
Trial + Premium entitlement tracker.

Philosophy: the 30-day free trial is tied to a HARDWARE fingerprint, not to
installed-app state. If a user uninstalls and reinstalls, the server still
knows this hardware has been seen before and counts the elapsed days from
the first_seen_at timestamp. This defeats "reset trial by reinstall".

`is_premium` here is *device-level*: a device can be upgraded to premium
by an admin (manually or via a paid key redeemed on the website). The
user's own subscription (tied to `users.expiry_at`) also grants premium —
the frontend merges both (device premium OR user premium).

Endpoint:
    POST /api/trial/status
    body: { hardware_id, mac_address?, model?, app_version? }
    returns:
      {
        hardware_id, first_seen_at, last_seen_at,
        trial_days, days_used, days_remaining,
        trial_expired, is_premium, premium_expiry
      }

Admin endpoints (auth: admin):
    GET    /api/admin/device-trials           list all device trials
    PATCH  /api/admin/device-trials/{hw_id}   flip is_premium / set premium_expiry
    DELETE /api/admin/device-trials/{hw_id}   reset / forget a device

Note: hardware_id is REQUIRED. The Android client sends Settings.Secure.
ANDROID_ID; the web preview sends a stable random UUID stored in
localStorage. Mobile re-install → same ANDROID_ID → same trial clock.
"""

from datetime import datetime, timezone
from typing import Optional, List

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field

from core.auth import require_admin
from core.db import get_db
import os

router = APIRouter(prefix="/api/trial", tags=["trial"])
admin_router = APIRouter(
    prefix="/api/admin/device-trials",
    tags=["admin-device-trials"],
    dependencies=[Depends(require_admin)],
)


def _trial_days(platform: Optional[str] = None) -> int:
    """Return the trial length in days for a given client platform.

    Smart-TV builds (Samsung Tizen, LG webOS) share the same 30-day trial
    as phones and the web preview. The function still accepts a
    `platform` arg so the admin can A/B-test a shorter TV trial via the
    `TV_TRIAL_DAYS` env var without redeploying.

    Env overrides:
      TV_TRIAL_DAYS  (default 30)  → tizen, webos
      TRIAL_DAYS     (default 30)  → everything else
    """
    p = (platform or "").strip().lower()
    if p in ("tizen", "webos"):
        try:
            return int(os.environ.get("TV_TRIAL_DAYS", "30"))
        except ValueError:
            return 30
    try:
        return int(os.environ.get("TRIAL_DAYS", "30"))
    except ValueError:
        return 30


class TrialStatusIn(BaseModel):
    hardware_id: str = Field(min_length=4, max_length=128)
    mac_address: Optional[str] = Field(default=None, max_length=32)
    model: Optional[str] = Field(default=None, max_length=128)
    app_version: Optional[str] = Field(default=None, max_length=32)
    # 2026-05 — client now reports its platform so the server can apply
    # the correct trial duration (15d for tizen/webos, 30d otherwise).
    platform: Optional[str] = Field(default=None, max_length=32)


class TrialStatusOut(BaseModel):
    hardware_id: str
    first_seen_at: str
    last_seen_at: str
    trial_days: int
    days_used: int
    days_remaining: int
    trial_expired: bool
    is_premium: bool
    premium_expiry: Optional[str] = None


def _parse_iso(s: str) -> datetime:
    if s.endswith("Z"):
        s = s[:-1] + "+00:00"
    dt = datetime.fromisoformat(s)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt


def _days_between(earlier_iso: str, now: datetime) -> int:
    earlier = _parse_iso(earlier_iso)
    delta = now - earlier
    return max(0, delta.days)


@router.post("/status", response_model=TrialStatusOut)
async def trial_status(body: TrialStatusIn):
    db = get_db()
    now = datetime.now(timezone.utc)
    now_iso = now.isoformat()

    hw = body.hardware_id.strip()
    if not hw:
        raise HTTPException(status_code=400, detail="hardware_id is required")

    # Upsert: first time we see this hardware_id, stamp first_seen_at.
    existing = await db.device_trials.find_one(
        {"hardware_id": hw}, {"_id": 0}
    )
    if existing is None:
        doc = {
            "hardware_id": hw,
            "mac_address": body.mac_address,
            "model": body.model,
            "app_version": body.app_version,
            "platform": body.platform,
            "first_seen_at": now_iso,
            "last_seen_at": now_iso,
            "is_premium": False,
            "premium_expiry": None,
        }
        await db.device_trials.insert_one(doc)
        existing = doc
    else:
        update_fields: dict = {"last_seen_at": now_iso}
        if body.mac_address:
            update_fields["mac_address"] = body.mac_address
        if body.model:
            update_fields["model"] = body.model
        if body.app_version:
            update_fields["app_version"] = body.app_version
        if body.platform:
            update_fields["platform"] = body.platform
        await db.device_trials.update_one(
            {"hardware_id": hw}, {"$set": update_fields}
        )

    # Trial length is platform-dependent. Prefer the platform the client
    # just reported; fall back to whatever's stored on the device row so
    # a TV that goes offline and reconnects still gets 15 (not 30) days.
    effective_platform = body.platform or existing.get("platform")
    trial_days = _trial_days(effective_platform)
    days_used = _days_between(existing["first_seen_at"], now)
    days_remaining = max(0, trial_days - days_used)
    trial_expired = days_remaining <= 0

    is_premium = bool(existing.get("is_premium", False))
    premium_expiry = existing.get("premium_expiry")
    # Auto-expire device-level premium if expiry is in the past.
    if is_premium and premium_expiry:
        try:
            if _parse_iso(premium_expiry) < now:
                is_premium = False
                await db.device_trials.update_one(
                    {"hardware_id": hw},
                    {"$set": {"is_premium": False}},
                )
        except Exception:
            pass

    return TrialStatusOut(
        hardware_id=hw,
        first_seen_at=existing["first_seen_at"],
        last_seen_at=now_iso,
        trial_days=trial_days,
        days_used=days_used,
        days_remaining=days_remaining,
        trial_expired=trial_expired,
        is_premium=is_premium,
        premium_expiry=premium_expiry,
    )


# ─────────────────────────────────────────────────────────────────────
# Admin endpoints — flip is_premium when an external license key is
# redeemed, list all known devices, or forget a device.
# ─────────────────────────────────────────────────────────────────────

class DeviceTrialAdminOut(BaseModel):
    hardware_id: str
    mac_address: Optional[str] = None
    model: Optional[str] = None
    app_version: Optional[str] = None
    first_seen_at: str
    last_seen_at: str
    is_premium: bool
    premium_expiry: Optional[str] = None
    days_used: int
    days_remaining: int
    trial_expired: bool


class DeviceTrialPatch(BaseModel):
    is_premium: Optional[bool] = None
    premium_expiry: Optional[str] = Field(
        default=None,
        description="ISO-8601 UTC timestamp. Send null to clear.",
    )
    note: Optional[str] = Field(default=None, max_length=500)


def _augment_admin_view(doc: dict) -> DeviceTrialAdminOut:
    """Add computed days_used / days_remaining / trial_expired fields."""
    now = datetime.now(timezone.utc)
    trial_days = _trial_days(doc.get("platform"))
    days_used = _days_between(doc["first_seen_at"], now)
    days_remaining = max(0, trial_days - days_used)
    return DeviceTrialAdminOut(
        hardware_id=doc["hardware_id"],
        mac_address=doc.get("mac_address"),
        model=doc.get("model"),
        app_version=doc.get("app_version"),
        first_seen_at=doc["first_seen_at"],
        last_seen_at=doc.get("last_seen_at", doc["first_seen_at"]),
        is_premium=bool(doc.get("is_premium", False)),
        premium_expiry=doc.get("premium_expiry"),
        days_used=days_used,
        days_remaining=days_remaining,
        trial_expired=days_remaining <= 0,
    )


@admin_router.get("", response_model=List[DeviceTrialAdminOut])
async def admin_list_device_trials(limit: int = 500):
    """List all known devices, newest-first by last_seen_at. Admin-only."""
    db = get_db()
    docs = (
        await db.device_trials.find({}, {"_id": 0})
        .sort("last_seen_at", -1)
        .to_list(min(max(limit, 1), 2000))
    )
    return [_augment_admin_view(d) for d in docs]


@admin_router.patch("/{hardware_id}", response_model=DeviceTrialAdminOut)
async def admin_patch_device_trial(hardware_id: str, body: DeviceTrialPatch):
    """Flip premium flag / set expiry / add a note. Used when a license
    key is redeemed on nadi.kr and the operator wants to unlock a device."""
    db = get_db()
    existing = await db.device_trials.find_one(
        {"hardware_id": hardware_id}, {"_id": 0}
    )
    if not existing:
        raise HTTPException(status_code=404, detail="device_trial not found")

    update: dict = {}
    if body.is_premium is not None:
        update["is_premium"] = bool(body.is_premium)
    if body.premium_expiry is not None:
        # Validate ISO format if non-empty.
        if body.premium_expiry == "" or body.premium_expiry is None:
            update["premium_expiry"] = None
        else:
            try:
                _parse_iso(body.premium_expiry)
            except Exception:
                raise HTTPException(
                    status_code=400, detail="Invalid premium_expiry ISO date"
                )
            update["premium_expiry"] = body.premium_expiry
    if body.note is not None:
        update["note"] = body.note

    if not update:
        raise HTTPException(status_code=400, detail="Nothing to update")

    await db.device_trials.update_one({"hardware_id": hardware_id}, {"$set": update})
    fresh = await db.device_trials.find_one({"hardware_id": hardware_id}, {"_id": 0})
    return _augment_admin_view(fresh)


@admin_router.delete("/{hardware_id}")
async def admin_delete_device_trial(hardware_id: str):
    """Forget a device (it will start a fresh trial on next contact)."""
    db = get_db()
    res = await db.device_trials.delete_one({"hardware_id": hardware_id})
    if res.deleted_count == 0:
        raise HTTPException(status_code=404, detail="device_trial not found")
    return {"ok": True}
