import json
import logging
import random
import re
from datetime import timedelta

import jwt
import requests
from django.conf import settings
from django.contrib.auth.hashers import check_password, make_password
from django.db import transaction
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from .models import KycSubmission, OtpCode, User, Wallet


logger = logging.getLogger(__name__)

OTP_PURPOSE_LOGIN = "login"
OTP_ALLOWED_ROLES = {User.Role.BUYER, User.Role.SELLER}


def _normalize_phone(raw_phone):
    phone = (raw_phone or "").strip()
    if not phone:
        return None

    digits_only = re.sub(r"\D", "", phone)
    if phone.startswith("+"):
        normalized = f"+{digits_only}"
    elif digits_only.startswith("255") and len(digits_only) == 12:
        normalized = f"+{digits_only}"
    elif digits_only.startswith("0") and len(digits_only) == 10:
        normalized = f"+255{digits_only[1:]}"
    elif len(digits_only) >= 9:
        normalized = f"+{digits_only}"
    else:
        return None

    return normalized


def _generate_otp_code():
    return f"{random.randint(0, 999999):06d}"


def _get_otp_expiry():
    return timezone.now() + timedelta(minutes=getattr(settings, "OTP_EXPIRY_MINUTES", 5))


def _build_auth_token(user):
    issued_at = timezone.now()
    expires_at = issued_at + timedelta(minutes=getattr(settings, "JWT_ACCESS_TOKEN_LIFETIME_MINUTES", 60))
    return jwt.encode(
        {
            "sub": str(user.id),
            "phone": user.phone,
            "role": user.role,
            "type": "access",
            "iat": int(issued_at.timestamp()),
            "exp": int(expires_at.timestamp()),
        },
        getattr(settings, "JWT_SECRET_KEY", settings.SECRET_KEY),
        algorithm=getattr(settings, "JWT_ALGORITHM", "HS256"),
    )


def _send_sms(phone, message):
    sms_url = getattr(settings, "SMS_API_URL", "")
    api_key = getattr(settings, "SMS_API_KEY", "")
    api_secret = getattr(settings, "SMS_API_SECRET", "")
    sender_id = getattr(settings, "SMS_SENDER_ID", "")
    delivery_report_url = getattr(settings, "SMS_DELIVERY_REPORT_URL", "")

    required_settings = {
        "SMS_API_URL": sms_url,
        "SMS_API_KEY": api_key,
        "SMS_API_SECRET": api_secret,
        "SMS_SENDER_ID": sender_id,
    }
    missing_settings = [name for name, value in required_settings.items() if not value]
    if missing_settings:
        raise ValueError(f"SMS gateway settings are incomplete: {', '.join(missing_settings)}")

    payload = {
        "senderId": sender_id,
        "messageType": "text",
        "message": message,
        "contacts": phone,
    }
    if delivery_report_url:
        payload["deliveryReportUrl"] = delivery_report_url

    try:
        response = requests.post(
            sms_url,
            json=payload,
            headers={
                "Content-Type": "application/json",
                "api_key": api_key,
                "api_secret": api_secret,
            },
            timeout=15,
        )
    except requests.RequestException as exc:
        logger.warning("SMS gateway unavailable: %s", exc)
        raise RuntimeError("SMS gateway unavailable") from exc

    if response.status_code != 200:
        logger.warning(
            "SMS gateway rejected OTP request with status %s: %s",
            response.status_code,
            response.text or "<empty body>",
        )
        raise RuntimeError(f"SMS gateway rejected request with status {response.status_code}")

    try:
        return response.json() if response.content else {}
    except ValueError:
        return {}


def _get_active_otp(phone, code=None, role=None):
    queryset = OtpCode.objects.filter(
        phone=phone,
        purpose=OTP_PURPOSE_LOGIN,
        consumed_at__isnull=True,
    ).order_by("-created_at")
    if role is not None:
        queryset = queryset.filter(role=role)
    if code is not None:
        queryset = queryset.filter(code=code)
    return queryset.first()


def _get_latest_otp(phone, role=None):
    return OtpCode.objects.filter(
        phone=phone,
        purpose=OTP_PURPOSE_LOGIN,
        **({"role": role} if role is not None else {}),
    ).order_by("-created_at").first()


def _get_latest_otp_for_code(phone, code, role=None):
    return OtpCode.objects.filter(
        phone=phone,
        purpose=OTP_PURPOSE_LOGIN,
        code=code,
        **({"role": role} if role is not None else {}),
    ).order_by("-created_at").first()


