"""
TimeMaster CLI – Kiosk-Geräteverwaltung

Verwendung (auf dem Server):
    cd /opt/timemaster/backend
    source venv/bin/activate
    python cli.py kiosk list
    python cli.py kiosk add --company "Acme GmbH" --name "Eingang" --pubkey ~/.ssh/kiosk.pub
"""

import asyncio
import base64
import hashlib
import ipaddress
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich import box

# ── Projekt-Imports ───────────────────────────────────────────────────────────
# Sicherstellen, dass das backend/-Verzeichnis im Python-Pfad ist,
# damit app.* importiert werden kann.
_HERE = Path(__file__).parent.resolve()
if str(_HERE) not in sys.path:
    sys.path.insert(0, str(_HERE))

# .env im selben Verzeichnis laden, bevor pydantic-settings greift
_env_file = _HERE / ".env"
if _env_file.exists():
    # Minimales manuelles Laden, damit DATABASE_URL vor dem Settings-Import steht
    import os
    for line in _env_file.read_text().splitlines():
        line = line.strip()
        if line and not line.startswith("#") and "=" in line:
            key, _, val = line.partition("=")
            key = key.strip()
            val = val.strip().strip('"').strip("'")
            if key not in os.environ:
                os.environ[key] = val

from sqlalchemy import select, text, update
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.core.config import settings
from app.models.company import Company
from app.models.kiosk_device import KioskDevice, KioskDeviceStatus

# ── Typer Apps ────────────────────────────────────────────────────────────────

app = typer.Typer(
    name="timemaster",
    help="TimeMaster CLI – Verwaltungstool für den Server",
    no_args_is_help=True,
    add_completion=False,
)

kiosk_app = typer.Typer(
    help="Kiosk-Geräteverwaltung",
    no_args_is_help=True,
)
app.add_typer(kiosk_app, name="kiosk")

console = Console()
err_console = Console(stderr=True)


# ── DB-Hilfsfunktionen ────────────────────────────────────────────────────────

def _make_engine():
    """Erstellt einen AsyncEngine auf Basis der aktuellen DATABASE_URL."""
    db_url = settings.database_url
    return create_async_engine(db_url, pool_pre_ping=True, pool_size=2, max_overflow=2)


async def _open_session() -> AsyncSession:
    """Gibt eine neue AsyncSession zurück mit deaktiviertem RLS."""
    engine = _make_engine()
    Session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    session = Session()
    await session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
    return session


def _run(coro):
    """Blockierendes asyncio.run() mit sauberem Fehler-Handling."""
    try:
        return asyncio.run(coro)
    except Exception as exc:
        err_console.print(f"[bold red]Fehler:[/bold red] {exc}")
        raise typer.Exit(1)


# ── Public-Key-Fingerprint (SHA-256, wie ssh-keygen -l -E sha256) ─────────────

def _pubkey_fingerprint(pubkey_str: str) -> str:
    """
    Berechnet SHA-256-Fingerprint eines OpenSSH-Public-Keys.
    Format: SHA256:<base64> (ohne trailing =, wie ssh-keygen -l -E sha256)
    Funktioniert für 'ssh-ed25519 AAAA...' und PEM-Blöcke.
    """
    pubkey_str = pubkey_str.strip()
    try:
        if pubkey_str.startswith("-----BEGIN"):
            # PEM → binär
            lines = [l for l in pubkey_str.splitlines() if not l.startswith("-----")]
            raw = base64.b64decode("".join(lines))
        else:
            # OpenSSH-Einzeiler: zweites Feld ist der Base64-Teil
            parts = pubkey_str.split()
            if len(parts) < 2:
                raise ValueError("Ungültiges Public-Key-Format")
            raw = base64.b64decode(parts[1])
        digest = hashlib.sha256(raw).digest()
        b64 = base64.b64encode(digest).decode().rstrip("=")
        return f"SHA256:{b64}"
    except Exception:
        return "(Fingerprint nicht berechenbar)"


# ── Heartbeat-Status-Hilfsfunktion ────────────────────────────────────────────

