"""
Gold AutoTrade License Server — FastAPI + uvicorn

Endpoints:
  POST /activate            Client activation (license key + hardware ID) → HMAC-signed
  POST /validate            Periodic online validation + revocation check
  GET  /health              Health check
  GET  /admin/licenses      List all licenses (admin, X-Admin-Token)
  POST /admin/revoke        Revoke a license (admin)
  GET  /api/update/manifest   App update manifest
  GET  /api/update/download/{filename}  Download release
  POST /api/update/upload    Upload release (admin)
  GET  /api/server/status     Server status
"""
import hashlib
import hmac
import json
import os
from datetime import datetime, timezone
from pathlib import Path

from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse

from store import (
    activate,
    check_online,
    get_license,
    init_db,
    list_licenses,
    revoke_license,
)
from config import SIGNING_SECRET, PRODUCT_ID

ADMIN_TOKEN = os.environ.get("GAT_ADMIN_TOKEN", "change-me-admin-token")

# Update server paths
RELEASES_DIR = Path(os.environ.get("GAT_RELEASES_DIR", "/root/license_server/releases"))
STATIC_MANIFEST_PATH = Path("/root/license_server/update_manifest.json")

app = FastAPI(title="Gold AutoTrade License Server")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ─── Helpers ─────────────────────────────────────────────────────────

def _sign(payload: dict) -> str:
    data = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode()
    return hmac.new(SIGNING_SECRET.encode(), data, hashlib.sha256).hexdigest()


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _platform_key(filename: str) -> str:
    name = filename.lower()
    if "macos" in name or "mac" in name:
        return "macos-arm64" if "arm64" in name else "macos-x64"
    if "win" in name or "windows" in name:
        return "windows-x64"
    return "linux-x64"


def _compare_versions(a: str, b: str) -> int:
    def parse(v):
        try:
            return tuple(int(x) for x in str(v).split("."))
        except Exception:
            return (0, 0, 0)
    pa, pb = parse(a), parse(b)
    return (pa > pb) - (pa < pb)


def _sha256_file(file_path: Path) -> str:
    h = hashlib.sha256()
    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()


# ─── License Endpoints ───────────────────────────────────────────────

@app.post("/activate")
async def handle_activate(request: Request):
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"ok": False, "error": "INVALID_JSON"}, status_code=400)

    license_key = str(body.get("license_key", "")).strip()
    hardware_id = str(body.get("hardware_id", "")).strip()
    machine_name = str(body.get("machine_name", "")).strip()

    if not license_key or not hardware_id:
        return JSONResponse(
            {"ok": False, "error": "MISSING_FIELDS",
             "message": "license_key and hardware_id are required."},
            status_code=400,
        )

    result = activate(license_key, hardware_id, machine_name)
    if result["ok"]:
        payload = result["license"]
        payload["hardware_id"] = hardware_id
        payload["activated_at"] = _now_iso()
        result["signed_license"] = {
            "payload": payload,
            "signature": _sign(payload),
        }
        return result
    else:
        code = 409 if result["error"] in (
            "LICENSE_EXPIRED", "LICENSE_REVOKED", "MACHINE_LIMIT_REACHED"
        ) else 404
        return JSONResponse(result, status_code=code)


@app.post("/validate")
async def handle_validate(request: Request):
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"ok": False, "error": "INVALID_JSON"}, status_code=400)

    license_key = str(body.get("license_key", "")).strip()
    hardware_id = str(body.get("hardware_id", "")).strip()

    if not license_key or not hardware_id:
        return JSONResponse({"ok": False, "error": "MISSING_FIELDS"}, status_code=400)

    result = check_online(license_key, hardware_id)
    if result["ok"]:
        result["signature"] = _sign(result["license"])
    return JSONResponse(result, status_code=200 if result["ok"] else 403)


@app.get("/health")
def health():
    return {"ok": True, "product": PRODUCT_ID}