def _is_rate_limited(phone):
    window_minutes = getattr(settings, "OTP_SEND_RATE_LIMIT_WINDOW_MINUTES", 10)
    max_requests = getattr(settings, "OTP_SEND_RATE_LIMIT_COUNT", 3)
    window_start = timezone.now() - timedelta(minutes=window_minutes)
    recent_count = OtpCode.objects.filter(
        phone=phone,
        purpose=OTP_PURPOSE_LOGIN,
        created_at__gte=window_start,
    ).count()
    return recent_count >= max_requests


def _register_failed_attempt(otp_record):
    otp_record.attempts += 1
    update_fields = ["attempts"]
    locked = otp_record.attempts >= getattr(settings, "OTP_MAX_VERIFY_ATTEMPTS", 5)
    if locked:
        otp_record.consumed_at = timezone.now()
        update_fields.append("consumed_at")
    otp_record.save(update_fields=update_fields)
    return locked


def _is_admin_request(request):
    configured_key = getattr(settings, "ADMIN_API_KEY", "")
    request_key = request.headers.get("X-Admin-API-Key", "")

    if configured_key and request_key == configured_key:
        return True

    user = getattr(request, "user", None)
    if user and user.is_authenticated and user.is_staff:
        return True

    return False


def _serialize_otp(otp_record):
    is_used = otp_record.consumed_at is not None
    is_expired = otp_record.expires_at <= timezone.now()
    status_label = "used" if is_used else ("expired" if is_expired else "active")
    return {
        "id": str(otp_record.id),
        "phone": otp_record.phone,
        "code": otp_record.code,
        "purpose": otp_record.purpose,
        "attempts": otp_record.attempts,
        "created_at": otp_record.created_at,
        "expires_at": otp_record.expires_at,
        "consumed_at": otp_record.consumed_at,
        "is_used": is_used,
        "is_verified": is_used,
        "status": status_label,
    }


def _extract_bearer_token(request):
    auth_header = request.headers.get("Authorization", "").strip()
    if not auth_header:
        return None

    # Some proxies/clients can join duplicate Authorization headers with commas.
    if "," in auth_header:
        auth_header = auth_header.split(",", 1)[0].strip()

    if auth_header.lower().startswith("bearer "):
        token = auth_header[7:].strip()
    else:
        token = auth_header

    # Be lenient with accidental double prefix and quoted token values.
    if token.lower().startswith("bearer "):
        token = token[7:].strip()
    token = token.strip().strip('"').strip("'")

    if not token:
        return None

    return token


def _get_authenticated_user(request):
    token = _extract_bearer_token(request)
    if not token:
        return None, "Authorization token is required."

    try:
        payload = jwt.decode(
            token,
            getattr(settings, "JWT_SECRET_KEY", settings.SECRET_KEY),
            algorithms=[getattr(settings, "JWT_ALGORITHM", "HS256")],
        )
    except jwt.ExpiredSignatureError:
        return None, "Authorization token has expired."
    except jwt.InvalidTokenError as exc:
        if getattr(settings, "DEBUG", False):
            return None, f"Invalid authorization token: {exc}"
        return None, "Invalid authorization token."

    user_id = payload.get("sub")
    if not user_id:
        return None, "Invalid authorization token."

    user = User.objects.filter(id=user_id).first()
    if user is None:
        return None, "User not found."

    return user, None


@api_view(['GET'])
def health_check(request):
    return Response({
        "message": "kitongaPayAPIs backend is running successfully"
    })


@api_view(["POST"])
def delivery_report_callback(request):
    logger.info("SMS delivery report received", extra={"payload": request.data})
    return Response(
        {
            "detail": "Delivery report received.",
            "payload": request.data,
        },
        status=status.HTTP_200_OK,
    )


@api_view(["GET"])
def admin_otp_list(request):
    if not _is_admin_request(request):
        return Response({"detail": "Not authorized."}, status=status.HTTP_403_FORBIDDEN)

    queryset = OtpCode.objects.all().order_by("-created_at")
    phone = request.query_params.get("phone")
    purpose = request.query_params.get("purpose")
    used = request.query_params.get("used")

    if phone:
        queryset = queryset.filter(phone=phone)
    if purpose:
        queryset = queryset.filter(purpose=purpose)
    if used == "true":
        queryset = queryset.filter(consumed_at__isnull=False)
    elif used == "false":
        queryset = queryset.filter(consumed_at__isnull=True)

    try:
        limit = int(request.query_params.get("limit", 50))
    except (TypeError, ValueError):
        limit = 50
    limit = max(1, min(limit, 200))

    otps = list(queryset[:limit])
    return Response(
        {
            "count": len(otps),
            "results": [_serialize_otp(otp_record) for otp_record in otps],
        },
        status=status.HTTP_200_OK,
    )