def _heartbeat_label(last_hb: Optional[datetime]) -> str:
    if last_hb is None:
        return "[dim]nie[/dim]"
    now = datetime.now(timezone.utc)
    if last_hb.tzinfo is None:
        last_hb = last_hb.replace(tzinfo=timezone.utc)
    delta = (now - last_hb).total_seconds()
    ts = last_hb.strftime("%d.%m.%Y %H:%M:%S")
    if delta < 90:
        return f"[green]online[/green] ({ts})"
    elif delta < 300:
        return f"[yellow]stale[/yellow] ({ts})"
    else:
        return f"[red]offline[/red] ({ts})"


def _status_icon(status: KioskDeviceStatus) -> str:
    return {
        KioskDeviceStatus.APPROVED: "[green]approved[/green]",
        KioskDeviceStatus.PENDING:  "[yellow]pending[/yellow]",
        KioskDeviceStatus.REVOKED:  "[red]revoked[/red]",
    }.get(status, status.value)


# ── CIDR-Validierung ──────────────────────────────────────────────────────────

def _validate_ip_whitelist(ip_whitelist: Optional[str]) -> None:
    """Wirft typer.BadParameter bei ungültiger CIDR-Notation."""
    if not ip_whitelist:
        return
    for cidr in ip_whitelist.split(","):
        cidr = cidr.strip()
        if not cidr:
            continue
        try:
            ipaddress.ip_network(cidr, strict=False)
        except ValueError:
            raise typer.BadParameter(
                f"Ungültige CIDR-Notation: '{cidr}'. "
                "Beispiel: 10.0.0.0/24,192.168.1.0/24"
            )


# ── Firma suchen ──────────────────────────────────────────────────────────────

async def _find_company(session: AsyncSession, company_name: str) -> Company:
    """
    Sucht Firma case-insensitiv. Fehler wenn 0 oder >1 Treffer.
    """
    result = await session.execute(
        select(Company).where(Company.name.ilike(f"%{company_name}%"))
    )
    companies = result.scalars().all()
    if not companies:
        raise typer.BadParameter(
            f"Keine Firma mit dem Namen '{company_name}' gefunden."
        )
    if len(companies) > 1:
        names = ", ".join(c.name for c in companies)
        raise typer.BadParameter(
            f"Mehrere Firmen gefunden: {names}\n"
            "Bitte den Namen genauer angeben."
        )
    return companies[0]


# ── Gerät suchen ──────────────────────────────────────────────────────────────

async def _find_device(session: AsyncSession, device_id: str) -> KioskDevice:
    try:
        dev_uuid = uuid.UUID(device_id)
    except ValueError:
        raise typer.BadParameter(f"Ungültige UUID: '{device_id}'")

    result = await session.execute(
        select(KioskDevice).where(KioskDevice.id == dev_uuid)
    )
    device = result.scalar_one_or_none()
    if device is None:
        raise typer.BadParameter(f"Kein Gerät mit ID '{device_id}' gefunden.")
    return device


# ── Subcommand: kiosk add ─────────────────────────────────────────────────────

@kiosk_app.command("add")
def kiosk_add(
    company: str = typer.Option(..., "--company", help="Firmenname (Teilübereinstimmung möglich)"),
    name: str = typer.Option(..., "--name", help="Name des Kiosk-Geräts, z.B. 'Eingang Berlin'"),
    location: Optional[str] = typer.Option(None, "--location", help="Standort-Beschreibung"),
    pubkey: Optional[Path] = typer.Option(None, "--pubkey", help="Pfad zur Public-Key-Datei (OpenSSH oder PEM)"),
    ip_whitelist: Optional[str] = typer.Option(None, "--ip-whitelist", help="CIDR-Liste, z.B. '10.0.0.0/24,192.168.1.0/24'"),
):
    """Neues Kiosk-Gerät registrieren (Status: pending)."""

    # Validierungen vor DB-Zugriff
    _validate_ip_whitelist(ip_whitelist)

    pubkey_str: Optional[str] = None
    if pubkey is not None:
        if not pubkey.exists():
            err_console.print(f"[bold red]Fehler:[/bold red] Public-Key-Datei nicht gefunden: {pubkey}")
            raise typer.Exit(1)
        pubkey_str = pubkey.read_text().strip()
        if not pubkey_str:
            err_console.print("[bold red]Fehler:[/bold red] Public-Key-Datei ist leer.")
            raise typer.Exit(1)

    async def _add():
        session = await _open_session()
        try:
            firm = await _find_company(session, company)

            device = KioskDevice(
                company_id=firm.id,
                name=name,
                location=location,
                status=KioskDeviceStatus.PENDING,
                public_key=pubkey_str,
                key_algorithm="ed25519",
                ip_whitelist=ip_whitelist,
            )
            session.add(device)
            await session.commit()
            await session.refresh(device)
            return device, firm
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

    device, firm = _run(_add())

    console.print()
    console.print(Panel(
        f"[bold green]✓ Gerät erfolgreich angelegt[/bold green]\n\n"
        f"  [bold]Name:[/bold]      {device.name}\n"
        f"  [bold]ID:[/bold]        {device.id}\n"
        f"  [bold]Firma:[/bold]     {firm.name}\n"
        f"  [bold]Standort:[/bold]  {device.location or '—'}\n"
        f"  [bold]Status:[/bold]    {_status_icon(device.status)}\n"
        + (f"  [bold]Fingerprint:[/bold] {_pubkey_fingerprint(device.public_key)}\n" if device.public_key else "  [bold]Public Key:[/bold] [dim]nicht gesetzt[/dim]\n")
        + (f"  [bold]IP-Whitelist:[/bold] {device.ip_whitelist}\n" if device.ip_whitelist else ""),
        title="Kiosk-Gerät angelegt",
        border_style="green",
    ))
    console.print()
    console.print("[yellow]Hinweis:[/yellow] Das Gerät wartet auf Admin-Freigabe im Web-Interface.")
    if not pubkey_str:
        console.print("[yellow]Hinweis:[/yellow] Kein Public Key gesetzt. Der Enrollment-Flow muss im Browser abgeschlossen werden.")
    console.print()