@app.get("/admin/licenses")
def handle_admin_list(request: Request):
    token = request.headers.get("X-Admin-Token", "")
    if token != ADMIN_TOKEN:
        return JSONResponse({"ok": False, "error": "UNAUTHORIZED"}, status_code=403)
    licenses = list_licenses(include_revoked=True)
    return {"ok": True, "licenses": licenses}


@app.post("/admin/revoke")
async def handle_admin_revoke(request: Request):
    token = request.headers.get("X-Admin-Token", "")
    if token != ADMIN_TOKEN:
        return JSONResponse({"ok": False, "error": "UNAUTHORIZED"}, status_code=403)

    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"ok": False, "error": "INVALID_JSON"}, status_code=400)

    license_key = str(body.get("license_key", "")).strip()
    reason = str(body.get("reason", "")).strip()
    if not license_key:
        return JSONResponse({"ok": False, "error": "MISSING_FIELDS"}, status_code=400)

    ok = revoke_license(license_key, reason)
    if ok:
        return {"ok": True, "message": f"Revoked: {license_key}"}
    return JSONResponse({"ok": False, "error": "NOT_FOUND"}, status_code=404)


# ─── Update Endpoints ────────────────────────────────────────────────

def build_manifest():
    # Prefer static manifest deployed via deploy_release.sh
    if STATIC_MANIFEST_PATH.exists():
        try:
            manifest = json.loads(STATIC_MANIFEST_PATH.read_text())
            # Patch download URLs to match server endpoint format
            for channel, pkg in manifest.get("packages", {}).items():
                filename = pkg.get("filename", "")
                if filename and not pkg.get("url", "").startswith("/api/update/download/"):
                    pkg["url"] = f"/api/update/download/{filename}"
            return manifest
        except Exception:
            pass

    # Fallback: scan releases directory dynamically
    if not RELEASES_DIR.exists():
        return {"version": "0.0.0", "channel": "stable", "packages": {}}
    packages = {}
    latest_ver = "0.0.0"
    for f in sorted(RELEASES_DIR.iterdir()):
        if not f.is_file() or f.suffix != ".zip":
            continue
        name = f.stem
        ver = "0.0.0"
        if "_v" in name:
            base = name.split("_v")[-1]
            ver = base.split("_")[0].replace(".zip", "")
        if _compare_versions(ver, latest_ver) > 0:
            latest_ver = ver
        pk = _platform_key(f.name)
        if pk == "unknown":
            continue
        packages[pk] = {
            "version": ver,
            "url": f"/api/update/download/{f.name}",
            "sha256": _sha256_file(f),
            "notes": "",
        }
    return {
        "version": latest_ver,
        "stable": latest_ver,
        "channel": "stable",
        "notes": "",
        "packages": packages,
    }


@app.get("/api/update/manifest")
def get_update_manifest():
    return build_manifest()


@app.get("/api/update/download/{filename:path}")
def download_release(filename: str):
    file_path = RELEASES_DIR / filename
    if not file_path.exists() or not file_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(
        str(file_path),
        media_type="application/octet-stream",
        filename=filename,
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@app.post("/api/update/upload")
async def upload_release(file: UploadFile = File(...), admin_token: str = Form(...)):
    if admin_token != ADMIN_TOKEN:
        raise HTTPException(status_code=403, detail="Forbidden")
    RELEASES_DIR.mkdir(parents=True, exist_ok=True)
    dest = RELEASES_DIR / file.filename
    content = await file.read()
    with open(dest, "wb") as out:
        out.write(content)
    return {"success": True, "filename": file.filename, "size": len(content)}


@app.get("/api/server/status")
def server_status():
    manifest = build_manifest()
    releases_list = []
    if RELEASES_DIR.exists():
        releases_list = [f.name for f in sorted(RELEASES_DIR.iterdir()) if f.suffix == ".zip"]
    return {
        "server": "Gold AutoTrade License Center",
        "product": PRODUCT_ID,
        "releases_count": len(releases_list),
        "releases": releases_list,
        "latest_version": manifest.get("version", "0.0.0"),
    }


# ─── Startup ──────────────────────────────────────────────────────────

@app.on_event("startup")
def startup():
    init_db()
