"""Authentication: password verification + JWT issue/verify.

Production-only mode: every user MUST have a bcrypt password_hash.
Users with NULL/empty hash CANNOT log in (returns False from verify).
This is the deploy-ready behaviour — there is no longer a "dev backdoor".

The legacy dev-mode (NULL hash → any password) was removed for production.
To bootstrap a new user, the admin must call /api/admin/users/{id}/reset-password
or use scripts/init_passwords.py.
"""
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Optional

import bcrypt
from jose import JWTError, jwt
from sqlalchemy import text
from sqlalchemy.orm import Session

from backend.config import settings


def hash_password(plain: str) -> str:
    """Bcrypt hash. Returns 'bcrypt$<hash>' string for storage."""
    # Bcrypt has a 72-byte input limit. Truncate longer passwords (rare).
    pw = plain.encode("utf-8")[:72]
    return bcrypt.hashpw(pw, bcrypt.gensalt()).decode("utf-8")


def verify_password(plain: str, hashed: Optional[str]) -> bool:
    """Strict bcrypt verify. Empty/NULL hash → no login (admin must reset
    via /api/admin/users/{id}/reset-password or scripts/init_passwords.py)."""
    if not hashed:
        return False
    try:
        pw = plain.encode("utf-8")[:72]
        return bcrypt.checkpw(pw, hashed.encode("utf-8"))
    except Exception:
        return False


def create_token(*, user_id: int, username: str, role: str,
                 type_: Optional[str], channel: Optional[str]) -> str:
    expire = datetime.now(timezone.utc) + timedelta(hours=settings.JWT_EXPIRE_HOURS)
    payload = {
        "sub": str(user_id),
        "user_id": user_id,
        "username": username,
        "role": role or "",
        "type": type_ or "",
        "channel": channel or "",
        "exp": expire,
    }
    return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)


def decode_token(token: str) -> Optional[dict]:
    try:
        return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
    except JWTError:
        return None


def authenticate(db: Session, username: str, password: str) -> Optional[dict]:
    """Look up user by username, verify password, return user dict with
    fresh token. Returns None if credentials are invalid or user inactive.
    """
    row = db.execute(
        text("""
            SELECT id, username, display_name, password_hash,
                   role, type, channel, COALESCE(active, true) AS active
            FROM users
            WHERE LOWER(username) = LOWER(:u)
            LIMIT 1
        """),
        {"u": username},
    ).fetchone()
    if not row:
        return None
    user_id, uname, display, pwd_hash, role, type_, channel, active = row
    if not active:
        return None
    if not verify_password(password, pwd_hash):
        return None
    token = create_token(
        user_id=user_id, username=uname, role=role or "",
        type_=type_, channel=channel,
    )
    return {
        "token": token,
        "user": {
            "id": user_id,
            "username": uname,
            "display_name": display,
            "role": role or "",
            "type": type_,
            "channel": channel,
            "must_change_password": pwd_hash is None or pwd_hash == "",
        },
    }


def set_password(db: Session, user_id: int, new_password: str) -> bool:
    new_hash = hash_password(new_password)
    res = db.execute(
        text("UPDATE users SET password_hash = :h WHERE id = :uid"),
        {"h": new_hash, "uid": user_id},
    )
    db.commit()
    return res.rowcount > 0


def change_password(db: Session, user_id: int,
                    old_password: str, new_password: str) -> bool:
    """Verify old password before setting new. Dev-mode (NULL hash)
    bypasses the old-password check so first-time setup works."""
    row = db.execute(
        text("SELECT password_hash FROM users WHERE id = :uid"),
        {"uid": user_id},
    ).fetchone()
    if not row:
        return False
    if not verify_password(old_password, row[0]):
        return False
    return set_password(db, user_id, new_password)
