"""Authentication API routes."""
from datetime import UTC, datetime
from functools import wraps
from typing import Callable, Iterable

from flask import Blueprint, current_app, g, jsonify, request
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from pydantic import ValidationError
from pydantic import Secret
from werkzeug.security import check_password_hash, generate_password_hash

from app.models.user import User
from app.repositories import (
    create_user,
    find_user_by_email,
    find_user_by_username,
)

auth_bp = Blueprint('auth_api', __name__)


def _serializer() -> URLSafeTimedSerializer:
    """Build a serializer using the app secret key."""
    return URLSafeTimedSerializer(current_app.config['SECRET_KEY'])


def _token_ttl_seconds() -> int:
    """Get token expiration in seconds."""
    return int(current_app.config.get('AUTH_TOKEN_TTL_SECONDS', 86400))


def _public_user_payload(user: User) -> dict:
    """Return a safe user payload without password hash."""
    payload = user.model_dump(by_alias=True, mode='json')
    payload.pop('passwdHash', None)
    return payload


def _issue_access_token(user: User) -> str:
    """Issue a signed token for API auth."""
    token_payload = {
        'sub': str(user.id),
        'email': user.email,
        'username': user.username,
        'roles': user.roles,
    }
    return _serializer().dumps(token_payload, salt='auth-access-token')


def _decode_access_token(token: str) -> dict:
    """Decode and validate an access token."""
    return _serializer().loads(
        token,
        salt='auth-access-token',
        max_age=_token_ttl_seconds(),
    )


def auth_required(view_func: Callable):
    """Require a valid Bearer token for protected routes."""
    @wraps(view_func)
    def _wrapped(*args, **kwargs):
        auth_header = request.headers.get('Authorization', '')
        if not auth_header.startswith('Bearer '):
            return jsonify({'error': 'MissingBearerToken'}), 401

        token = auth_header.removeprefix('Bearer ').strip()
        try:
            payload = _decode_access_token(token)
        except SignatureExpired:
            return jsonify({'error': 'TokenExpired'}), 401
        except BadSignature:
            return jsonify({'error': 'InvalidToken'}), 401

        # Attach decoded token payload for downstream route handlers.
        g.auth_user = payload
        return view_func(*args, **kwargs)

    return _wrapped


def require_roles(*allowed_roles: str):
    """Allow access only to users with at least one required role."""
    required = set(allowed_roles)

    def _decorator(view_func: Callable):
        @wraps(view_func)
        def _wrapped(*args, **kwargs):
            auth_user = g.get('auth_user') or {}
            token_roles = set(auth_user.get('roles', []))
            if not token_roles.intersection(required):
                return jsonify({'error': 'Forbidden', 'detail': 'Insufficient role'}), 403
            return view_func(*args, **kwargs)

        return _wrapped

    return _decorator


def require_self_or_admin(param_name: str = 'user_id'):
    """Allow access to resource owner or admins."""
    def _decorator(view_func: Callable):
        @wraps(view_func)
        def _wrapped(*args, **kwargs):
            auth_user = g.get('auth_user') or {}
            requester_id = str(auth_user.get('sub', ''))
            target_id = str(kwargs.get(param_name, ''))
            roles = set(auth_user.get('roles', []))

            if requester_id == target_id or 'admin' in roles:
                return view_func(*args, **kwargs)

            return jsonify({'error': 'Forbidden', 'detail': 'Not allowed for this user'}), 403

        return _wrapped

    return _decorator


def _can_read_user_data(owner_user_id: str, shared_with_user_ids: Iterable[str] | None = None) -> bool:
    """Check whether current requester can read another user's data.

    Access is allowed when requester is admin, owner of the data,
    or a practitioner included in the owner's share list.
    """
    auth_user = g.get('auth_user') or {}
    requester_id = str(auth_user.get('sub', ''))
    requester_roles = set(auth_user.get('roles', []))

    if 'admin' in requester_roles:
        return True

    if requester_id == str(owner_user_id):
        return True

    if 'practitioner' in requester_roles:
        shared_with = {str(user_id) for user_id in (shared_with_user_ids or [])}
        return requester_id in shared_with

    return False


# Public alias for reuse from other route modules.
can_read_user_data = _can_read_user_data