# ── Subcommand: kiosk list ────────────────────────────────────────────────────

@kiosk_app.command("list")
def kiosk_list(
    company: Optional[str] = typer.Option(None, "--company", help="Nur Geräte dieser Firma anzeigen"),
    status: Optional[str] = typer.Option(None, "--status", help="Filter: pending|approved|revoked"),
):
    """Alle registrierten Kiosk-Geräte auflisten."""

    # Status-Enum validieren
    status_filter: Optional[KioskDeviceStatus] = None
    if status is not None:
        try:
            status_filter = KioskDeviceStatus(status.lower())
        except ValueError:
            err_console.print(
                f"[bold red]Fehler:[/bold red] Ungültiger Status '{status}'. "
                "Erlaubt: pending, approved, revoked"
            )
            raise typer.Exit(1)

    async def _list():
        session = await _open_session()
        try:
            q = select(KioskDevice, Company).join(Company, KioskDevice.company_id == Company.id)
            if status_filter is not None:
                q = q.where(KioskDevice.status == status_filter)
            if company:
                q = q.where(Company.name.ilike(f"%{company}%"))
            q = q.order_by(Company.name, KioskDevice.name)
            result = await session.execute(q)
            return result.all()
        finally:
            await session.close()

    rows = _run(_list())

    if not rows:
        console.print("[dim]Keine Geräte gefunden.[/dim]")
        return

    table = Table(
        show_header=True,
        header_style="bold cyan",
        box=box.ROUNDED,
        show_lines=False,
    )
    table.add_column("ID", style="dim", min_width=8, max_width=36, no_wrap=True)
    table.add_column("Firma", min_width=10)
    table.add_column("Name", min_width=12)
    table.add_column("Standort")
    table.add_column("Status", min_width=10)
    table.add_column("Heartbeat", min_width=14)
    table.add_column("Key-Algo", justify="center")

    for device, firm in rows:
        # UUID kurz anzeigen (erste 8 Zeichen + ...)
        short_id = str(device.id)[:8] + "…"
        table.add_row(
            short_id,
            firm.name,
            device.name,
            device.location or "—",
            _status_icon(device.status),
            _heartbeat_label(device.last_heartbeat_at),
            device.key_algorithm or "—",
        )

    console.print()
    console.print(table)
    console.print(f"  [dim]{len(rows)} Gerät(e)[/dim]")
    console.print()


# ── Subcommand: kiosk approve ─────────────────────────────────────────────────

