from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import re
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional

import jwt
import stripe
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field

APP_NAME = "ATHENODE Auth Server"
DB_PATH = Path((os.getenv("EKSAMENAPP_SERVER_DB") or "./server/data/app.db").strip()).expanduser().resolve()
JWT_SECRET = (os.getenv("EKSAMENAPP_SERVER_JWT_SECRET") or "dev-change-this-secret").strip()
JWT_TTL_HOURS = int((os.getenv("EKSAMENAPP_SERVER_JWT_TTL_HOURS") or "12").strip() or "12")
ADMIN_PASSWORD = (os.getenv("EKSAMENAPP_ADMIN_PASSWORD") or "admin1234").strip() or "admin1234"
ALLOWED_ORIGINS = [o.strip() for o in (os.getenv("EKSAMENAPP_SERVER_CORS") or "*").split(",") if o.strip()]
APP_LATEST_VERSION = (os.getenv("EKSAMENAPP_LATEST_VERSION") or "").strip()
APP_MIN_SUPPORTED_VERSION = (os.getenv("EKSAMENAPP_MIN_SUPPORTED_VERSION") or "").strip()
APP_DOWNLOAD_URL = (os.getenv("EKSAMENAPP_DOWNLOAD_URL") or "").strip()
APP_RELEASE_NOTES = (os.getenv("EKSAMENAPP_RELEASE_NOTES") or "").strip()
APP_RELEASED_AT = (os.getenv("EKSAMENAPP_RELEASED_AT") or "").strip()

STRIPE_SECRET_KEY = (os.getenv("STRIPE_SECRET_KEY") or "").strip()
STRIPE_WEBHOOK_SECRET = (os.getenv("STRIPE_WEBHOOK_SECRET") or "").strip()
STRIPE_PAYMENT_METHOD_TYPES = [
    p.strip() for p in (os.getenv("STRIPE_PAYMENT_METHOD_TYPES") or "card").split(",") if p.strip()
]

if STRIPE_SECRET_KEY:
    stripe.api_key = STRIPE_SECRET_KEY


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


def _db_conn() -> sqlite3.Connection:
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
    conn.row_factory = sqlite3.Row
    return conn


def _init_db() -> None:
    with _db_conn() as conn:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS users (
                username TEXT PRIMARY KEY,
                password_salt TEXT NOT NULL,
                password_hash TEXT NOT NULL,
                email TEXT NOT NULL DEFAULT '',
                role TEXT NOT NULL DEFAULT 'user',
                subscription_active INTEGER NOT NULL DEFAULT 0,
                stripe_customer_id TEXT,
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
            """
        )
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS payments (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT,
                provider TEXT NOT NULL,
                external_id TEXT,
                amount_dkk INTEGER,
                currency TEXT,
                status TEXT,
                payload_json TEXT,
                created_at TEXT NOT NULL
            )
            """
        )
        user_columns = {str(r["name"]) for r in conn.execute("PRAGMA table_info(users)").fetchall()}
        if "email" not in user_columns:
            conn.execute("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''")
        conn.commit()


def _b64_encode(data: bytes) -> str:
    return base64.b64encode(data).decode("ascii")


def _b64_decode(data: str) -> bytes:
    return base64.b64decode((data or "").encode("ascii"))


def _hash_password(password: str, salt: bytes) -> bytes:
    return hashlib.pbkdf2_hmac("sha256", (password or "").encode("utf-8"), salt, 200_000)


def make_password_hash(password: str) -> tuple[str, str]:
    salt = os.urandom(16)
    digest = _hash_password(password, salt)
    return _b64_encode(salt), _b64_encode(digest)


def verify_password(password: str, salt_b64: str, hash_b64: str) -> bool:
    try:
        salt = _b64_decode(salt_b64)
        expected = _b64_decode(hash_b64)
    except Exception:
        return False
    got = _hash_password(password, salt)
    return hmac.compare_digest(got, expected)


def norm_username(username: str) -> str:
    return "".join((username or "").strip().lower().split())


def valid_username(username: str) -> bool:
    return bool(re.fullmatch(r"[a-z0-9._-]{3,40}", norm_username(username)))


def norm_email(email: str) -> str:
    return (email or "").strip().lower()


