# =============================================================================
#  license_server/main.py — AR Electronics License Server
# =============================================================================
#
#  FastAPI REST service that manages serial number activations for all
#  AR Electronics products deployed on J6412 mini PCs.
#
#  Endpoints (public):
#    POST   /api/v1/activate          — activate a serial on a machine
#    GET    /api/v1/validate/{serial} — check activation status
#
#  Endpoints (admin — require X-Admin-Key header):
#    GET    /api/v1/admin/licenses       — list all issued licenses
#    GET    /api/v1/admin/activations    — list all activations
#    POST   /api/v1/admin/issue         — issue a new serial number
#    DELETE /api/v1/admin/revoke/{serial} — revoke a license
#
#  Run:
#    uvicorn license_server.main:app --host 0.0.0.0 --port 8888 --reload
#
#  Environment variables:
#    DATABASE_URL  — SQLAlchemy URL (default: sqlite:///./ar_licenses.db)
#    ADMIN_API_KEY — Secret key for /admin/* endpoints
# =============================================================================

from __future__ import annotations

import os
import uuid
from datetime import datetime, timezone

from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from sqlalchemy.orm import Session

from .database import Base, engine, get_db
from .models import Activation, License
from .schemas import (
    ActivationRequest,
    ActivationAdminRecord,
    ActivationResponse,
    LicenseAdminRecord,
    ValidateResponse,
)

# ── Create tables on startup ─────────────────────────────────────────────────
Base.metadata.create_all(bind=engine)

# ── App ──────────────────────────────────────────────────────────────────────
app = FastAPI(
    title="AR Electronics License Server",
    description="Serial-number activation and validation for J6412 deployments.",
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["GET", "POST", "DELETE"],
    allow_headers=["*"],
)

ADMIN_KEY = os.getenv("ADMIN_API_KEY", "change-me-in-production")


# ---------------------------------------------------------------------------
#  Auth helpers
# ---------------------------------------------------------------------------

def require_admin(x_admin_key: str = Header(...)):
    if x_admin_key != ADMIN_KEY:
        raise HTTPException(status_code=403, detail="Invalid admin key.")


# ---------------------------------------------------------------------------
#  Admin request schemas (defined here — too small for schemas.py)
# ---------------------------------------------------------------------------

class IssueRequest(BaseModel):
    serial: str
    vessel: str = ""
    notes:  str = ""


# ---------------------------------------------------------------------------
#  Public — Activation
# ---------------------------------------------------------------------------

@app.post("/api/v1/activate", response_model=ActivationResponse, tags=["Public"])
def activate(request: ActivationRequest, db: Session = Depends(get_db)):
    """
    Activate a serial number on a specific hardware machine.

    - First activation: accepted, creates a new Activation row.
    - Same hardware re-activating the same serial: accepted, updates last_seen.
    - Different hardware trying to activate an already-activated serial: rejected.
    """
    serial = request.serial.upper()

    # Verify the serial exists and is not revoked
    lic = db.query(License).filter(License.serial == serial).first()
    if lic is None:
        raise HTTPException(status_code=404, detail="Serial number not found.")
    if not lic.is_active:
        raise HTTPException(status_code=403, detail="This license has been revoked.")

    hw_id = request.hardware_id

    # Check for an existing activation
    existing = (
        db.query(Activation)
        .filter(Activation.serial == serial, Activation.revoked == False)  # noqa: E712
        .first()
    )

    if existing is not None:
        if existing.hardware_id == hw_id:
            # Same machine re-activating — refresh heartbeat
            existing.last_seen_at = datetime.now(timezone.utc)
            existing.app_version  = request.app_version or existing.app_version
            db.commit()
            db.refresh(existing)
            return ActivationResponse(
                activation_id = existing.activation_id,
                vessel_slot   = existing.vessel_slot,
                licensed_to   = existing.licensed_to,
                activated_at  = existing.activated_at,
            )
        else:
            raise HTTPException(
                status_code=409,
                detail=(
                    "Este número de serie ya está activado en otro equipo. "
                    "Contacte a AR Electronics para transferir la licencia."
                ),
            )

    # New activation
    activation = Activation(
        activation_id = str(uuid.uuid4()),
        serial        = serial,
        hardware_id   = hw_id,
        app_version   = request.app_version,
        platform      = request.platform,
        hostname      = request.hostname,
        vessel_slot   = 1,
        licensed_to   = lic.vessel,
    )
    db.add(activation)
    db.commit()
    db.refresh(activation)

    return ActivationResponse(
        activation_id = activation.activation_id,
        vessel_slot   = activation.vessel_slot,
        licensed_to   = activation.licensed_to,
        activated_at  = activation.activated_at,
    )