@api_view(["POST"])
def send_otp(request):
    phone = _normalize_phone(request.data.get("phone"))
    role = str(request.data.get("role") or "").strip().lower()
    if not phone:
        return Response({"detail": "A valid phone number is required."}, status=status.HTTP_400_BAD_REQUEST)
    if role not in OTP_ALLOWED_ROLES:
        return Response({"detail": "role must be either 'buyer' or 'seller'."}, status=status.HTTP_400_BAD_REQUEST)

    if _is_rate_limited(phone):
        return Response(
            {"detail": "Too many OTP requests. Please try again later."},
            status=status.HTTP_429_TOO_MANY_REQUESTS,
        )

    otp_code = _generate_otp_code()
    expires_at = _get_otp_expiry()

    OtpCode.objects.filter(
        phone=phone,
        purpose=OTP_PURPOSE_LOGIN,
        consumed_at__isnull=True,
    ).update(consumed_at=timezone.now())

    OtpCode.objects.create(
        phone=phone,
        role=role,
        code=otp_code,
        purpose=OTP_PURPOSE_LOGIN,
        expires_at=expires_at,
    )

    message = f"Your KitongaPay OTP is {otp_code}. It expires in {getattr(settings, 'OTP_EXPIRY_MINUTES', 5)} minutes."

    try:
        gateway_response = _send_sms(phone=phone, message=message)
    except (ValueError, RuntimeError) as exc:
        return Response(
            {"detail": str(exc)},
            status=status.HTTP_503_SERVICE_UNAVAILABLE,
        )

    return Response(
        {
            "detail": "OTP sent successfully.",
            "phone": phone,
            "role": role,
            "expires_at": expires_at,
            "provider_response": gateway_response,
        },
        status=status.HTTP_200_OK,
    )


