# -*- coding: utf-8 -*-
"""Local auth service for single-user login."""
from __future__ import annotations

import hashlib
import hmac
import json
import re
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from ..config import load_config, save_config
from ..config.utils import get_auth_sessions_path, get_auth_users_path
from ..constant import AUTH_COOKIE_SECURE, AUTH_SESSION_COOKIE_NAME

PASSWORD_HASH_ALGO = "pbkdf2_sha256"
PASSWORD_HASH_ITERS = 260_000
PASSWORD_MIN_LENGTH = 8
DISPLAY_NAME_MAX_LENGTH = 64
AVATAR_DATA_URL_MAX_LENGTH = 2_000_000
USERNAME_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]{2,31}$")
EMAIL_PATTERN = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
PHONE_PATTERN = re.compile(r"^\+?[0-9]{6,20}$")
SESSION_COOKIE_NAME = AUTH_SESSION_COOKIE_NAME
SESSION_COOKIE_SECURE = AUTH_COOKIE_SECURE


@dataclass
class AuthUser:
    """Authenticated user."""

    username: str
    display_name: str
    avatar_data_url: str | None = None


class AuthError(ValueError):
    """Domain error for auth operations with stable error codes."""

    def __init__(self, code: str, message: str):
        super().__init__(message)
        self.code = code


def _now_utc() -> datetime:
    return datetime.now(timezone.utc)


def _to_iso(dt: datetime) -> str:
    return dt.astimezone(timezone.utc).isoformat()


def _from_iso(value: str) -> datetime | None:
    try:
        dt = datetime.fromisoformat(value)
    except (TypeError, ValueError):
        return None
    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


def _read_json(path: Path, default: Any) -> Any:
    if not path.is_file():
        return default
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return default


