"""RBS REGAL — Licensing, Subscription Plans, Device Activation & Trial.

Backend-managed license server for the SaaS / desktop hybrid model.  Each
RBS REGAL deployment can be:

  • TRIAL   — auto-issued on first install, 30 days, all features.
  • BASIC   — single firm, basic modules, ₹499/mo equivalent.
  • PRO     — multi-firm, accounting, e-commerce, AI, ₹999/mo.
  • ENTERPRISE — unlimited everything + dealer/customer portals + multi-device.

Endpoints:
  • GET  /api/license/status            — current license + days left + features.
  • POST /api/license/activate          — apply a license key.
  • POST /api/license/start-trial       — start a fresh trial (idempotent).
  • GET  /api/license/plans             — public plan catalogue.
  • POST /api/license/devices/register  — bind current browser fingerprint.
  • GET  /api/license/devices           — list bound devices.
  • DELETE /api/license/devices/{id}    — unbind a device.

License keys are validated against an HMAC signature derived from JWT_SECRET
plus the embedded plan + expiry.  For real-world distribution you would mint
these from an external license server, but this approach lets RBS REGAL run
fully self-contained.
"""
import os
import hmac
import hashlib
import base64
import json
from datetime import datetime, timezone, timedelta
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from bson import ObjectId

from auth import get_current_user, require_admin

router = APIRouter(prefix="/api/license", tags=["license"])


def _secret() -> str:
    return os.environ.get("JWT_SECRET", "rm-regal-dev-secret")


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


def _now_iso() -> str:
    return _now().isoformat()


# ---- Plan Catalogue -----------------------------------------------------
PLANS = {
    "trial": {
        "key": "trial", "label": "Free Trial", "price_inr": 0, "period": "30 days", "color": "amber",
        "max_firms": 2, "max_users": 3, "max_devices": 2, "max_items": 500,
        "features": {
            "invoicing": True, "gst": True, "inventory": True, "payments": True,
            "accounting": True, "barcode": True, "godowns": True,
            "ai_assistant": True, "ecommerce": False, "portal": False,
            "dealer_portal": False, "multi_firm": False, "thermal_print": True,
            "ocr_scan": True, "whatsapp_reminders": True, "white_label": False,
        },
        "tagline": "Try every feature for 30 days. No credit card.",
    },
    "basic": {
        "key": "basic", "label": "Basic", "price_inr": 499, "period": "monthly", "color": "slate",
        "max_firms": 1, "max_users": 2, "max_devices": 2, "max_items": 5000,
        "features": {
            "invoicing": True, "gst": True, "inventory": True, "payments": True,
            "accounting": False, "barcode": True, "godowns": False,
            "ai_assistant": False, "ecommerce": False, "portal": False,
            "dealer_portal": False, "multi_firm": False, "thermal_print": True,
            "ocr_scan": False, "whatsapp_reminders": True, "white_label": False,
        },
        "tagline": "For single-shop retailers — GST billing & inventory.",
    },
    "pro": {
        "key": "pro", "label": "Pro", "price_inr": 999, "period": "monthly", "color": "emerald",
        "max_firms": 3, "max_users": 10, "max_devices": 5, "max_items": 50000,
        "features": {
            "invoicing": True, "gst": True, "inventory": True, "payments": True,
            "accounting": True, "barcode": True, "godowns": True,
            "ai_assistant": True, "ecommerce": True, "portal": True,
            "dealer_portal": False, "multi_firm": True, "thermal_print": True,
            "ocr_scan": True, "whatsapp_reminders": True, "white_label": False,
        },
        "tagline": "Most popular — AI insights, accounting, customer portal.",
        "badge": "POPULAR",
    },
    "enterprise": {
        "key": "enterprise", "label": "Enterprise", "price_inr": 2499, "period": "monthly", "color": "indigo",
        "max_firms": 999, "max_users": 100, "max_devices": 50, "max_items": 999999,
        "features": {
            "invoicing": True, "gst": True, "inventory": True, "payments": True,
            "accounting": True, "barcode": True, "godowns": True,
            "ai_assistant": True, "ecommerce": True, "portal": True,
            "dealer_portal": True, "multi_firm": True, "thermal_print": True,
            "ocr_scan": True, "whatsapp_reminders": True, "white_label": True,
        },
        "tagline": "Unlimited firms, multi-device sync, dealer portal & branding.",
    },
}


# ---- Key Format & Signing -----------------------------------------------
SIG_LEN = 16  # chars of HMAC kept in the key tail