@kiosk_app.command("approve")
def kiosk_approve(
    device_id: str = typer.Argument(..., help="UUID des Kiosk-Geräts"),
):
    """Kiosk-Gerät freigeben (Status: pending → approved)."""

    async def _approve():
        session = await _open_session()
        try:
            device = await _find_device(session, device_id)
            if device.status == KioskDeviceStatus.APPROVED:
                return device, "already_approved"
            old_status = device.status
            device.status = KioskDeviceStatus.APPROVED
            await session.commit()
            return device, old_status
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

    device, old_status = _run(_approve())

    if old_status == "already_approved":
        console.print(f"[yellow]Info:[/yellow] Gerät '{device.name}' ist bereits [green]approved[/green].")
    else:
        console.print(
            f"[bold green]✓[/bold green] Gerät [bold]{device.name}[/bold] "
            f"({str(device.id)[:8]}…) wurde [green]freigegeben[/green] "
            f"(vorher: {old_status.value})."
        )


# ── Subcommand: kiosk revoke ──────────────────────────────────────────────────

@kiosk_app.command("revoke")
def kiosk_revoke(
    device_id: str = typer.Argument(..., help="UUID des Kiosk-Geräts"),
    yes: bool = typer.Option(False, "--yes", "-y", help="Ohne Bestätigung sperren"),
):
    """Kiosk-Gerät sperren (Status → revoked)."""

    async def _get():
        session = await _open_session()
        try:
            return await _find_device(session, device_id), session
        except Exception:
            await session.close()
            raise

    async def _revoke():
        session = await _open_session()
        try:
            device = await _find_device(session, device_id)
            if device.status == KioskDeviceStatus.REVOKED:
                return device, "already_revoked"
            old_status = device.status
            device.status = KioskDeviceStatus.REVOKED
            await session.commit()
            return device, old_status
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

    # Bestätigung einholen, wenn --yes nicht gesetzt
    if not yes:
        async def _peek():
            session = await _open_session()
            try:
                return await _find_device(session, device_id)
            finally:
                await session.close()
        device = _run(_peek())
        confirm = typer.confirm(
            f"Gerät '{device.name}' ({str(device.id)[:8]}…) wirklich sperren?"
        )
        if not confirm:
            console.print("[dim]Abgebrochen.[/dim]")
            raise typer.Exit(0)

    device, old_status = _run(_revoke())

    if old_status == "already_revoked":
        console.print(f"[yellow]Info:[/yellow] Gerät '{device.name}' ist bereits [red]revoked[/red].")
    else:
        console.print(
            f"[bold red]✓[/bold red] Gerät [bold]{device.name}[/bold] "
            f"({str(device.id)[:8]}…) wurde [red]gesperrt[/red] "
            f"(vorher: {old_status.value})."
        )


# ── Subcommand: kiosk rotate-key ─────────────────────────────────────────────

@kiosk_app.command("rotate-key")
def kiosk_rotate_key(
    device_id: str = typer.Argument(..., help="UUID des Kiosk-Geräts"),
    pubkey: Optional[Path] = typer.Option(None, "--pubkey", help="Pfad zur neuen Public-Key-Datei (optional)"),
    yes: bool = typer.Option(False, "--yes", "-y", help="Ohne Bestätigung fortfahren"),
):
    """Ed25519-Schlüssel eines Kiosk-Geräts rotieren (Status → pending, Re-Enrollment erforderlich)."""

    # Neuen Public Key einlesen falls angegeben
    new_pubkey_str: Optional[str] = None
    if pubkey is not None:
        if not pubkey.exists():
            err_console.print(f"[bold red]Fehler:[/bold red] Public-Key-Datei nicht gefunden: {pubkey}")
            raise typer.Exit(1)
        new_pubkey_str = pubkey.read_text().strip()
        if not new_pubkey_str:
            err_console.print("[bold red]Fehler:[/bold red] Public-Key-Datei ist leer.")
            raise typer.Exit(1)

    # Gerät vorab laden für Bestätigungsdialog
    if not yes:
        async def _peek():
            session = await _open_session()
            try:
                return await _find_device(session, device_id)
            finally:
                await session.close()
        device_peek = _run(_peek())
        console.print(
            f"[yellow]Achtung:[/yellow] Gerät [bold]{device_peek.name}[/bold] "
            f"({str(device_peek.id)[:8]}…) wird auf [yellow]pending[/yellow] gesetzt.\n"
            "Der aktuelle Schlüssel wird gelöscht. Re-Enrollment ist erforderlich."
        )
        confirm = typer.confirm("Schlüssel wirklich rotieren?")
        if not confirm:
            console.print("[dim]Abgebrochen.[/dim]")
            raise typer.Exit(0)

    async def _rotate():
        session = await _open_session()
        try:
            device = await _find_device(session, device_id)
            device.public_key = new_pubkey_str
            device.enrollment_token_hash = None
            device.enrollment_expires_at = None
            device.status = KioskDeviceStatus.PENDING
            await session.commit()
            await session.refresh(device)
            return device
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

    device = _run(_rotate())

    console.print()
    if new_pubkey_str:
        fingerprint = _pubkey_fingerprint(new_pubkey_str)
        console.print(
            f"[bold green]✓[/bold green] Schlüssel für Gerät [bold]{device.name}[/bold] "
            f"({str(device.id)[:8]}…) rotiert.\n"
            f"  [bold]Neuer Fingerprint:[/bold] {fingerprint}\n"
            f"  [bold]Status:[/bold] {_status_icon(device.status)}"
        )
    else:
        console.print(
            f"[bold green]✓[/bold green] Public Key für Gerät [bold]{device.name}[/bold] "
            f"({str(device.id)[:8]}…) wurde gelöscht.\n"
            f"  [bold]Status:[/bold] {_status_icon(device.status)}\n"
            "  [yellow]Hinweis:[/yellow] Bitte Public Key per Enrollment-Flow neu setzen."
        )
    console.print()