def _write_json_atomic(path: Path, data: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp_path = path.with_suffix(path.suffix + ".tmp")
    tmp_path.write_text(
        json.dumps(data, indent=2, ensure_ascii=False),
        encoding="utf-8",
    )
    tmp_path.replace(path)


def _normalize_username(username: str) -> str:
    return (username or "").strip()


def _validate_username(username: str) -> str:
    name = _normalize_username(username)
    if not (
        USERNAME_PATTERN.fullmatch(name)
        or EMAIL_PATTERN.fullmatch(name)
        or PHONE_PATTERN.fullmatch(name)
    ):
        raise AuthError(
            "invalid_username",
            (
                "Account must be a valid username, email address, "
                "or phone number."
            ),
        )
    return name


def _validate_password(password: str) -> None:
    if len(password or "") < PASSWORD_MIN_LENGTH:
        raise AuthError(
            "weak_password",
            f"Password must be at least {PASSWORD_MIN_LENGTH} characters.",
        )


def _normalize_display_name(
    display_name: str | None,
    username: str,
) -> str:
    name = (display_name or "").strip()
    if not name:
        return username
    if len(name) > DISPLAY_NAME_MAX_LENGTH:
        raise AuthError(
            "invalid_display_name",
            f"Display name must be <= {DISPLAY_NAME_MAX_LENGTH} characters.",
        )
    return name


def _normalize_avatar_data_url(
    avatar_data_url: str | None,
) -> str | None:
    if avatar_data_url is None:
        return None
    raw = avatar_data_url.strip()
    if not raw:
        return None
    if len(raw) > AVATAR_DATA_URL_MAX_LENGTH:
        raise AuthError(
            "invalid_avatar",
            "Avatar image is too large.",
        )
    if not raw.startswith("data:image/") or ";base64," not in raw:
        raise AuthError(
            "invalid_avatar",
            "Avatar must be a base64 data URL of an image.",
        )
    return raw


def _hash_password(password: str) -> str:
    salt = secrets.token_hex(16)
    digest = hashlib.pbkdf2_hmac(
        "sha256",
        password.encode("utf-8"),
        bytes.fromhex(salt),
        PASSWORD_HASH_ITERS,
    ).hex()
    return (
        f"{PASSWORD_HASH_ALGO}${PASSWORD_HASH_ITERS}"
        f"${salt}${digest}"
    )


def _verify_password(password: str, password_hash: str) -> bool:
    try:
        algo, iterations_str, salt, expected = password_hash.split("$", 3)
    except ValueError:
        return False
    if algo != PASSWORD_HASH_ALGO:
        return False
    try:
        iterations = int(iterations_str)
    except ValueError:
        return False
    actual = hashlib.pbkdf2_hmac(
        "sha256",
        password.encode("utf-8"),
        bytes.fromhex(salt),
        iterations,
    ).hex()
    return hmac.compare_digest(actual, expected)


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


def _load_user_record() -> dict[str, Any] | None:
    data = _read_json(get_auth_users_path(), {})
    if not isinstance(data, dict):
        return None
    if not data.get("username") or not data.get("password_hash"):
        return None
    return data


def _save_user_record(record: dict[str, Any]) -> None:
    _write_json_atomic(get_auth_users_path(), record)


def _delete_user_record() -> None:
    path = get_auth_users_path()
    try:
        path.unlink()
    except FileNotFoundError:
        return


def _build_auth_user_from_record(data: dict[str, Any]) -> AuthUser:
    username = str(data.get("username", "")).strip()
    display_name = str(data.get("display_name") or "").strip() or username
    avatar_data_url = data.get("avatar_data_url")
    if not isinstance(avatar_data_url, str):
        avatar_data_url = None
    return AuthUser(
        username=username,
        display_name=display_name,
        avatar_data_url=avatar_data_url,
    )


def _load_sessions() -> list[dict[str, Any]]:
    data = _read_json(get_auth_sessions_path(), {"sessions": []})
    sessions = data.get("sessions", []) if isinstance(data, dict) else []
    if not isinstance(sessions, list):
        return []
    output: list[dict[str, Any]] = []
    for item in sessions:
        if not isinstance(item, dict):
            continue
        if not item.get("token_hash") or not item.get("username"):
            continue
        if not item.get("expires_at"):
            continue
        output.append(item)
    return output


def _save_sessions(sessions: list[dict[str, Any]]) -> None:
    _write_json_atomic(
        get_auth_sessions_path(),
        {"sessions": sessions},
    )


def _prune_expired_sessions(
    sessions: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
    now = _now_utc()
    source = sessions if sessions is not None else _load_sessions()
    alive: list[dict[str, Any]] = []
    for item in source:
        expires_at = _from_iso(item.get("expires_at", ""))
        if expires_at is None:
            continue
        if expires_at > now:
            alive.append(item)
    if sessions is None and len(alive) != len(source):
        _save_sessions(alive)
    return alive


def is_user_registered() -> bool:
    """Return True if a local account has been created."""
    return _load_user_record() is not None


def get_registered_user() -> AuthUser | None:
    """Return local registered user profile."""
    data = _load_user_record()
    if data is None:
        return None
    return _build_auth_user_from_record(data)


def get_registered_username() -> str | None:
    """Return local registered username."""
    user = get_registered_user()
    return user.username if user else None


def register_first_user(
    username: str,
    password: str,
    display_name: str | None = None,
    avatar_data_url: str | None = None,
) -> str:
    """Create first local user. Registration is single-shot."""
    if is_user_registered():
        raise AuthError(
            "already_registered",
            "This device is already registered and cannot register again.",
        )
    name = _validate_username(username)
    _validate_password(password)
    normalized_display_name = _normalize_display_name(display_name, name)
    normalized_avatar_data_url = _normalize_avatar_data_url(avatar_data_url)
    now = _to_iso(_now_utc())
    _save_user_record(
        {
            "username": name,
            "display_name": normalized_display_name,
            "avatar_data_url": normalized_avatar_data_url,
            "password_hash": _hash_password(password),
            "created_at": now,
            "updated_at": now,
        },
    )
    return name


def verify_user_credentials(username: str, password: str) -> bool:
    """Validate username/password against local stored account."""
    user = _load_user_record()
    if user is None:
        return False
    input_name = _normalize_username(username)
    if input_name != user["username"]:
        return False
    return _verify_password(password, user["password_hash"])


def get_session_ttl_days() -> int:
    """Read auth session TTL days from config."""
    cfg = load_config()
    ttl = getattr(getattr(cfg, "auth", None), "session_ttl_days", 7)
    return max(1, int(ttl))


def set_session_ttl_days(days: int) -> int:
    """Update auth session TTL days in config."""
    if days < 1 or days > 365:
        raise AuthError(
            "invalid_ttl",
            "session_ttl_days must be between 1 and 365.",
        )
    cfg = load_config()
    cfg.auth.session_ttl_days = int(days)
    save_config(cfg)
    return cfg.auth.session_ttl_days


def create_session_for_user(
    username: str,
    ttl_days: int | None = None,
) -> tuple[str, datetime]:
    """Create a new session token and persist it to local text file."""
    if ttl_days is None:
        ttl_days = get_session_ttl_days()
    now = _now_utc()
    expires_at = now + timedelta(days=ttl_days)
    token = secrets.token_urlsafe(32)
    token_hash = _hash_session_token(token)

    sessions = _prune_expired_sessions()
    sessions.append(
        {
            "token_hash": token_hash,
            "username": username,
            "created_at": _to_iso(now),
            "expires_at": _to_iso(expires_at),
        },
    )
    _save_sessions(sessions)
    return token, expires_at


def clear_all_sessions() -> None:
    """Delete all sessions."""
    _save_sessions([])


def revoke_session(token: str) -> bool:
    """Delete one session by cookie token."""
    token_hash = _hash_session_token(token)
    sessions = _prune_expired_sessions()
    kept = [
        item
        for item in sessions
        if not hmac.compare_digest(item.get("token_hash", ""), token_hash)
    ]
    if len(kept) == len(sessions):
        return False
    _save_sessions(kept)
    return True


def authenticate_session(token: str | None) -> AuthUser | None:
    """Resolve session cookie token into authenticated user."""
    if not token:
        return None

    token_hash = _hash_session_token(token)
    sessions = _prune_expired_sessions()
    for item in sessions:
        if hmac.compare_digest(item.get("token_hash", ""), token_hash):
            username = str(item.get("username") or "")
            registered_user = get_registered_user()
            if registered_user is not None and username == registered_user.username:
                return registered_user
            return None
    return None


def change_password(
    username: str,
    old_password: str,
    new_password: str,
) -> None:
    """Change local account password and rotate sessions."""
    user = _load_user_record()
    if user is None:
        raise AuthError("not_registered", "No registered account found.")
    if username != user["username"]:
        raise AuthError("invalid_credentials", "Invalid credentials.")
    if not _verify_password(old_password, user["password_hash"]):
        raise AuthError("invalid_credentials", "Invalid credentials.")

    _validate_password(new_password)
    user["password_hash"] = _hash_password(new_password)
    user["updated_at"] = _to_iso(_now_utc())
    _save_user_record(user)
    clear_all_sessions()


def delete_account(
    username: str,
    password: str,
) -> None:
    """Delete local account after password verification."""
    user = _load_user_record()
    if user is None:
        raise AuthError("not_registered", "No registered account found.")
    if username != user["username"]:
        raise AuthError("invalid_credentials", "Invalid credentials.")
    if not _verify_password(password, user["password_hash"]):
        raise AuthError("invalid_credentials", "Invalid credentials.")

    _delete_user_record()
    clear_all_sessions()
