from __future__ import annotations

import base64
import hashlib
import hmac
import os
import secrets
from datetime import timedelta
from typing import Any

from datahub.crypto import SecretVault, mask_secret
from datahub.db import HubDB
from datahub.timeutil import isoformat, now_shanghai

PBKDF2_ROUNDS = 200_000
SESSION_HOURS = 12
LOGIN_FAIL_LIMIT = 5
LOCK_MINUTES = 10


def hash_password(password: str, salt: bytes | None = None) -> tuple[str, str]:
    raw_salt = salt or os.urandom(16)
    digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), raw_salt, PBKDF2_ROUNDS, dklen=32)
    return (
        base64.urlsafe_b64encode(raw_salt).decode("ascii"),
        base64.urlsafe_b64encode(digest).decode("ascii"),
    )


def verify_password(password: str, salt_text: str, expected_hash: str) -> bool:
    try:
        salt = base64.urlsafe_b64decode(salt_text.encode("ascii"))
        _, actual = hash_password(password, salt)
    except (ValueError, TypeError):
        return False
    return hmac.compare_digest(actual, expected_hash)


def token_hash(token: str) -> str:
    return hashlib.sha256(token.encode("utf-8")).hexdigest()


class AuthService:
    def __init__(self, db: HubDB, vault: SecretVault, api_token: str, admin_password: str) -> None:
        self.db = db
        self.vault = vault
        self._bootstrap(api_token, admin_password)

    def _bootstrap(self, api_token: str, admin_password: str) -> None:
        if api_token:
            existing = self.db.fetchone("SELECT token_hash FROM api_tokens WHERE name = ?", ("review",))
            hashed = token_hash(api_token)
            last4 = mask_secret(api_token)
            if existing is None:
                self.db.execute(
                    "INSERT INTO api_tokens(token_hash, name, last4, created_at) VALUES (?,?,?,?)",
                    (hashed, "review", last4, isoformat()),
                )
            elif existing["token_hash"] != hashed:
                self.db.execute(
                    "UPDATE api_tokens SET token_hash = ?, last4 = ? WHERE name = ?",
                    (hashed, last4, "review"),
                )
        admin = self.db.fetchone("SELECT id FROM hub_admin WHERE username = ?", ("hub_admin",))
        if admin is None and admin_password:
            salt, hashed = hash_password(admin_password)
            now = isoformat()
            self.db.execute(
                """
                INSERT INTO hub_admin(username, password_salt, password_hash, password_must_change, created_at, updated_at)
                VALUES (?, ?, ?, 1, ?, ?)
                """,
                ("hub_admin", salt, hashed, now, now),
            )

    def check_api_token(self, supplied: str) -> bool:
        if not supplied:
            return False
        row = self.db.fetchone(
            "SELECT token_hash FROM api_tokens WHERE token_hash = ? AND revoked_at IS NULL",
            (token_hash(supplied),),
        )
        return row is not None

    def login(self, username: str, password: str) -> dict[str, Any]:
        user = self.db.fetchone("SELECT * FROM hub_admin WHERE username = ?", (username,))
        if not user:
            raise PermissionError("账号或密码错误")
        now = now_shanghai()
        locked_until = user.get("locked_until")
        if locked_until:
            try:
                from datetime import datetime

                if datetime.fromisoformat(str(locked_until)) > now:
                    raise PermissionError("账号已锁定，请稍后再试")
            except ValueError:
                pass
        if not verify_password(password, str(user["password_salt"]), str(user["password_hash"])):
            fails = int(user["failed_attempts"] or 0) + 1
            lock = isoformat(now + timedelta(minutes=LOCK_MINUTES)) if fails >= LOGIN_FAIL_LIMIT else None
            self.db.execute(
                "UPDATE hub_admin SET failed_attempts = ?, locked_until = ? WHERE id = ?",
                (fails, lock, user["id"]),
            )
            raise PermissionError("账号或密码错误")
        self.db.execute(
            "UPDATE hub_admin SET failed_attempts = 0, locked_until = NULL WHERE id = ?",
            (user["id"],),
        )
        session = secrets.token_urlsafe(32)
        csrf = secrets.token_urlsafe(24)
        expires = isoformat(now + timedelta(hours=SESSION_HOURS))
        self.db.execute(
            "INSERT INTO hub_sessions(token_hash, csrf_token, expires_at, created_at) VALUES (?,?,?,?)",
            (token_hash(session), csrf, expires, isoformat(now)),
        )
        return {
            "session": session,
            "csrf": csrf,
            "must_change": bool(user["password_must_change"]),
            "expires_at": expires,
        }

    def session_user(self, raw_token: str) -> dict[str, Any] | None:
        if not raw_token:
            return None
        row = self.db.fetchone(
            "SELECT * FROM hub_sessions WHERE token_hash = ?",
            (token_hash(raw_token),),
        )
        if not row:
            return None
        if str(row["expires_at"]) < isoformat():
            self.db.execute("DELETE FROM hub_sessions WHERE token_hash = ?", (row["token_hash"],))
            return None
        admin = self.db.fetchone("SELECT username, password_must_change FROM hub_admin WHERE username = ?", ("hub_admin",))
        return {
            "username": (admin or {}).get("username") or "hub_admin",
            "csrf_token": row["csrf_token"],
            "must_change": bool((admin or {}).get("password_must_change")),
            "token_hash": row["token_hash"],
        }

    def logout(self, raw_token: str) -> None:
        if raw_token:
            self.db.execute("DELETE FROM hub_sessions WHERE token_hash = ?", (token_hash(raw_token),))

    def change_password(self, current: str, new_password: str) -> None:
        if len(new_password) < 8:
            raise ValueError("新密码至少 8 位")
        user = self.db.fetchone("SELECT * FROM hub_admin WHERE username = ?", ("hub_admin",))
        if not user or not verify_password(current, str(user["password_salt"]), str(user["password_hash"])):
            raise PermissionError("当前密码错误")
        salt, hashed = hash_password(new_password)
        self.db.execute(
            "UPDATE hub_admin SET password_salt=?, password_hash=?, password_must_change=0, updated_at=? WHERE id=?",
            (salt, hashed, isoformat(), user["id"]),
        )

    def confirm_password(self, password: str) -> bool:
        user = self.db.fetchone("SELECT * FROM hub_admin WHERE username = ?", ("hub_admin",))
        if not user:
            return False
        return verify_password(password, str(user["password_salt"]), str(user["password_hash"]))

    def credential_status(self, name: str) -> dict[str, Any]:
        row = self.db.fetchone("SELECT last4, updated_at FROM credentials WHERE name = ?", (name,))
        if not row:
            return {"configured": False, "last4": "", "updated_at": ""}
        return {"configured": True, "last4": row["last4"], "updated_at": row["updated_at"]}

    def store_credential(self, name: str, secret: str) -> None:
        payload = self.vault.encrypt_json({name: secret})
        self.db.execute(
            """
            INSERT INTO credentials(name, encrypted_payload, last4, updated_at)
            VALUES (?, ?, ?, ?)
            ON CONFLICT(name) DO UPDATE SET
              encrypted_payload=excluded.encrypted_payload, last4=excluded.last4, updated_at=excluded.updated_at
            """,
            (name, payload, mask_secret(secret), isoformat()),
        )

    def load_credential(self, name: str) -> str:
        row = self.db.fetchone("SELECT encrypted_payload FROM credentials WHERE name = ?", (name,))
        if not row:
            return ""
        data = self.vault.decrypt_json(str(row["encrypted_payload"]))
        return str(data.get(name) or "")
