"""Public candidate self-registration (careers portal).

Unauthenticated candidates create their own account here. The user is always
created with role CANDIDATE, a linked Candidate profile is created (optionally
with a résumé), JWT tokens are issued, and — if a job_id is supplied — the new
candidate is immediately applied to that Published JD.
"""

import logging
import random

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.mail import send_mail
from django.db import transaction
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView

from core.responses import success_response, error_response
from core.roles import CANDIDATE
from .services import issue_tokens

User = get_user_model()

logger = logging.getLogger(__name__)

OTP_TTL_SECONDS = 600  # 10 minutes


def _otp_key(email: str) -> str:
    return f"regotp:{email.strip().lower()}"


PHONE_TAKEN_MESSAGE = (
    "This phone number is already registered. Please use a different phone "
    "number or log in to your existing account."
)


def _phone_variants(phone: str) -> list[str]:
    """Likely stored spellings of the same number.

    Numbers reach the system as '9876500001', '+919876500001', '91 98765 00001'
    etc. (validate_phone_number allows an optional '+' and 9-15 digits), so a
    plain exact match would miss real duplicates. We compare on the digits and
    the common country-code prefixes rather than scanning every row in Python.
    """
    digits = "".join(ch for ch in (phone or "") if ch.isdigit())
    if not digits:
        return []
    last10 = digits[-10:] if len(digits) > 10 else digits
    variants = {phone.strip(), digits, last10, f"+{digits}", f"91{last10}", f"+91{last10}"}
    return [v for v in variants if v]


def phone_already_registered(phone: str, exclude_user_id=None) -> bool:
    """True if this phone already belongs to a user account or candidate profile.

    Application-level uniqueness (no schema change): the number can live on
    User.phone and/or Candidate.phone_number, so both are checked.
    """
    from django.db.models import Q

    from apps.candidates.models import Candidate

    variants = _phone_variants(phone)
    if not variants:
        return False

    user_q = Q()
    cand_q = Q()
    for v in variants:
        user_q |= Q(phone=v)
        cand_q |= Q(phone_number=v)

    users = User.objects.filter(user_q)
    if exclude_user_id:
        users = users.exclude(pk=exclude_user_id)
    if users.exists():
        return True

    # Candidate.objects excludes soft-deleted rows by default; a deleted profile
    # must not permanently block a number from being reused.
    return Candidate.objects.filter(cand_q).exists()


class SendRegisterOTPView(APIView):
    """POST /api/v1/auth/register/send-otp/  (public)
    Body: { email }. Emails a 6-digit verification code, valid 10 minutes.
    Rejects emails that already have an account (409 -> use login)."""

    permission_classes = [AllowAny]

    def post(self, request):
        email = (request.data.get("email") or "").strip().lower()
        mobile = (request.data.get("mobile") or request.data.get("phone") or "").strip()
        if not email or "@" not in email:
            return error_response("Enter a valid email address.", status_code=400)
        if User.objects.filter(email__iexact=email).exists():
            return error_response(
                "An account with this email already exists. Please log in instead.",
                status_code=409,
            )
        # Reject a duplicate phone BEFORE any code is generated or sent, so a
        # duplicate registration attempt never triggers an OTP/verification
        # email or SMS.
        if mobile and phone_already_registered(mobile):
            return error_response(PHONE_TAKEN_MESSAGE, status_code=409)
        # ONE code, delivered on BOTH channels (email + SMS) so registration
        # still works if one channel fails.
        otp = f"{random.randint(100000, 999999)}"
        cache.set(_otp_key(email), otp, timeout=OTP_TTL_SECONDS)

        console_only = "console" in (settings.EMAIL_BACKEND or "")
        sent, errors = [], []

        # --- Email ---
        try:
            send_mail(
                subject="Your Indovision Careers verification code",
                message=(
                    f"Your verification code is {otp}.\n\n"
                    "It is valid for 10 minutes. If you did not request this, ignore this email."
                ),
                from_email=settings.DEFAULT_FROM_EMAIL,
                recipient_list=[email],
                fail_silently=False,
            )
            sent.append("email")
        except Exception as e:  # noqa: BLE001
            errors.append(f"email: {e}")

        # --- SMS (same OTP) — only if a mobile number was supplied ---
        if mobile:
            try:
                from apps.candidates.gateways import send_sms_otp
                ok, detail = send_sms_otp(mobile, otp)
                if ok:
                    sent.append("mobile")
                else:
                    errors.append(f"sms: {detail}")
            except Exception as e:  # noqa: BLE001
                errors.append(f"sms: {e}")

        # Console email backend "succeeds" for dev even though nothing is delivered.
        if not sent and not console_only:
            return error_response(
                "Could not send the verification code. " + "; ".join(errors),
                status_code=502,
            )

        where = " and ".join(sent) if sent else "console"
        return success_response(
            {"email": email, "mobile": mobile, "channels": sent,
             "expires_in": OTP_TTL_SECONDS, "console_only": console_only},
            f"Verification code sent to your {where}.",
        )


