from __future__ import annotations

import secrets
import sqlite3
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path


def utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def generate_token(prefix: str) -> str:
    return f"{prefix}_{secrets.token_urlsafe(32)}"


@dataclass(frozen=True)
class RelayAccount:
    user_id: str
    email: str | None
    display_name: str | None
    plan: str
    status: str
    expires_at: str | None


@dataclass(frozen=True)
class RelayDevice:
    device_id: str
    user_id: str
    name: str
    mac_service_url: str
    mac_service_token: str
    tunnel_port: int
    status: str
    account: RelayAccount


class RelayRegistry:
    """Small SQLite registry that maps user API tokens to Mac relay devices."""

    def __init__(self, path: Path) -> None:
        self.path = path
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self._init_db()

    def _connect(self) -> sqlite3.Connection:
        conn = sqlite3.connect(self.path)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA foreign_keys = ON")
        return conn

    def _init_db(self) -> None:
        with self._connect() as conn:
            conn.executescript(
                """
                CREATE TABLE IF NOT EXISTS users (
                  user_id TEXT PRIMARY KEY,
                  email TEXT UNIQUE,
                  display_name TEXT,
                  plan TEXT NOT NULL DEFAULT 'free',
                  status TEXT NOT NULL DEFAULT 'active',
                  expires_at TEXT,
                  created_at TEXT NOT NULL,
                  updated_at TEXT NOT NULL
                );

                CREATE TABLE IF NOT EXISTS devices (
                  device_id TEXT PRIMARY KEY,
                  user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
                  name TEXT NOT NULL,
                  mac_service_url TEXT NOT NULL,
                  mac_service_token TEXT NOT NULL,
                  tunnel_port INTEGER NOT NULL,
                  status TEXT NOT NULL DEFAULT 'active',
                  last_seen_at TEXT,
                  created_at TEXT NOT NULL,
                  updated_at TEXT NOT NULL
                );

                CREATE TABLE IF NOT EXISTS api_tokens (
                  token_hash TEXT PRIMARY KEY,
                  user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
                  device_id TEXT REFERENCES devices(device_id) ON DELETE CASCADE,
                  label TEXT NOT NULL,
                  status TEXT NOT NULL DEFAULT 'active',
                  created_at TEXT NOT NULL,
                  last_used_at TEXT
                );

                CREATE INDEX IF NOT EXISTS idx_api_tokens_user ON api_tokens(user_id);
                CREATE INDEX IF NOT EXISTS idx_devices_user ON devices(user_id);
                """
            )

    def ensure_owner_device(
        self,
        *,
        token: str,
        mac_service_url: str,
        mac_service_token: str,
        email: str | None = None,
        display_name: str = "Owner",
        device_name: str = "Owner Mac",
        tunnel_port: int = 18765,
    ) -> None:
        now = utc_now_iso()
        with self._connect() as conn:
            conn.execute(
                """
                INSERT INTO users (user_id, email, display_name, plan, status, expires_at, created_at, updated_at)
                VALUES ('owner', ?, ?, 'admin', 'active', NULL, ?, ?)
                ON CONFLICT(user_id) DO UPDATE SET
                  email=COALESCE(excluded.email, users.email),
                  display_name=excluded.display_name,
                  plan='admin',
                  status='active',
                  updated_at=excluded.updated_at
                """,
                (email, display_name, now, now),
            )
            conn.execute(
                """
                INSERT INTO devices (
                  device_id, user_id, name, mac_service_url, mac_service_token,
                  tunnel_port, status, last_seen_at, created_at, updated_at
                )
                VALUES ('owner-mac', 'owner', ?, ?, ?, ?, 'active', NULL, ?, ?)
                ON CONFLICT(device_id) DO UPDATE SET
                  name=excluded.name,
                  mac_service_url=excluded.mac_service_url,
                  mac_service_token=excluded.mac_service_token,
                  tunnel_port=excluded.tunnel_port,
                  status='active',
                  updated_at=excluded.updated_at
                """,
                (device_name, mac_service_url, mac_service_token, tunnel_port, now, now),
            )
            conn.execute(
                """
                INSERT INTO api_tokens (token_hash, user_id, device_id, label, status, created_at, last_used_at)
                VALUES (?, 'owner', 'owner-mac', 'owner app token', 'active', ?, NULL)
                ON CONFLICT(token_hash) DO UPDATE SET
                  user_id='owner',
                  device_id='owner-mac',
                  status='active'
                """,
                (self.hash_token(token), now),
            )

    def create_user_with_device(
        self,
        *,
        email: str | None,
        display_name: str | None,
        device_name: str,
        mac_service_url: str,
        mac_service_token: str,
        tunnel_port: int,
        plan: str = "pro",
        expires_at: str | None = None,
    ) -> tuple[str, RelayDevice]:
        now = utc_now_iso()
        user_id = f"user_{secrets.token_hex(8)}"
        device_id = f"device_{secrets.token_hex(8)}"
        token = generate_token("cmu")
        with self._connect() as conn:
            conn.execute(
                """
                INSERT INTO users (user_id, email, display_name, plan, status, expires_at, created_at, updated_at)
                VALUES (?, ?, ?, ?, 'active', ?, ?, ?)
                """,
                (user_id, email, display_name, plan, expires_at, now, now),
            )
            conn.execute(
                """
                INSERT INTO devices (
                  device_id, user_id, name, mac_service_url, mac_service_token,
                  tunnel_port, status, last_seen_at, created_at, updated_at
                )
                VALUES (?, ?, ?, ?, ?, ?, 'active', NULL, ?, ?)
                """,
                (device_id, user_id, device_name, mac_service_url, mac_service_token, tunnel_port, now, now),
            )
            conn.execute(
                """
                INSERT INTO api_tokens (token_hash, user_id, device_id, label, status, created_at, last_used_at)
                VALUES (?, ?, ?, 'mobile app token', 'active', ?, NULL)
                """,
                (self.hash_token(token), user_id, device_id, now),
            )
        device = self.authenticate(token)
        if device is None:
            raise RuntimeError("Created relay token could not be loaded")
        return token, device

    def list_devices(self) -> list[RelayDevice]:
        with self._connect() as conn:
            rows = conn.execute(
                """
                SELECT
                  d.device_id, d.user_id, d.name, d.mac_service_url, d.mac_service_token,
                  d.tunnel_port, d.status AS device_status,
                  u.email, u.display_name, u.plan, u.status AS user_status, u.expires_at
                FROM devices d
                JOIN users u ON u.user_id = d.user_id
                ORDER BY u.created_at ASC, d.created_at ASC
                """
            ).fetchall()
        return [self._device_from_row(row) for row in rows]

    def authenticate(self, token: str) -> RelayDevice | None:
        token_hash = self.hash_token(token)
        now = utc_now_iso()
        with self._connect() as conn:
            row = conn.execute(
                """
                SELECT
                  d.device_id, d.user_id, d.name, d.mac_service_url, d.mac_service_token,
                  d.tunnel_port, d.status AS device_status,
                  u.email, u.display_name, u.plan, u.status AS user_status, u.expires_at
                FROM api_tokens t
                JOIN users u ON u.user_id = t.user_id
                JOIN devices d ON d.device_id = COALESCE(t.device_id, (
                  SELECT device_id FROM devices WHERE user_id = u.user_id AND status = 'active' ORDER BY updated_at DESC LIMIT 1
                ))
                WHERE t.token_hash = ? AND t.status = 'active'
                LIMIT 1
                """,
                (token_hash,),
            ).fetchone()
            if row is None:
                return None
            if row["user_status"] != "active" or row["device_status"] != "active":
                return None
            if row["expires_at"]:
                expires = datetime.fromisoformat(row["expires_at"].replace("Z", "+00:00"))
                if expires < datetime.now(timezone.utc):
                    return None
            conn.execute("UPDATE api_tokens SET last_used_at = ? WHERE token_hash = ?", (now, token_hash))
            conn.execute("UPDATE devices SET last_seen_at = ?, updated_at = ? WHERE device_id = ?", (now, now, row["device_id"]))
            account = RelayAccount(
                user_id=row["user_id"],
                email=row["email"],
                display_name=row["display_name"],
                plan=row["plan"],
                status=row["user_status"],
                expires_at=row["expires_at"],
            )
            return self._device_from_row(row, account=account)

    def describe_token(self, token: str) -> dict | None:
        device = self.authenticate(token)
        if device is None:
            return None
        return {
            "user": {
                "user_id": device.account.user_id,
                "email": device.account.email,
                "display_name": device.account.display_name,
                "plan": device.account.plan,
                "status": device.account.status,
                "expires_at": device.account.expires_at,
            },
            "device": {
                "device_id": device.device_id,
                "name": device.name,
                "mac_service_url": device.mac_service_url,
                "tunnel_port": device.tunnel_port,
                "status": device.status,
            },
        }

    @staticmethod
    def hash_token(token: str) -> str:
        import hashlib

        return hashlib.sha256(token.encode("utf-8")).hexdigest()

    def _device_from_row(self, row: sqlite3.Row, account: RelayAccount | None = None) -> RelayDevice:
        if account is None:
            account = RelayAccount(
                user_id=row["user_id"],
                email=row["email"],
                display_name=row["display_name"],
                plan=row["plan"],
                status=row["user_status"],
                expires_at=row["expires_at"],
            )
        return RelayDevice(
            device_id=row["device_id"],
            user_id=row["user_id"],
            name=row["name"],
            mac_service_url=row["mac_service_url"],
            mac_service_token=row["mac_service_token"],
            tunnel_port=int(row["tunnel_port"]),
            status=row["device_status"],
            account=account,
        )