# ── Subcommand: kiosk info ────────────────────────────────────────────────────

@kiosk_app.command("info")
def kiosk_info(
    device_id: str = typer.Argument(..., help="UUID des Kiosk-Geräts"),
):
    """Detailinfo zu einem Kiosk-Gerät anzeigen."""

    async def _info():
        session = await _open_session()
        try:
            device = await _find_device(session, device_id)
            # Firma nachladen
            result = await session.execute(
                select(Company).where(Company.id == device.company_id)
            )
            firm = result.scalar_one_or_none()
            return device, firm
        finally:
            await session.close()

    device, firm = _run(_info())

    fingerprint = "—"
    pubkey_preview = "—"
    if device.public_key:
        fingerprint = _pubkey_fingerprint(device.public_key)
        # Ersten 60 Zeichen des Keys als Vorschau
        key_stripped = device.public_key.strip()
        pubkey_preview = (key_stripped[:60] + "…") if len(key_stripped) > 60 else key_stripped

    created_str = "—"
    if device.created_at:
        ts = device.created_at
        if ts.tzinfo is None:
            ts = ts.replace(tzinfo=timezone.utc)
        created_str = ts.strftime("%d.%m.%Y %H:%M:%S UTC")

    enrollment_str = "—"
    if device.enrollment_expires_at:
        ts = device.enrollment_expires_at
        if ts.tzinfo is None:
            ts = ts.replace(tzinfo=timezone.utc)
        enrollment_str = ts.strftime("%d.%m.%Y %H:%M:%S UTC")

    lines = [
        f"  [bold]ID:[/bold]                  {device.id}",
        f"  [bold]Firma:[/bold]               {firm.name if firm else str(device.company_id)}",
        f"  [bold]Name:[/bold]                {device.name}",
        f"  [bold]Standort:[/bold]            {device.location or '—'}",
        f"  [bold]Status:[/bold]              {_status_icon(device.status)}",
        f"  [bold]Key-Algorithmus:[/bold]     {device.key_algorithm or '—'}",
        f"  [bold]Public Key:[/bold]          {pubkey_preview}",
        f"  [bold]Key-Fingerprint:[/bold]     {fingerprint}",
        f"  [bold]IP-Whitelist:[/bold]        {device.ip_whitelist or '—'}",
        f"  [bold]Heartbeat:[/bold]           {_heartbeat_label(device.last_heartbeat_at)}",
        f"  [bold]Client-Version:[/bold]      {device.client_version or '—'}",
        f"  [bold]Offline-Queue:[/bold]       {device.offline_queue_size}",
        f"  [bold]Aktueller User:[/bold]      {device.current_user_id or '—'}",
        f"  [bold]Enrollment läuft ab:[/bold] {enrollment_str}",
        f"  [bold]Angelegt am:[/bold]         {created_str}",
    ]

    console.print()
    console.print(Panel(
        "\n".join(lines),
        title=f"Kiosk-Gerät: {device.name}",
        border_style="cyan",
    ))
    console.print()


# ── Entry Point ───────────────────────────────────────────────────────────────

if __name__ == "__main__":
    app()
