from typing import Optional
import time
from datetime import datetime, timedelta, timezone

import jwt
import hashlib
import hmac
import secrets

try:
    from argon2 import PasswordHasher
    from argon2.exceptions import InvalidHashError, VerificationError
    _hasher = PasswordHasher()
except ImportError:  # hosts where argon2 cannot be installed use PBKDF2 from the standard library
    _hasher = None
from fastapi import Depends, HTTPException, Request, Response
from sqlalchemy.orm import Session

from . import config
from .db import get_db
from .models import User

COOKIE = "em_session"

# --- passwords -------------------------------------------------------------

PBKDF2_ROUNDS = 600_000


def _pbkdf2(pw: str, salt: bytes, rounds: int) -> str:
    return hashlib.pbkdf2_hmac("sha256", pw.encode(), salt, rounds).hex()


def hash_password(pw: str) -> str:
    if _hasher is not None:
        return _hasher.hash(pw)
    salt = secrets.token_bytes(16)
    return f"pbkdf2${PBKDF2_ROUNDS}${salt.hex()}${_pbkdf2(pw, salt, PBKDF2_ROUNDS)}"


def check_password(pw: str, hashed: str) -> bool:
    if hashed.startswith("pbkdf2$"):
        try:
            _, rounds, salt, digest = hashed.split("$")
            return hmac.compare_digest(_pbkdf2(pw, bytes.fromhex(salt), int(rounds)), digest)
        except ValueError:
            return False
    if _hasher is None:
        return False
    try:
        return _hasher.verify(hashed, pw)
    except (VerificationError, InvalidHashError):
        return False


def password_problem(pw: str) -> Optional[str]:
    if len(pw) < 8:
        return "Use at least 8 characters."
    if pw.lower() == pw or not any(c.isdigit() for c in pw):
        return "Mix upper and lower case letters and add a number."
    return None


# --- sessions --------------------------------------------------------------

def start_session(response: Response, user: User):
    exp = datetime.now(timezone.utc) + timedelta(hours=config.SESSION_HOURS)
    token = jwt.encode({"sub": str(user.id), "exp": exp}, config.SECRET_KEY, algorithm="HS256")
    response.set_cookie(COOKIE, token, httponly=True, samesite="lax", secure=config.COOKIE_SECURE,
                        max_age=config.SESSION_HOURS * 3600, path="/")


def end_session(response: Response):
    response.delete_cookie(COOKIE, path="/")


def optional_user(request: Request, db: Session = Depends(get_db)) -> Optional[User]:
    token = request.cookies.get(COOKIE)
    if not token:
        return None
    try:
        data = jwt.decode(token, config.SECRET_KEY, algorithms=["HS256"])
    except jwt.PyJWTError:
        return None
    user = db.get(User, int(data["sub"]))
    return user if user and user.active else None


def current_user(user: Optional[User] = Depends(optional_user)) -> User:
    if not user:
        raise HTTPException(401, "Please sign in.")
    return user


def staff_user(user: User = Depends(current_user)) -> User:
    if user.role not in ("owner", "staff"):
        raise HTTPException(403, "This area is for the store team only.")
    return user


def owner_user(user: User = Depends(current_user)) -> User:
    if user.role != "owner":
        raise HTTPException(403, "Only the store owner can do this.")
    return user


# --- sign-in throttle (per email, in memory; fine for one server process) ---

_fails: dict[str, list[float]] = {}
LIMIT, WINDOW = 5, 15 * 60


def throttle_check(key: str):
    now = time.time()
    recent = [t for t in _fails.get(key, []) if now - t < WINDOW]
    _fails[key] = recent
    if len(recent) >= LIMIT:
        raise HTTPException(429, "Too many wrong attempts. Please wait 15 minutes and try again.")


def throttle_fail(key: str):
    _fails.setdefault(key, []).append(time.time())


def throttle_clear(key: str):
    _fails.pop(key, None)


def throttle_reset_all():
    _fails.clear()