# ---------------------------------------------------------------------------
#  Public — Validation
# ---------------------------------------------------------------------------

@app.get("/api/v1/validate/{serial}", response_model=ValidateResponse, tags=["Public"])
def validate(
    serial: str,
    hardware_id: str = Query(..., min_length=16),
    db: Session = Depends(get_db),
):
    """
    Check whether a (serial, hardware_id) pair is currently active.

    Called by the installed app on each boot to refresh its offline cache.
    """
    serial = serial.upper()

    activation = (
        db.query(Activation)
        .filter(
            Activation.serial      == serial,
            Activation.hardware_id == hardware_id,
            Activation.revoked     == False,  # noqa: E712
        )
        .first()
    )

    if activation is None:
        raise HTTPException(status_code=404, detail="No active activation found.")

    activation.last_seen_at = datetime.now(timezone.utc)
    db.commit()

    return ValidateResponse(
        serial        = activation.serial,
        active        = True,
        hardware_id   = activation.hardware_id,
        activation_id = activation.activation_id,
        vessel_slot   = activation.vessel_slot,
        licensed_to   = activation.licensed_to,
        activated_at  = activation.activated_at,
        last_seen_at  = activation.last_seen_at,
    )


# ---------------------------------------------------------------------------
#  Admin — Issue new serial
# ---------------------------------------------------------------------------

@app.post("/api/v1/admin/issue",
          tags=["Admin"],
          dependencies=[Depends(require_admin)])
def issue_license(request: IssueRequest, db: Session = Depends(get_db)):
    """Register a pre-generated serial number in the database."""
    serial = request.serial.upper()
    if db.query(License).filter(License.serial == serial).first():
        raise HTTPException(status_code=409, detail="Serial already in database.")
    lic = License(serial=serial, vessel=request.vessel, notes=request.notes, is_active=True)
    db.add(lic)
    db.commit()
    return {"status": "issued", "serial": lic.serial}


# ---------------------------------------------------------------------------
#  Admin — List licenses
# ---------------------------------------------------------------------------

@app.get("/api/v1/admin/licenses",
         response_model=list[LicenseAdminRecord],
         tags=["Admin"],
         dependencies=[Depends(require_admin)])
def list_licenses(db: Session = Depends(get_db)):
    licenses = db.query(License).order_by(License.issued_at.desc()).all()
    result = []
    for lic in licenses:
        count = db.query(Activation).filter(
            Activation.serial == lic.serial, Activation.revoked == False  # noqa: E712
        ).count()
        result.append(
            LicenseAdminRecord(
                serial      = lic.serial,
                vessel      = lic.vessel,
                issued_at   = lic.issued_at,
                is_active   = lic.is_active,
                activations = count,
            )
        )
    return result


# ---------------------------------------------------------------------------
#  Admin — List activations
# ---------------------------------------------------------------------------

@app.get("/api/v1/admin/activations",
         response_model=list[ActivationAdminRecord],
         tags=["Admin"],
         dependencies=[Depends(require_admin)])
def list_activations(db: Session = Depends(get_db)):
    rows = db.query(Activation).order_by(Activation.activated_at.desc()).all()
    return [
        ActivationAdminRecord(
            activation_id = r.activation_id,
            serial        = r.serial,
            hardware_id   = r.hardware_id,
            app_version   = r.app_version,
            platform      = r.platform,
            hostname      = r.hostname,
            activated_at  = r.activated_at,
            last_seen_at  = r.last_seen_at,
            vessel_slot   = r.vessel_slot,
            revoked       = r.revoked,
            licensed_to   = r.licensed_to,
        )
        for r in rows
    ]


# ---------------------------------------------------------------------------
#  Admin — Revoke
# ---------------------------------------------------------------------------

@app.delete("/api/v1/admin/revoke/{serial}",
            tags=["Admin"],
            dependencies=[Depends(require_admin)])
def revoke_license(serial: str, db: Session = Depends(get_db)):
    """Revoke a license — all activations are invalidated immediately."""
    serial = serial.upper()
    lic = db.query(License).filter(License.serial == serial).first()
    if lic is None:
        raise HTTPException(status_code=404, detail="Serial not found.")
    lic.is_active = False
    db.query(Activation).filter(Activation.serial == serial).update({"revoked": True})
    db.commit()
    return {"status": "revoked", "serial": serial}


# ---------------------------------------------------------------------------
#  Health check
# ---------------------------------------------------------------------------

@app.get("/health", tags=["System"])
def health():
    return {"status": "ok", "service": "AR Electronics License Server"}