def _sign(payload: dict) -> str:
    """Generate `XXXX-XXXX-XXXX-XXXX` style license key signed with JWT_SECRET."""
    msg = json.dumps(payload, sort_keys=True).encode()
    sig = hmac.new(_secret().encode(), msg, hashlib.sha256).digest()
    short = base64.b32encode(sig).decode().rstrip("=")[:SIG_LEN]
    body = base64.b32encode(msg).decode().rstrip("=")
    raw = (body + short).upper()
    chunks = [raw[i:i + 4] for i in range(0, len(raw), 4)]
    return "-".join(chunks)


def _decode(key: str) -> Optional[dict]:
    """Decode + verify a license key.  Returns payload or None on failure."""
    try:
        normalized = key.replace("-", "").replace(" ", "").upper()
        if len(normalized) < SIG_LEN + 4:
            return None
        body, sig = normalized[:-SIG_LEN], normalized[-SIG_LEN:]
        # Base32 needs padding to multiple of 8
        padded = body + "=" * ((8 - len(body) % 8) % 8)
        raw = base64.b32decode(padded.encode())
        payload = json.loads(raw)
        expected = hmac.new(_secret().encode(), json.dumps(payload, sort_keys=True).encode(), hashlib.sha256).digest()
        expected_short = base64.b32encode(expected).decode().rstrip("=")[:SIG_LEN]
        if expected_short.upper() == sig:
            return payload
    except Exception:
        return None
    return None


def _make_license(plan: str, days: int, owner: str = "RGE REGALGOA Customer") -> str:
    return _sign({"plan": plan, "owner": owner, "expires": (_now() + timedelta(days=days)).isoformat(), "issued": _now().isoformat()})


# ---- Models -------------------------------------------------------------
class ActivateIn(BaseModel):
    key: str
    owner_name: Optional[str] = ""


class DeviceIn(BaseModel):
    fingerprint: str
    name: Optional[str] = ""
    user_agent: Optional[str] = ""


class DeviceSyncToggle(BaseModel):
    sync_enabled: bool


class DeviceRename(BaseModel):
    name: str


# ---- Helpers ------------------------------------------------------------
async def _get_license_doc(db) -> dict:
    """Load the singleton license record.  Auto-creates a 30-day trial if missing."""
    doc = await db.license.find_one({"_id": "active"})
    if not doc:
        trial_key = _make_license("trial", 30, "RGE REGALGOA Trial")
        payload = _decode(trial_key) or {"plan": "trial", "expires": (_now() + timedelta(days=30)).isoformat()}
        doc = {
            "_id": "active",
            "key": trial_key,
            "plan": payload["plan"],
            "owner": payload.get("owner", "Trial"),
            "issued": payload.get("issued", _now_iso()),
            "expires": payload["expires"],
            "activated_at": _now_iso(),
            "trial_used": True,
            "is_trial": True,
        }
        await db.license.insert_one(doc)
    # Internal helper — `_id` is the literal string "active" (not an ObjectId).
    # Return a shallow copy so the rest of the module can mutate freely.
    return dict(doc)


def _plan_info(plan_key: str) -> dict:
    return PLANS.get(plan_key, PLANS["trial"])


