import hashlib
import json
import os
import re
import secrets
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from urllib.parse import quote

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import RedirectResponse

BASE_DIR = Path(__file__).parent
CREDENTIALS_PATH = BASE_DIR / "data" / "admin_credentials.json"
PBKDF2_ITERATIONS = 120_000

ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin123@gmail.com")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
SESSION_SECRET = os.getenv("SESSION_SECRET", "manjet-change-me-in-production")

LOGIN_PATH = "/admin/login"
CHANGE_PASSWORD_PATH = "/admin/change-password"

PASSWORD_MIN_LENGTH = 8
PASSWORD_MAX_LENGTH = 128
SPECIAL_CHAR_PATTERN = re.compile(r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>/?`~]")

COMMON_PASSWORDS = frozenset(
    {
        "password",
        "password1",
        "password12",
        "password123",
        "admin",
        "admin123",
        "admin1234",
        "12345678",
        "123456789",
        "1234567890",
        "qwerty123",
        "letmein",
        "welcome",
        "changeme",
        "iloveyou",
        "monkey123",
        "football",
        "baseball",
        "trustno1",
    }
)


def password_rule_status(password: str) -> Dict[str, bool]:
    return {
        "length": PASSWORD_MIN_LENGTH <= len(password) <= PASSWORD_MAX_LENGTH,
        "upper": bool(re.search(r"[A-Z]", password)),
        "lower": bool(re.search(r"[a-z]", password)),
        "digit": bool(re.search(r"\d", password)),
        "special": bool(SPECIAL_CHAR_PATTERN.search(password)),
        "no_space": not bool(re.search(r"\s", password)),
        "not_common": password.lower() not in COMMON_PASSWORDS if password else False,
    }


def validate_new_password(password: str) -> Tuple[bool, List[str]]:
    errors: List[str] = []
    rules = password_rule_status(password)

    if not rules["length"]:
        errors.append(
            f"Use between {PASSWORD_MIN_LENGTH} and {PASSWORD_MAX_LENGTH} characters."
        )
    if not rules["upper"]:
        errors.append("Include at least one uppercase letter (A-Z).")
    if not rules["lower"]:
        errors.append("Include at least one lowercase letter (a-z).")
    if not rules["digit"]:
        errors.append("Include at least one number (0-9).")
    if not rules["special"]:
        errors.append("Include at least one special character (!@#$%^&* etc.).")
    if not rules["no_space"]:
        errors.append("Password cannot contain spaces.")
    if password and not rules["not_common"]:
        errors.append("This password is too common. Choose a more unique password.")

    return len(errors) == 0, errors


def _hash_password(password: str, salt: bytes) -> str:
    digest = hashlib.pbkdf2_hmac(
        "sha256",
        password.encode("utf-8"),
        salt,
        PBKDF2_ITERATIONS,
    )
    return digest.hex()


def _load_stored_credentials() -> Optional[Tuple[str, str, bytes]]:
    if not CREDENTIALS_PATH.exists():
        return None
    try:
        data = json.loads(CREDENTIALS_PATH.read_text(encoding="utf-8"))
        username = data["username"]
        password_hash = data["password_hash"]
        salt = bytes.fromhex(data["salt"])
        return username, password_hash, salt
    except (OSError, json.JSONDecodeError, KeyError, ValueError):
        return None


def get_admin_username() -> str:
    stored = _load_stored_credentials()
    if stored:
        return stored[0]
    return ADMIN_USERNAME


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


def check_admin_login(username: str, password: str) -> Tuple[bool, Optional[str]]:
    normalized_username = normalize_login_username(username)
    password = password or ""

    if not normalized_username:
        return False, "wrong_username"

    expected_username = normalize_login_username(get_admin_username())
    if not secrets.compare_digest(normalized_username, expected_username):
        return False, "wrong_username"

    if not password:
        return False, "wrong_password"

    stored = _load_stored_credentials()
    if stored:
        _, password_hash, salt = stored
        candidate_hash = _hash_password(password, salt)
        if not secrets.compare_digest(candidate_hash, password_hash):
            return False, "wrong_password"
        return True, None

    if not secrets.compare_digest(password, ADMIN_PASSWORD):
        return False, "wrong_password"
    return True, None


def verify_admin(username: str, password: str) -> bool:
    success, _ = check_admin_login(username, password)
    return success


def change_admin_password(
    current_password: str,
    new_password: str,
    confirm_password: str,
) -> Tuple[bool, str]:
    current_password = current_password or ""
    new_password = new_password or ""
    confirm_password = confirm_password or ""

    if not verify_admin(get_admin_username(), current_password):
        return False, "Current password is incorrect."

    is_valid, validation_errors = validate_new_password(new_password)
    if not is_valid:
        return False, validation_errors[0]

    if new_password != confirm_password:
        return False, "New password and confirmation do not match."

    if secrets.compare_digest(current_password, new_password):
        return False, "New password must be different from the current password."

    username = get_admin_username()
    salt = os.urandom(16)
    password_hash = _hash_password(new_password, salt)
    payload = {
        "username": username,
        "salt": salt.hex(),
        "password_hash": password_hash,
    }

    try:
        CREDENTIALS_PATH.parent.mkdir(parents=True, exist_ok=True)
        temp_path = CREDENTIALS_PATH.with_suffix(".tmp")
        temp_path.write_text(json.dumps(payload), encoding="utf-8")
        temp_path.replace(CREDENTIALS_PATH)
    except OSError:
        return False, "Could not save the new password. Check server write permissions."

    return True, "Password updated successfully."


def is_authenticated(request: Request) -> bool:
    return bool(request.session.get("is_admin"))


def safe_next_path(next_path: str | None) -> str:
    if not next_path or not next_path.startswith("/dashboard"):
        return "/dashboard/spam"
    return next_path


def _is_public_admin_path(path: str, method: str) -> bool:
    if path == LOGIN_PATH or path.startswith(f"{LOGIN_PATH}/"):
        return True
    if path == "/admin/logout" and method.upper() == "POST":
        return True
    return False


class AdminAuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        path = request.url.path

        if path.startswith("/admin") and not _is_public_admin_path(path, request.method):
            if not is_authenticated(request):
                return RedirectResponse(
                    f"{LOGIN_PATH}?next={quote(path)}",
                    status_code=303,
                )

        if path.startswith("/dashboard"):
            if not is_authenticated(request):
                next_url = path
                if request.url.query:
                    next_url = f"{path}?{request.url.query}"
                return RedirectResponse(
                    f"{LOGIN_PATH}?next={quote(next_url)}",
                    status_code=303,
                )

        return await call_next(request)