def valid_email(email: str) -> bool:
    return bool(re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", norm_email(email)))


def get_user(username: str) -> Optional[sqlite3.Row]:
    uname = norm_username(username)
    if not uname:
        return None
    with _db_conn() as conn:
        cur = conn.execute("SELECT * FROM users WHERE username = ?", (uname,))
        return cur.fetchone()


def upsert_admin() -> None:
    uname = "admin"
    with _db_conn() as conn:
        cur = conn.execute("SELECT username FROM users WHERE username = ?", (uname,))
        row = cur.fetchone()
        if row:
            return
        salt_b64, hash_b64 = make_password_hash(ADMIN_PASSWORD)
        now = now_iso()
        conn.execute(
            """
            INSERT INTO users(username, password_salt, password_hash, role, subscription_active, created_at, updated_at)
            VALUES (?, ?, ?, 'admin', 1, ?, ?)
            """,
            (uname, salt_b64, hash_b64, now, now),
        )
        conn.commit()


def create_access_token(user: sqlite3.Row) -> str:
    now = datetime.now(timezone.utc)
    exp = now + timedelta(hours=max(1, JWT_TTL_HOURS))
    payload = {
        "sub": str(user["username"]),
        "role": str(user["role"]),
        "paid": bool(int(user["subscription_active"])),
        "iat": int(now.timestamp()),
        "exp": int(exp.timestamp()),
    }
    return jwt.encode(payload, JWT_SECRET, algorithm="HS256")


def decode_token(token: str) -> dict[str, Any]:
    try:
        return jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token er udløbet")
    except Exception:
        raise HTTPException(status_code=401, detail="Ugyldig token")


class LoginIn(BaseModel):
    username: str
    password: str


class RegisterIn(BaseModel):
    username: str
    password: str
    email: str


class UserOut(BaseModel):
    username: str
    email: str = ""
    role: str
    subscription_active: bool
    created_at: str = ""


class LoginOut(BaseModel):
    access_token: str
    token_type: str = "bearer"
    expires_in: int
    user: UserOut


class CreateUserIn(BaseModel):
    username: str
    password: str
    email: str = ""
    role: str = "user"
    subscription_active: bool = False


class UpdateUserIn(BaseModel):
    role: Optional[str] = None
    subscription_active: Optional[bool] = None


class ResetPasswordIn(BaseModel):
    new_password: str


class CheckoutIn(BaseModel):
    price_dkk: int = Field(default=39, ge=1, le=5000)
    success_url: str
    cancel_url: str


class AppUpdateOut(BaseModel):
    latest_version: str = ""
    minimum_supported_version: str = ""
    download_url: str = ""
    release_notes: str = ""
    released_at: str = ""


app = FastAPI(title=APP_NAME)
app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS if ALLOWED_ORIGINS else ["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.on_event("startup")
def on_startup() -> None:
    _init_db()
    upsert_admin()


def get_current_user_row(authorization: str = Header(default="")) -> sqlite3.Row:
    auth = (authorization or "").strip()
    if not auth.lower().startswith("bearer "):
        raise HTTPException(status_code=401, detail="Mangler bearer token")
    token = auth.split(" ", 1)[1].strip()
    payload = decode_token(token)
    username = str(payload.get("sub") or "").strip()
    row = get_user(username)
    if not row:
        raise HTTPException(status_code=401, detail="Bruger findes ikke")
    return row


def require_admin(user: sqlite3.Row = Depends(get_current_user_row)) -> sqlite3.Row:
    role = str(user["role"] or "user").strip().lower()
    if role != "admin":
        raise HTTPException(status_code=403, detail="Kræver admin-rettighed")
    return user


def row_to_user_out(row: sqlite3.Row) -> UserOut:
    return UserOut(
        username=str(row["username"]),
        email=str(row["email"] or ""),
        role=str(row["role"]),
        subscription_active=bool(int(row["subscription_active"])),
        created_at=str(row["created_at"] or ""),
    )


@app.get("/health")
def health() -> dict[str, Any]:
    return {
        "ok": True,
        "name": APP_NAME,
        "stripe_configured": bool(STRIPE_SECRET_KEY),
        "payment_methods": STRIPE_PAYMENT_METHOD_TYPES,
        "latest_version": APP_LATEST_VERSION,
        "minimum_supported_version": APP_MIN_SUPPORTED_VERSION,
        "update_download_url_set": bool(APP_DOWNLOAD_URL),
    }


@app.get("/v1/app/update", response_model=AppUpdateOut)
def app_update() -> AppUpdateOut:
    return AppUpdateOut(
        latest_version=APP_LATEST_VERSION,
        minimum_supported_version=APP_MIN_SUPPORTED_VERSION,
        download_url=APP_DOWNLOAD_URL,
        release_notes=APP_RELEASE_NOTES,
        released_at=APP_RELEASED_AT,
    )


@app.post("/v1/auth/login", response_model=LoginOut)
def auth_login(body: LoginIn) -> LoginOut:
    user = get_user(body.username)
    if not user:
        raise HTTPException(status_code=401, detail="Bruger findes ikke")
    if not verify_password(body.password, str(user["password_salt"]), str(user["password_hash"])):
        raise HTTPException(status_code=401, detail="Forkert adgangskode")

    token = create_access_token(user)
    return LoginOut(
        access_token=token,
        expires_in=max(1, JWT_TTL_HOURS) * 3600,
        user=row_to_user_out(user),
    )


@app.post("/v1/auth/register", response_model=LoginOut)
def auth_register(body: RegisterIn) -> LoginOut:
    username = norm_username(body.username)
    email = norm_email(body.email)
    if not valid_username(username):
        raise HTTPException(
            status_code=400,
            detail="Ugyldigt brugernavn. Brug 3-40 tegn: a-z, 0-9, punktum, underscore eller bindestreg.",
        )
    if not valid_email(email):
        raise HTTPException(status_code=400, detail="Ugyldig email-adresse")
    if len(body.password or "") < 8:
        raise HTTPException(status_code=400, detail="Adgangskode skal være mindst 8 tegn")
    if get_user(username):
        raise HTTPException(status_code=409, detail="Brugernavn findes allerede")

    salt_b64, hash_b64 = make_password_hash(body.password)
    now = now_iso()
    with _db_conn() as conn:
        conn.execute(
            """
            INSERT INTO users(username, password_salt, password_hash, email, role, subscription_active, created_at, updated_at)
            VALUES (?, ?, ?, ?, 'user', 0, ?, ?)
            """,
            (username, salt_b64, hash_b64, email, now, now),
        )
        conn.commit()

    user = get_user(username)
    if not user:
        raise HTTPException(status_code=500, detail="Kunne ikke oprette konto")
    token = create_access_token(user)
    return LoginOut(
        access_token=token,
        expires_in=max(1, JWT_TTL_HOURS) * 3600,
        user=row_to_user_out(user),
    )


@app.get("/v1/auth/me", response_model=UserOut)
def auth_me(user: sqlite3.Row = Depends(get_current_user_row)) -> UserOut:
    return row_to_user_out(user)


@app.get("/v1/admin/users")
def admin_list_users(_: sqlite3.Row = Depends(require_admin)) -> dict[str, Any]:
    with _db_conn() as conn:
        rows = conn.execute(
            "SELECT username, email, role, subscription_active, created_at FROM users ORDER BY username ASC"
        ).fetchall()
    users = [
        {
            "username": str(r["username"]),
            "email": str(r["email"] or ""),
            "role": str(r["role"]),
            "subscription_active": bool(int(r["subscription_active"])),
            "created_at": str(r["created_at"] or ""),
        }
        for r in rows
    ]
    return {"users": users}


@app.post("/v1/admin/users")
def admin_create_user(body: CreateUserIn, _: sqlite3.Row = Depends(require_admin)) -> dict[str, Any]:
    username = norm_username(body.username)
    email = norm_email(body.email)
    if not valid_username(username):
        raise HTTPException(
            status_code=400,
            detail="Ugyldigt brugernavn. Brug 3-40 tegn: a-z, 0-9, punktum, underscore eller bindestreg.",
        )
    if email and not valid_email(email):
        raise HTTPException(status_code=400, detail="Ugyldig email-adresse")
    if len(body.password or "") < 8:
        raise HTTPException(status_code=400, detail="Adgangskode skal være mindst 8 tegn")
    if get_user(username):
        raise HTTPException(status_code=409, detail="Brugernavn findes allerede")

    role = "admin" if str(body.role or "user").strip().lower() == "admin" else "user"
    salt_b64, hash_b64 = make_password_hash(body.password)
    now = now_iso()
    with _db_conn() as conn:
        conn.execute(
            """
            INSERT INTO users(username, password_salt, password_hash, email, role, subscription_active, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (username, salt_b64, hash_b64, email, role, 1 if body.subscription_active else 0, now, now),
        )
        conn.commit()
    return {"ok": True}


@app.patch("/v1/admin/users/{username}")
def admin_update_user(username: str, body: UpdateUserIn, _: sqlite3.Row = Depends(require_admin)) -> dict[str, Any]:
    uname = norm_username(username)
    row = get_user(uname)
    if not row:
        raise HTTPException(status_code=404, detail="Bruger findes ikke")

    role = str(row["role"])
    paid = bool(int(row["subscription_active"]))
    if body.role is not None:
        role = "admin" if str(body.role).strip().lower() == "admin" else "user"
    if body.subscription_active is not None:
        paid = bool(body.subscription_active)

    with _db_conn() as conn:
        conn.execute(
            "UPDATE users SET role = ?, subscription_active = ?, updated_at = ? WHERE username = ?",
            (role, 1 if paid else 0, now_iso(), uname),
        )
        conn.commit()
    return {"ok": True}


@app.post("/v1/admin/users/{username}/reset-password")
def admin_reset_password(username: str, body: ResetPasswordIn, _: sqlite3.Row = Depends(require_admin)) -> dict[str, Any]:
    uname = norm_username(username)
    row = get_user(uname)
    if not row:
        raise HTTPException(status_code=404, detail="Bruger findes ikke")
    if len(body.new_password or "") < 8:
        raise HTTPException(status_code=400, detail="Ny adgangskode skal være mindst 8 tegn")

    salt_b64, hash_b64 = make_password_hash(body.new_password)
    with _db_conn() as conn:
        conn.execute(
            "UPDATE users SET password_salt = ?, password_hash = ?, updated_at = ? WHERE username = ?",
            (salt_b64, hash_b64, now_iso(), uname),
        )
        conn.commit()
    return {"ok": True}


@app.post("/v1/billing/checkout-session")
def billing_checkout_session(body: CheckoutIn, user: sqlite3.Row = Depends(get_current_user_row)) -> dict[str, Any]:
    if not STRIPE_SECRET_KEY:
        raise HTTPException(
            status_code=501,
            detail=(
                "Stripe er ikke konfigureret på serveren. Sæt STRIPE_SECRET_KEY. "
                "For MobilePay kræves derudover gateway-understøttelse og merchant-opsætning."
            ),
        )

    username = str(user["username"])
    metadata = {"username": username}

    params: dict[str, Any] = {
        "mode": "payment",
        "success_url": body.success_url,
        "cancel_url": body.cancel_url,
        "line_items": [
            {
                "quantity": 1,
                "price_data": {
                    "currency": "dkk",
                    "unit_amount": int(body.price_dkk) * 100,
                    "product_data": {"name": "ATHENODE adgang"},
                },
            }
        ],
        "metadata": metadata,
        "payment_method_types": STRIPE_PAYMENT_METHOD_TYPES or ["card"],
    }

    try:
        session = stripe.checkout.Session.create(**params)
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Stripe checkout fejlede: {e}")

    with _db_conn() as conn:
        conn.execute(
            """
            INSERT INTO payments(username, provider, external_id, amount_dkk, currency, status, payload_json, created_at)
            VALUES (?, 'stripe', ?, ?, 'dkk', 'created', ?, ?)
            """,
            (
                username,
                str(session.get("id") or ""),
                int(body.price_dkk),
                json.dumps(session, ensure_ascii=False),
                now_iso(),
            ),
        )
        conn.commit()

    return {
        "checkout_url": str(session.get("url") or ""),
        "session_id": str(session.get("id") or ""),
        "provider": "stripe",
        "payment_method_types": STRIPE_PAYMENT_METHOD_TYPES,
    }


@app.post("/v1/webhooks/stripe")
async def stripe_webhook(request: Request, stripe_signature: str = Header(default="")) -> dict[str, Any]:
    if not STRIPE_SECRET_KEY:
        raise HTTPException(status_code=501, detail="Stripe er ikke konfigureret")

    payload = await request.body()

    try:
        if STRIPE_WEBHOOK_SECRET:
            event = stripe.Webhook.construct_event(payload=payload, sig_header=stripe_signature, secret=STRIPE_WEBHOOK_SECRET)
        else:
            event = json.loads(payload.decode("utf-8"))
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Webhook kunne ikke valideres: {e}")

    event_type = str(event.get("type") or "")
    obj = ((event.get("data") or {}).get("object") or {}) if isinstance(event, dict) else {}

    if event_type == "checkout.session.completed" and isinstance(obj, dict):
        metadata = obj.get("metadata") if isinstance(obj.get("metadata"), dict) else {}
        username = norm_username(str((metadata or {}).get("username") or ""))
        if username:
            with _db_conn() as conn:
                conn.execute(
                    "UPDATE users SET subscription_active = 1, updated_at = ? WHERE username = ?",
                    (now_iso(), username),
                )
                conn.execute(
                    """
                    INSERT INTO payments(username, provider, external_id, amount_dkk, currency, status, payload_json, created_at)
                    VALUES (?, 'stripe', ?, ?, ?, 'completed', ?, ?)
                    """,
                    (
                        username,
                        str(obj.get("id") or ""),
                        int(round((obj.get("amount_total") or 0) / 100)),
                        str(obj.get("currency") or ""),
                        json.dumps(obj, ensure_ascii=False),
                        now_iso(),
                    ),
                )
                conn.commit()

    return {"ok": True}