@api_view(["POST"])
def verify_otp(request):
    phone = _normalize_phone(request.data.get("phone"))
    code = str(request.data.get("otp") or request.data.get("code") or "").strip()
    role = str(request.data.get("role") or "").strip().lower() or None
    now = timezone.now()

    if not phone or not code:
        return Response(
            {"detail": "Phone number and OTP are required."},
            status=status.HTTP_400_BAD_REQUEST,
        )
    if role is not None and role not in OTP_ALLOWED_ROLES:
        return Response({"detail": "role must be either 'buyer' or 'seller'."}, status=status.HTTP_400_BAD_REQUEST)

    with transaction.atomic():
        latest_otp = _get_latest_otp(phone=phone, role=role)
        otp_record = _get_active_otp(phone=phone, role=role)
        submitted_otp = _get_latest_otp_for_code(phone=phone, code=code, role=role)

        if otp_record is None:
            if submitted_otp and submitted_otp.consumed_at is not None:
                return Response({"detail": "OTP already used. Request a new code."}, status=status.HTTP_400_BAD_REQUEST)
            if submitted_otp and submitted_otp.expires_at <= now:
                return Response({"detail": "OTP has expired."}, status=status.HTTP_400_BAD_REQUEST)
            if latest_otp and latest_otp.attempts >= getattr(settings, "OTP_MAX_VERIFY_ATTEMPTS", 5) and latest_otp.expires_at > now:
                return Response(
                    {"detail": "OTP is locked. Request a new code."},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            if latest_otp and latest_otp.expires_at <= now:
                return Response({"detail": "OTP has expired."}, status=status.HTTP_400_BAD_REQUEST)
            return Response({"detail": "Invalid OTP."}, status=status.HTTP_400_BAD_REQUEST)

        if otp_record.expires_at <= now:
            otp_record.consumed_at = now
            otp_record.save(update_fields=["consumed_at"])
            return Response({"detail": "OTP has expired."}, status=status.HTTP_400_BAD_REQUEST)

        if otp_record.attempts >= getattr(settings, "OTP_MAX_VERIFY_ATTEMPTS", 5):
            otp_record.consumed_at = otp_record.consumed_at or now
            otp_record.save(update_fields=["consumed_at"])
            return Response(
                {"detail": "OTP is locked. Request a new code."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if otp_record.code != code:
            if submitted_otp and submitted_otp.consumed_at is not None:
                return Response({"detail": "OTP already used. Request a new code."}, status=status.HTTP_400_BAD_REQUEST)
            if submitted_otp and submitted_otp.expires_at <= now:
                return Response({"detail": "OTP has expired."}, status=status.HTTP_400_BAD_REQUEST)
            locked = _register_failed_attempt(otp_record)
            detail = "OTP is locked. Request a new code." if locked else "Invalid OTP."
            return Response({"detail": detail}, status=status.HTTP_400_BAD_REQUEST)

        otp_record.consumed_at = now
        otp_record.save(update_fields=["consumed_at"])

        otp_role = otp_record.role if otp_record.role in OTP_ALLOWED_ROLES else None
        user_role = otp_role or role or User.Role.BUYER
        user, created = User.objects.get_or_create(phone=phone, defaults={"role": user_role})
        if created:
            Wallet.objects.create(user=user)

    token = _build_auth_token(user)

    return Response(
        {
            "detail": "OTP verified successfully.",
            "token": token,
            "user": {
                "id": str(user.id),
                "phone": user.phone,
                "name": user.name,
                "is_verified": user.is_verified,
                "role": user.role,
            },
        },
        status=status.HTTP_200_OK,
    )


@api_view(["POST"])
def setup_pin(request):
    user, auth_error = _get_authenticated_user(request)
    if auth_error:
        return Response({"detail": auth_error}, status=status.HTTP_401_UNAUTHORIZED)

    pin = str(request.data.get("pin") or "").strip()
    current_pin = str(request.data.get("current_pin") or "").strip()

    if not re.fullmatch(r"\d{4}", pin):
        return Response({"detail": "PIN must be exactly 4 digits."}, status=status.HTTP_400_BAD_REQUEST)

    if user.pin_hash:
        if not current_pin:
            return Response(
                {"detail": "current_pin is required to change PIN."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not re.fullmatch(r"\d{4}", current_pin):
            return Response({"detail": "current_pin must be exactly 4 digits."}, status=status.HTTP_400_BAD_REQUEST)
        if not check_password(current_pin, user.pin_hash):
            return Response({"detail": "Current PIN is incorrect."}, status=status.HTTP_400_BAD_REQUEST)
        if current_pin == pin:
            return Response(
                {"detail": "New PIN must be different from current PIN."},
                status=status.HTTP_400_BAD_REQUEST,
            )

    user.pin_hash = make_password(pin)
    user.save(update_fields=["pin_hash", "updated_at"])

    return Response(
        {
            "detail": "PIN saved successfully.",
            "pin_set": True,
        },
        status=status.HTTP_200_OK,
    )


@api_view(["GET"])
def get_profile(request):
    user, auth_error = _get_authenticated_user(request)
    if auth_error:
        return Response({"detail": auth_error}, status=status.HTTP_401_UNAUTHORIZED)

    return Response(
        {
            "id": str(user.id),
            "phone": user.phone,
            "name": user.name,
            "email": user.email,
            "role": user.role,
            "is_verified": user.is_verified,
            "kyc_level": user.kyc_level,
            "reputation_score": str(user.reputation_score),
            "total_deals": user.total_deals,
            "completed_deals": user.completed_deals,
            "pin_set": bool(user.pin_hash),
            "created_at": user.created_at,
            "updated_at": user.updated_at,
        },
        status=status.HTTP_200_OK,
    )


@api_view(["POST"])
def submit_kyc(request):
    user, auth_error = _get_authenticated_user(request)
    if auth_error:
        return Response({"detail": auth_error}, status=status.HTTP_401_UNAUTHORIZED)

    full_name = str(request.data.get("full_name") or "").strip()
    nin = str(request.data.get("nin") or "").strip()
    nida_image = request.FILES.get("nida_image")

    if not full_name:
        return Response({"detail": "full_name is required."}, status=status.HTTP_400_BAD_REQUEST)
    if not nin:
        return Response({"detail": "nin is required."}, status=status.HTTP_400_BAD_REQUEST)
    if nida_image is None:
        return Response({"detail": "nida_image is required."}, status=status.HTTP_400_BAD_REQUEST)

    existing_submission = KycSubmission.objects.filter(nin=nin).exclude(user=user).first()
    if existing_submission is not None:
        return Response({"detail": "nin is already used by another user."}, status=status.HTTP_400_BAD_REQUEST)

    submission, _ = KycSubmission.objects.update_or_create(
        user=user,
        defaults={
            "full_name": full_name,
            "nin": nin,
            "nida_image": nida_image,
            "status": KycSubmission.Status.PENDING,
            "review_notes": None,
        },
    )

    user.name = full_name
    user.kyc_level = max(user.kyc_level, 1)
    user.save(update_fields=["name", "kyc_level", "updated_at"])

    nida_image_url = request.build_absolute_uri(submission.nida_image.url) if submission.nida_image else None
    return Response(
        {
            "detail": "KYC submitted successfully.",
            "kyc": {
                "id": str(submission.id),
                "full_name": submission.full_name,
                "nin": submission.nin,
                "nida_image_url": nida_image_url,
                "status": submission.status,
                "submitted_at": submission.submitted_at,
                "updated_at": submission.updated_at,
            },
        },
        status=status.HTTP_200_OK,
    )