class RegisterView(APIView):
    """POST /api/v1/auth/register/  (public)

    Body (multipart or JSON):
      email, password, first_name, last_name, phone   [required]
      resume (file)                                    [optional]
      job_id                                           [optional — auto-apply]
    Returns: { access, refresh, user }.
    """

    permission_classes = [AllowAny]
    parser_classes = [MultiPartParser, FormParser, JSONParser]

    def post(self, request):
        data = request.data
        email = (data.get("email") or "").strip().lower()
        password = data.get("password") or ""
        first_name = (data.get("first_name") or "").strip()
        last_name = (data.get("last_name") or "").strip()
        phone = (data.get("phone") or data.get("phone_number") or "").strip()
        otp = (data.get("otp") or "").strip()
        resume = request.FILES.get("resume")
        job_id = data.get("job_id") or None
        telegram_chat_id = (data.get("telegram_chat_id") or "").strip() or None
        whatsapp_number = (data.get("whatsapp_number") or "").strip() or None

        # --- validation ---
        missing = [
            f for f, v in (
                ("email", email), ("password", password),
                ("first_name", first_name), ("last_name", last_name),
                ("phone", phone), ("otp", otp),
            ) if not v
        ]
        if missing:
            return error_response(
                f"Missing required field(s): {', '.join(missing)}.", status_code=400
            )
        if "@" not in email or "." not in email.split("@")[-1]:
            return error_response("Enter a valid email address.", status_code=400)
        if len(password) < 8:
            return error_response("Password must be at least 8 characters.", status_code=400)
        if User.objects.filter(email__iexact=email).exists():
            return error_response(
                "An account with this email already exists. Please log in instead.",
                status_code=409,
            )
        # Phone uniqueness — enforced here (not just in the UI or the OTP step),
        # so a direct API call can't create a duplicate account either. Checked
        # before the OTP is consumed and before any record is created.
        if phone_already_registered(phone):
            return error_response(PHONE_TAKEN_MESSAGE, status_code=409)
        # --- email OTP verification ---
        cached = cache.get(_otp_key(email))
        if not cached:
            return error_response(
                "Your verification code has expired. Please request a new one.",
                status_code=400,
            )
        if str(cached) != otp:
            return error_response("Incorrect verification code.", status_code=400)

        # --- create user + candidate profile atomically ---
        try:
            with transaction.atomic():
                user = User(
                    email=email,
                    username=email,
                    first_name=first_name,
                    last_name=last_name,
                    phone=phone,
                    role=CANDIDATE,
                    is_active=True,
                )
                user.set_password(password)
                user.save()  # post_save signals attach the CANDIDATE group

                from apps.candidates.models import Candidate
                from apps.jobs.tracking_tokens import resolve_tracking_token

                # `t` is the encrypted tracking token carried by the public
                # application URL — it resolves the platform server-side, so the
                # source can't be read or edited by the visitor. The plain
                # `source` value stays accepted for links already distributed
                # before tokens existed. Neither present → DIRECT, as before.
                tracked = resolve_tracking_token(str(data.get("t") or "").strip())
                raw_source = (
                    tracked["source"] if tracked
                    else (data.get("source") or "").strip().upper()
                )
                candidate_source = (
                    raw_source if raw_source in Candidate.SourceChoices.values else Candidate.SourceChoices.DIRECT
                )

                candidate = Candidate.objects.create(
                    user=user,
                    first_name=first_name,
                    last_name=last_name,
                    email=email,
                    phone_number=phone,
                    source=candidate_source,
                    created_by=user,
                )
                if resume:
                    candidate.resume = resume
                    candidate.save(update_fields=["resume"])

                # Best-effort: never let this block account creation — skip
                # silently if the chat id is already claimed by a different
                # candidate, or the column isn't there yet. Runs in its own
                # savepoint so a failure here can't poison the outer
                # transaction that already created the user + candidate.
                if telegram_chat_id:
                    try:
                        with transaction.atomic():
                            if not Candidate.objects.filter(telegram_chat_id=telegram_chat_id).exclude(id=candidate.id).exists():
                                candidate.telegram_chat_id = telegram_chat_id
                                candidate.save(update_fields=["telegram_chat_id"])
                    except Exception:
                        logger.exception("Could not link telegram_chat_id for %s during registration", email)
                if whatsapp_number:
                    try:
                        with transaction.atomic():
                            if not Candidate.objects.filter(whatsapp_number=whatsapp_number).exclude(id=candidate.id).exists():
                                candidate.whatsapp_number = whatsapp_number
                                candidate.save(update_fields=["whatsapp_number"])
                    except Exception:
                        logger.exception("Could not link whatsapp_number for %s during registration", email)
        except Exception as e:  # noqa: BLE001 — surface a clean message, never a 500
            return error_response(f"Could not create account: {e}", status_code=400)

        cache.delete(_otp_key(email))  # OTP is single-use

        # --- parse the résumé (accuracy-first LLM parser) so the profile-
        # completion form can be pre-filled with high-confidence values only.
        # Never blocks or fails registration — a parse error just yields {}.
        parsed_data = {}
        if resume and getattr(candidate, "resume", None):
            try:
                from apps.candidates.llm_parser import parse_resume
                parsed_data, _parse_source = parse_resume(candidate.resume.path, user=user)
            except Exception:  # noqa: BLE001
                logger.exception("Résumé parse during registration failed for %s", email)
                parsed_data = {}

        # --- optional: apply to a job right away ---
        applied_to = None
        if job_id:
            applied_to = _safe_apply(candidate, job_id, user)

        tokens = issue_tokens(user)
        return success_response(
            {
                **tokens,
                "user": {
                    "id": user.id,
                    "email": user.email,
                    "first_name": user.first_name,
                    "last_name": user.last_name,
                    "role": user.role,
                },
                "candidate_id": candidate.id,
                "applied_to_job": applied_to,
                "parsed_data": parsed_data,
            },
            "Registration successful.",
            status_code=201,
        )


def _safe_apply(candidate, job_id, user):
    """Create a JobApplication for a Published JD; return the job id or None.
    Never raises — registration must succeed even if the apply step can't."""
    try:
        from apps.jobs.models import JobDescription
        from apps.pipeline.models import JobApplication, PipelineStage

        job = JobDescription.objects.filter(id=job_id, status="Published").first()
        if not job:
            return None
        stage = PipelineStage.objects.filter(is_active=True).order_by("sort_order", "id").first()
        application, created = JobApplication.objects.get_or_create(
            candidate=candidate, job=job,
            defaults={"stage": stage, "created_by": user},
        )
        if created:
            # Notify the JD's assigned recruiter(s) and confirm receipt to the
            # candidate — same as the careers-portal apply flow. Neither call
            # raises; registration must succeed even if either email fails.
            from apps.notifications.services import (
                notify_candidate_of_application,
                notify_recruiters_of_application,
            )
            notify_recruiters_of_application(application)
            notify_candidate_of_application(application)
        return job.id
    except Exception:  # noqa: BLE001
        return None