@auth_bp.route('/auth/register', methods=['POST'])
def register():
    """Register a new user and return an access token.
    ---
    tags:
        - Auth
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    $ref: '#/components/schemas/AuthRegisterRequest'
    responses:
        201:
            description: User created and token issued
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/AuthTokenResponse'
        400:
            description: Validation error
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        409:
            description: Email or username already exists
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    data = request.get_json(silent=True) or {}

    username = (data.get('username') or '').strip()
    email = (data.get('email') or '').strip()
    password = data.get('password')
    disp_name = (data.get('dispName') or data.get('displayName') or username).strip()
    role = (data.get('role') or 'patient').strip().lower()

    # Only allow self-registration as patient or practitioner
    allowed_self_register_roles = {'patient', 'practitioner'}
    if role not in allowed_self_register_roles:
        return jsonify({
            'error': 'InvalidRole',
            'detail': f"Role must be one of: {', '.join(sorted(allowed_self_register_roles))}",
        }), 400

    if not username or not email or not password:
        return jsonify({
            'error': 'MissingFields',
            'required': ['username', 'email', 'password']
        }), 400

    if len(password) < 8:
        return jsonify({
            'error': 'WeakPassword',
            'detail': 'Password must be at least 8 characters long.'
        }), 400

    if find_user_by_email(email):
        return jsonify({'error': 'EmailAlreadyExists'}), 409

    if find_user_by_username(username):
        return jsonify({'error': 'UsernameAlreadyExists'}), 409

    user_payload = {
        'username': username,
        'dispName': disp_name,
        'email': email,
        'passwdHash': Secret(generate_password_hash(password)),
        'roles': [role],
        'active': True,
        'residentCountry': data.get('residentCountry', 'TR'),
        'residentCity': data.get('residentCity', 'Ankara'),
        'bloodType': data.get('bloodType'),
        'height': data.get('height'),
        'weight': data.get('weight'),
        'allergies': data.get('allergies', []),
        'sharingInfo': data.get('sharingInfo', []),
    }

    try:
        user = User.model_validate(user_payload)
    except ValidationError as exc:
        return jsonify({'error': 'ValidationError', 'detail': str(exc)}), 400

    created = create_user(user)
    token = _issue_access_token(created)

    return jsonify({
        'accessToken': token,
        'tokenType': 'Bearer',
        'expiresIn': _token_ttl_seconds(),
        'user': _public_user_payload(created),
    }), 201


@auth_bp.route('/auth/login', methods=['POST'])
def login():
    """Login with email or username and return an access token.
    ---
    tags:
        - Auth
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    $ref: '#/components/schemas/AuthLoginRequest'
    responses:
        200:
            description: Authenticated
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/AuthTokenResponse'
        400:
            description: Missing fields
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        401:
            description: Invalid credentials
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
        403:
            description: Inactive user
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    data = request.get_json(silent=True) or {}
    identifier = (data.get('email') or data.get('username') or '').strip()
    password = data.get('password')

    if not identifier or not password:
        return jsonify({
            'error': 'MissingFields',
            'required': ['email or username', 'password']
        }), 400

    user = find_user_by_email(identifier)
    if not user:
        user = find_user_by_username(identifier)

    if not user:
        return jsonify({'error': 'InvalidCredentials'}), 401

    password_hash = user.passwdHash.get_secret_value()
    if not check_password_hash(password_hash, password):
        return jsonify({'error': 'InvalidCredentials'}), 401

    if not user.active:
        return jsonify({'error': 'InactiveUser'}), 403

    token = _issue_access_token(user)

    return jsonify({
        'accessToken': token,
        'tokenType': 'Bearer',
        'expiresIn': _token_ttl_seconds(),
        'user': _public_user_payload(user),
    })


@auth_bp.route('/auth/change-password', methods=['PATCH'])
@auth_required
def change_password():
    """Change the authenticated user's password.
    ---
    tags:
        - Auth
    security:
        - BearerAuth: []
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    type: object
                    properties:
                        currentPassword:
                            type: string
                        newPassword:
                            type: string
                    required: [currentPassword, newPassword]
    responses:
        200:
            description: Password changed successfully
        400:
            description: Missing fields or weak password
        401:
            description: Current password is incorrect
    """
    data = request.get_json(silent=True) or {}
    current_password = data.get('currentPassword')
    new_password = data.get('newPassword')

    if not current_password or not new_password:
        return jsonify({
            'error': 'MissingFields',
            'required': ['currentPassword', 'newPassword'],
        }), 400

    if len(new_password) < 8:
        return jsonify({
            'error': 'WeakPassword',
            'detail': 'Password must be at least 8 characters long.',
        }), 400

    auth_user = g.get('auth_user') or {}
    user_id = str(auth_user.get('sub', ''))

    from app.repositories import find_user_by_id, update_user

    user = find_user_by_id(user_id)
    if not user:
        return jsonify({'error': 'NotFound'}), 404

    if not check_password_hash(user.passwdHash.get_secret_value(), current_password):
        return jsonify({'error': 'InvalidCurrentPassword'}), 401

    new_hash = generate_password_hash(new_password)
    update_user(user_id, {'passwdHash': Secret(new_hash)})

    return jsonify({'message': 'Password changed successfully'})


@auth_bp.route('/auth/verify', methods=['GET'])
def verify_token():
    """Verify a Bearer token and return the token payload.
    ---
    tags:
        - Auth
    security:
        - BearerAuth: []
    responses:
        200:
            description: Token valid
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/AuthVerifyResponse'
        401:
            description: Missing, expired, or invalid token
            content:
                application/json:
                    schema:
                        $ref: '#/components/schemas/ErrorResponse'
    """
    auth_header = request.headers.get('Authorization', '')
    if not auth_header.startswith('Bearer '):
        return jsonify({'error': 'MissingBearerToken'}), 401

    try:
        payload = _decode_access_token(auth_header.removeprefix('Bearer ').strip())
    except SignatureExpired:
        return jsonify({'error': 'TokenExpired'}), 401
    except BadSignature:
        return jsonify({'error': 'InvalidToken'}), 401

    return jsonify({
        'valid': True,
        'payload': payload,
        'verifiedAt': datetime.now(UTC).isoformat(),
    })