def _days_left(expires_iso: Optional[str]) -> int:
    if not expires_iso:
        return 0
    try:
        exp = datetime.fromisoformat(expires_iso)
        if exp.tzinfo is None:
            exp = exp.replace(tzinfo=timezone.utc)
        delta = exp - _now()
        return max(0, int(delta.total_seconds() // 86400))
    except Exception:
        return 0


# ---- Endpoints ----------------------------------------------------------
@router.get("/plans")
async def plans():
    """Public — no auth required so the marketing page can show them."""
    return {"plans": list(PLANS.values()), "current_count": len(PLANS)}


@router.get("/status")
async def license_status(request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    doc = await _get_license_doc(db)
    info = _plan_info(doc["plan"])
    days = _days_left(doc.get("expires"))
    return {
        "plan": doc["plan"],
        "label": info["label"],
        "owner": doc.get("owner", ""),
        "issued": doc.get("issued"),
        "expires": doc.get("expires"),
        "days_left": days,
        "is_trial": doc.get("is_trial", doc["plan"] == "trial"),
        "is_active": days > 0,
        "limits": {
            "max_firms": info["max_firms"], "max_users": info["max_users"],
            "max_devices": info["max_devices"], "max_items": info["max_items"],
        },
        "features": info["features"],
        "tagline": info.get("tagline"),
        "key_preview": (doc.get("key") or "")[:9] + "…",
    }


@router.post("/activate")
async def activate(payload: ActivateIn, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    decoded = _decode(payload.key)
    if not decoded:
        raise HTTPException(400, "Invalid or tampered license key")
    if decoded.get("plan") not in PLANS:
        raise HTTPException(400, f"Unknown plan in key: {decoded.get('plan')}")
    expires = decoded.get("expires")
    if _days_left(expires) <= 0:
        raise HTTPException(400, f"License has expired on {expires}")
    # Preserve `trial_used` flag from any prior license — burning a trial is one-shot per install.
    prior = await db.license.find_one({"_id": "active"})
    prior_trial_used = bool((prior or {}).get("trial_used"))
    new_doc = {
        "_id": "active",
        "key": payload.key,
        "plan": decoded["plan"],
        "owner": payload.owner_name or decoded.get("owner", "RGE REGALGOA Customer"),
        "issued": decoded.get("issued", _now_iso()),
        "expires": expires,
        "activated_at": _now_iso(),
        "activated_by": user["email"],
        "is_trial": decoded["plan"] == "trial",
        # Once any trial has been issued, mark for life so users can't reset the clock.
        "trial_used": prior_trial_used or decoded["plan"] == "trial",
    }
    await db.license.replace_one({"_id": "active"}, new_doc, upsert=True)
    info = _plan_info(decoded["plan"])
    return {"ok": True, "plan": decoded["plan"], "label": info["label"], "days_left": _days_left(expires), "expires": expires}


@router.post("/start-trial")
async def start_trial(request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    existing = await db.license.find_one({"_id": "active"})
    if existing and existing.get("trial_used"):
        raise HTTPException(409, "Trial already used on this account")
    trial_key = _make_license("trial", 30, user["email"])
    decoded = _decode(trial_key)
    doc = {
        "_id": "active",
        "key": trial_key,
        "plan": "trial",
        "owner": user["email"],
        "issued": decoded["issued"],
        "expires": decoded["expires"],
        "activated_at": _now_iso(),
        "trial_used": True,
        "is_trial": True,
    }
    await db.license.replace_one({"_id": "active"}, doc, upsert=True)
    return {"ok": True, "plan": "trial", "days_left": 30, "key": trial_key}


@router.post("/generate-demo-key")
async def generate_demo_key(plan: str = "pro", days: int = 365, user=Depends(require_admin)):
    """Admin-only helper to mint a working license key for testing each plan."""
    if plan not in PLANS:
        raise HTTPException(400, "Unknown plan")
    return {"key": _make_license(plan, days, user["email"]), "plan": plan, "days": days}


# ---- Device Activation --------------------------------------------------
def _detect_device_kind(ua: str) -> dict:
    """Best-effort device classification from user-agent string."""
    ua_low = (ua or "").lower()
    if "iphone" in ua_low or "ipod" in ua_low:
        return {"kind": "mobile", "os": "iOS", "icon": "smartphone"}
    if "ipad" in ua_low:
        return {"kind": "tablet", "os": "iPadOS", "icon": "tablet"}
    if "android" in ua_low:
        kind = "tablet" if "tablet" in ua_low or ("mobile" not in ua_low) else "mobile"
        return {"kind": kind, "os": "Android", "icon": "smartphone" if kind == "mobile" else "tablet"}
    if "windows" in ua_low:
        return {"kind": "desktop", "os": "Windows", "icon": "monitor"}
    if "mac os" in ua_low or "macintosh" in ua_low:
        return {"kind": "desktop", "os": "macOS", "icon": "monitor"}
    if "linux" in ua_low or "x11" in ua_low:
        return {"kind": "desktop", "os": "Linux", "icon": "monitor"}
    return {"kind": "unknown", "os": "Unknown", "icon": "globe"}


def _short_browser(ua: str) -> str:
    """Pick the dominant browser token for display."""
    ua_low = (ua or "").lower()
    if "edg/" in ua_low or "edge/" in ua_low:
        return "Edge"
    if "chrome" in ua_low and "safari" in ua_low:
        return "Chrome"
    if "firefox" in ua_low:
        return "Firefox"
    if "safari" in ua_low:
        return "Safari"
    if "opera" in ua_low or "opr/" in ua_low:
        return "Opera"
    return "Browser"


@router.post("/devices/register")
async def register_device(payload: DeviceIn, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    lic = await _get_license_doc(db)
    info = _plan_info(lic["plan"])
    ua = payload.user_agent or request.headers.get("user-agent", "")
    ip = request.client.host if request.client else ""
    kind_info = _detect_device_kind(ua)
    browser = _short_browser(ua)
    existing = await db.devices.find_one({"fingerprint": payload.fingerprint})
    if existing:
        await db.devices.update_one(
            {"_id": existing["_id"]},
            {"$set": {
                "last_seen": _now_iso(),
                "last_user": user["email"],
                "last_ip": ip,
                "user_agent": ua,
                "kind": kind_info["kind"],
                "os": kind_info["os"],
                "icon": kind_info["icon"],
                "browser": browser,
            }},
        )
        return {"ok": True, "device_id": str(existing["_id"]), "new": False,
                "sync_enabled": existing.get("sync_enabled", True)}
    # New device — enforce plan device cap
    count = await db.devices.count_documents({})
    if count >= info["max_devices"]:
        raise HTTPException(409, f"Device limit reached ({info['max_devices']}). Upgrade your plan or remove an old device.")
    doc = {
        "fingerprint": payload.fingerprint,
        "name": payload.name or f"{kind_info['os']} {browser}",
        "user_agent": ua,
        "kind": kind_info["kind"],
        "os": kind_info["os"],
        "icon": kind_info["icon"],
        "browser": browser,
        "sync_enabled": True,                          # Admin can toggle later
        "registered_at": _now_iso(),
        "registered_by": user["email"],
        "last_seen": _now_iso(),
        "last_user": user["email"],
        "last_ip": ip,
    }
    r = await db.devices.insert_one(doc)
    return {"ok": True, "device_id": str(r.inserted_id), "new": True, "sync_enabled": True}


@router.get("/devices")
async def list_devices(request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    docs = await db.devices.find({}).sort("last_seen", -1).to_list(200)
    for d in docs:
        d["id"] = str(d.pop("_id"))
        d.setdefault("kind", "unknown")
        d.setdefault("os", "Unknown")
        d.setdefault("icon", "globe")
        d.setdefault("browser", "Browser")
        d.setdefault("sync_enabled", True)
    lic = await _get_license_doc(db)
    info = _plan_info(lic["plan"])
    return {"devices": docs, "count": len(docs), "max": info["max_devices"]}


@router.patch("/devices/{device_id}/sync")
async def toggle_device_sync(device_id: str, payload: DeviceSyncToggle, request: Request, user=Depends(require_admin)):
    """Admin-only — turn sync ON / OFF for a specific device."""
    db = request.app.state.db
    try:
        oid = ObjectId(device_id)
    except Exception:
        raise HTTPException(400, "Invalid device id")
    r = await db.devices.update_one(
        {"_id": oid},
        {"$set": {"sync_enabled": bool(payload.sync_enabled), "sync_updated_at": _now_iso(), "sync_updated_by": user["email"]}},
    )
    if r.matched_count == 0:
        raise HTTPException(404, "Device not found")
    return {"ok": True, "sync_enabled": payload.sync_enabled}


@router.patch("/devices/{device_id}/rename")
async def rename_device(device_id: str, payload: DeviceRename, request: Request, user=Depends(require_admin)):
    """Admin-only — give the device a friendly name (e.g. 'Vasant's iPhone')."""
    db = request.app.state.db
    try:
        oid = ObjectId(device_id)
    except Exception:
        raise HTTPException(400, "Invalid device id")
    name = (payload.name or "").strip()[:60]
    if not name:
        raise HTTPException(422, "Name cannot be empty")
    r = await db.devices.update_one({"_id": oid}, {"$set": {"name": name}})
    if r.matched_count == 0:
        raise HTTPException(404, "Device not found")
    return {"ok": True, "name": name}


@router.get("/devices/me/sync-status")
async def my_sync_status(request: Request, user=Depends(get_current_user)):
    """Frontend pings this — if sync_enabled=false, the offline sync engine pauses."""
    db = request.app.state.db
    fp = request.headers.get("x-device-fingerprint") or request.cookies.get("device_fp")
    if not fp:
        return {"sync_enabled": True, "registered": False}     # Unregistered devices are permissive
    d = await db.devices.find_one({"fingerprint": fp})
    if not d:
        return {"sync_enabled": True, "registered": False}
    return {
        "sync_enabled": bool(d.get("sync_enabled", True)),
        "registered": True,
        "device_id": str(d["_id"]),
        "name": d.get("name", ""),
    }


@router.delete("/devices/{device_id}")
async def remove_device(device_id: str, request: Request, user=Depends(require_admin)):
    db = request.app.state.db
    try:
        oid = ObjectId(device_id)
    except Exception:
        raise HTTPException(400, "Invalid device id")
    r = await db.devices.delete_one({"_id": oid})
    if r.deleted_count == 0:
        raise HTTPException(404, "Device not found")
    return {"ok": True}
