import json
import logging
import os
import time
from collections import deque
import requests

from django.conf import settings
from django.db import transaction
from django.db.models import F
from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.http import require_POST

from . import llm, vision, naukriyan
from .models import Interview, ProctorEvent, Question
import base64
import json
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from urllib.parse import unquote
import os
from dotenv import load_dotenv
from django.http import HttpResponse

load_dotenv()


log = logging.getLogger(__name__)


# These are tuning values, not exact real-world measurements.
WARNING_MIN_FRAMES = 4
VISION_WINDOW_SIZE = 10
VISION_MIN_HITS = 6
MAX_FRAME_REQUEST_BYTES = 12 * 1024 * 1024
MAX_ELAPSED_SECONDS_PER_FRAME = 2.0


def _session_authorized(request, pk):
    """Only the browser session that started this interview may submit data."""
    return bool(request.session.get(f"iv_{pk}"))

def already_done(request):
    return render(request, "already_done.html")


def _parse_json_body(request):
    try:
        payload = json.loads(request.body or b"{}")
    except (json.JSONDecodeError, UnicodeDecodeError):
        return None
    return payload if isinstance(payload, dict) else None


def _verify_temporal_detection(request, pk, event_name, detected):
    """
    Verify a detection across multiple frames using Django session storage.
    Returns True only after enough recent frames contain the detection.
    This is a lightweight per-browser-session verifier.
    """
    key = f"vision_history_{pk}_{event_name}"
    history = request.session.get(key, [])
    if not isinstance(history, list):
        history = []

    history.append(bool(detected))
    history = history[-VISION_WINDOW_SIZE:]
    request.session[key] = history

    return (
        len(history) == VISION_WINDOW_SIZE
        and sum(1 for value in history if value) >= VISION_MIN_HITS
    )


def _reset_vision_history(request, pk):
    for suffix in (
        "phone",
        "multiple_people",
        "away",
        "frame_time",
        "warning_started",
        "logged_phone",
        "logged_multiple_people",
    ):
        request.session.pop(f"vision_history_{pk}_{suffix}", None)
    request.session.pop(f"away_{pk}", None)
    request.session.pop(f"frame_time_{pk}", None)
    request.session.pop(f"warning_started_{pk}", None)
    request.session.pop(f"logged_phone_{pk}", None)
    request.session.pop(f"logged_multiple_people_{pk}", None)


def home(request, token=None):
    # Browser ki favicon request ko ignore karo
    if token in ("favicon.ico", "robots.txt"):
        return HttpResponse(status=204)

    if not token:
        return render(request, "home.html", {"stage": "home"})

    try:
        # 1. Token saaf karo (URL encoding, spaces, padding)
        token = unquote(token).strip()
        token = token.replace(" ", "+")      # '+' kabhi space ban jaata hai
        token = token.rstrip("=")
        token += "=" * (-len(token) % 4)

        # 2. Base64 decode: urlsafe (- _) aur normal (+ /) dono chalenge
        try:
            payload = base64.urlsafe_b64decode(token)
        except Exception:
            payload = base64.b64decode(token)

        if len(payload) < 28:
            raise ValueError("Token too short")

        # 3. Key
        key = base64.b64decode(
            "ME9Tj9mbn3zEj3ZK2IgN69BPmNiIhcKhm26SIdXN6M4=",
            validate=True,
        )
        if len(key) != 32:
            raise ValueError("Invalid AES-GCM key")

        aes = AESGCM(key)

        # 4. Decrypt: pehle layout nonce | tag | ciphertext,
        #    nahi chala to nonce | ciphertext | tag
        nonce = payload[:12]
        try:
            tag = payload[12:28]
            ciphertext = payload[28:]
            plaintext = aes.decrypt(nonce, ciphertext + tag, None)
        except Exception:
            plaintext = aes.decrypt(nonce, payload[12:], None)

        # 5. JSON nikalo
        data = json.loads(plaintext.decode("utf-8"))
        if not isinstance(data, dict):
            raise ValueError("Invalid candidate data")

        name = str(data.get("candidate_name") or data.get("name") or "").strip()
        jd = str(data.get("jd") or data.get("job_description") or "").strip()

        def _to_int(v):
            try:
                return int(v)
            except (TypeError, ValueError):
                return None

        jsuser_id = _to_int(data.get("jsuser_id") or data.get("js_user_id") or data.get("user_id"))
        job_id = _to_int(data.get("job_id") or data.get("jobId"))
        print("Request data:", data)
        print("JS User ID:", jsuser_id)
        print("Job ID:", job_id)

        log.info("Token payload keys: %s", list(data.keys()))

        if not name or not jd:
            raise ValueError("Candidate name or JD is missing")
        

    except Exception:
        log.exception("Token decoding failed")
        return render(
            request,
            "home.html",
            {
                "stage": "home",
                "error": "Invalid link or candidate details are missing.",
            },
            status=400,
        )
    # Check whether the candidate has an existing interview
    try:
        response = requests.post(
            settings.CHECK_EXISTING_URL,
            json={
                "jsuser_id": jsuser_id,
                "job_id": job_id,
                },
            headers={
                "X-Authorization": os.getenv("DJANGO_SERVICE_TOKEN"),
                },
        timeout=10,
        )
        print("API Status:", response.status_code)
        print("API Response:", response.text[:1000])
        response.raise_for_status()
        api_data = response.json()
        # Validate API response

        if (
            not isinstance(api_data, dict)
            or not isinstance(api_data.get("success"), bool)
            or not isinstance(api_data.get("already_interviewed"), bool)
            ):
            raise ValueError("Invalid check-existing API response")
    
        # Allow ONLY when success=True and already_interviewed=False
        if (
            api_data["success"] is True
            and api_data["already_interviewed"] is False
            ):
            log.info(
                "Interview allowed for jsuser_id=%s, job_id=%s",
                jsuser_id,
                job_id,
            )
    
        else:
            log.info(
                "Interview blocked: jsuser_id=%s, job_id=%s, "
                "success=%s, already_interviewed=%s",
                jsuser_id,
                job_id,
                api_data["success"],
                api_data["already_interviewed"],
                )
            return render(
                request,
                "already_done.html",
                {
                    "stage": "home",
                    "error": (
                        "You cannot start this interview. "
                        "You may have already completed an interview "
                        "or are not eligible."
                    ),
                },
                status=403,
            )
    except (requests.RequestException, ValueError):
        log.exception("Check-existing API request failed")
        return render(
            request,
            "already_done.html",
            {
                "stage": "home",
                "error": (
                    "Unable to verify your interview eligibility. "
                    "Please try again later."
                ),
            },
            status=503,
        )

    # 6. Interview create karo
    iv = Interview.objects.create(
        candidate_name=name,
        jd=jd,
        jsuser_id=jsuser_id,
        job_id=job_id
    )

    try:
        # 7. Questions generate karo
        generated_questions = llm.generate_questions(iv.jd)

        for i, q in enumerate(generated_questions):
            question_text = str(q.get("question", "")).strip()
            if question_text:
                Question.objects.create(interview=iv, order=i, text=question_text)

        if not iv.questions.exists():
            raise ValueError("No interview questions generated")

    except Exception:
        log.exception("Question generation failed for interview %s", iv.pk)
        iv.delete()
        return render(
            request,
            "home.html",
            {
                "stage": "home",
                "error": "Could not prepare the interview. Please try again.",
            },
            status=502,
        )

    # 8. Session initialize
    request.session[f"iv_{iv.pk}"] = True
    _reset_vision_history(request, iv.pk)

    # 9. Interview room par redirect
    return redirect("room", pk=iv.pk)

def room(request, pk):
    iv = get_object_or_404(Interview, pk=pk)

    current = iv.questions.filter(answer="").order_by("order").first()

    if not current:
        if not iv.report:
            try:
                finalize_interview(iv)
            except Exception:
                log.exception(
                    "finalize_interview failed (room) for interview %s",
                    pk,
                )

        return render(
            request,
            "app.html",
            {
                "stage": "done",
                "iv": iv,
            },
        )

    if not current.asked_at:
        current.asked_at = timezone.now()
        current.save(update_fields=["asked_at"])

    return render(
        request,
        "app.html",
        {
            "stage": "room",
            "iv": iv,
            "q": current,
            "total": iv.questions.count(),
            "number": current.order + 1,
        },
    )


@require_POST
def answer(request, pk):
    if not _session_authorized(request, pk):
        return JsonResponse({"error": "not allowed"}, status=403)

    iv = get_object_or_404(Interview, pk=pk)
    payload = _parse_json_body(request)
    if payload is None or not isinstance(payload.get("answer"), str):
        return JsonResponse({"error": "invalid answer payload"}, status=400)

    text = payload["answer"].strip()
    if not text:
        return JsonResponse({"error": "answer cannot be empty"}, status=400)

    answered_at = timezone.now()

    with transaction.atomic():
        q = (
            Question.objects.select_for_update()
            .filter(interview=iv, answer="")
            .order_by("order")
            .first()
        )

        if q is None:
            if not iv.report:
                try:
                    finalize_interview(iv)
                except Exception:
                    log.exception(
                        "finalize_interview failed (answer) for interview %s",
                        pk,
                    )
            return JsonResponse({"done": True})

        # Avoid holding a database row lock while making the LLM call.
        question_id = q.pk
        question_text = q.text

    try:
        ev = llm.evaluate_answer(iv.jd, question_text, text)
        score = ev["score"]
        feedback = ev["feedback"]
    except Exception:
        log.exception("Answer evaluation failed for interview %s", pk)
        return JsonResponse(
            {"error": "Could not evaluate the answer. Please try again."},
            status=502,
        )

    with transaction.atomic():
        q = Question.objects.select_for_update().get(pk=question_id)
        # If a duplicate request already answered it, don't overwrite that answer.
        if q.answer:
            return JsonResponse({"error": "question already answered"}, status=409)

        q.answer = text
        q.score = score
        q.feedback = feedback
        q.answered_at = answered_at
        if q.asked_at:
            q.time_taken_seconds = max(
                0, int((answered_at - q.asked_at).total_seconds())
            )
        q.save()

    nxt = iv.questions.filter(answer="").order_by("order").first()
    if not nxt:
        try:
            finalize_interview(iv)
        except Exception:
            log.exception("finalize_interview failed (answer) for interview %s", pk)
        return JsonResponse({"done": True})

    if not nxt.asked_at:
        nxt.asked_at = timezone.now()
        nxt.save(update_fields=["asked_at"])

    return JsonResponse(
        {
            "done": False,
            "question": nxt.text,
            "number": nxt.order + 1,
        }
    )


@require_POST
def frame(request, pk):
    if not _session_authorized(request, pk):
        return JsonResponse({"error": "not allowed"}, status=403)

    if len(request.body) > MAX_FRAME_REQUEST_BYTES:
        return JsonResponse({"error": "frame request too large"}, status=413)

    iv = Interview.objects.filter(pk=pk).first()
    if iv is None:
        return JsonResponse({"error": "interview not found"}, status=404)

    current = iv.questions.filter(answer="").order_by("order").first()
    if current is None:
        return JsonResponse(
            {"warning": "", "violation": "", "total": iv.events.count()}
        )

    payload = _parse_json_body(request)
    if payload is None or not isinstance(payload.get("image"), str):
        return JsonResponse({"error": "invalid image payload"}, status=400)

    data_url = payload["image"]
    if not data_url.startswith("data:image/"):
        return JsonResponse({"error": "expected an image data URL"}, status=400)

    # Use the existing vision.inspect(data_url) signature. It performs the
    # face and object inference once and returns (violation, warning).
    try:
        violation, msg = vision.inspect(data_url)
    except (ValueError, RuntimeError) as exc:
        log.warning("Invalid/failed vision frame for interview %s: %s", pk, exc)
        return JsonResponse({"error": "could not process frame"}, status=400)
    except Exception:
        log.exception("Vision processing failed for interview %s", pk)
        return JsonResponse({"error": "vision processing failed"}, status=500)

    now = time.time()
    previous_time = request.session.get(f"frame_time_{pk}")
    request.session[f"frame_time_{pk}"] = now

    elapsed = 0.0
    if isinstance(previous_time, (int, float)):
        elapsed = max(0.0, min(now - previous_time, MAX_ELAPSED_SECONDS_PER_FRAME))

    # Verify phone and multiple-person detections across a rolling window.
    phone_verified = _verify_temporal_detection(
        request, pk, "phone", violation == "phone"
    )
    people_verified = _verify_temporal_detection(
        request, pk, "multiple_people", violation == "multiple_people"
    )

    verified_violation = ""
    if phone_verified:
        verified_violation = "phone"
    elif people_verified:
        verified_violation = "multiple_people"

    # Require consecutive warning frames before counting a distraction event.
    away_key = f"away_{pk}"
    count = request.session.get(away_key, 0) + 1 if msg else 0
    request.session[away_key] = count

    warning = msg if count >= WARNING_MIN_FRAMES else ""

    # Track when a warning streak began so elapsed time is counted only
    # after the configured consecutive-frame threshold is reached.
    started_key = f"warning_started_{pk}"
    if msg and count == 1:
        request.session[started_key] = now
    elif not msg:
        request.session.pop(started_key, None)

    if warning and current:
        Question.objects.filter(pk=current.pk).update(
            distracted_seconds=F("distracted_seconds") + elapsed
        )

    # Log one event per warning streak, not once per frame.
    if count == WARNING_MIN_FRAMES:
        ProctorEvent.objects.create(
            interview=iv,
            message=msg,
            question=current,
        )
        if current:
            Question.objects.filter(pk=current.pk).update(
                distraction_count=F("distraction_count") + 1
            )

    # Log only the transition into a verified violation; do not create an
    # event for every subsequent frame in the same continuous detection.
    for kind, verified in (
        ("phone", phone_verified),
        ("multiple_people", people_verified),
    ):
        state_key = f"logged_{kind}_{pk}"
        was_logged = bool(request.session.get(state_key, False))

        if verified and not was_logged:
            event_messages = {
                "phone": "Persistent phone detection; review required.",
                "multiple_people": (
                    "Persistent multiple-person detection; review required."
                ),
            }
            ProctorEvent.objects.create(
                interview=iv,
                message=event_messages[kind],
                question=current,
            )
            request.session[state_key] = True
        elif not verified:
            request.session[state_key] = False

    return JsonResponse(
        {
            "warning": warning,
            "violation": verified_violation,
            "total": iv.events.count(),
        }
    )
def report(request, pk):
    iv = get_object_or_404(Interview, pk=pk)
    if not request.user.is_staff:
        return redirect("room", pk=pk)

    if not iv.report:
        try:
            finalize_interview(iv)
        except Exception:
            log.exception("finalize_interview failed (report) for interview %s", pk)
            return render(
                request,
                "app.html",
                {
                    "stage": "report",
                    "iv": iv,
                    "events": iv.events.all(),
                    "error": "The report could not be generated yet.",
                },
                status=502,
            )

    return render(
        request,
        "app.html",
        {
            "stage": "report",
            "iv": iv,
            "events": iv.events.all(),
        },
    )


@require_POST
def video_chunk(request, pk):
    iv = get_object_or_404(Interview, pk=pk)
    if not _session_authorized(request, pk):
        return HttpResponseBadRequest("not allowed")

    chunk = request.FILES.get("chunk")
    if not chunk:
        return HttpResponseBadRequest("no chunk")

    # Prevent a single upload request from being unbounded.
    max_chunk_size = 25 * 1024 * 1024
    if chunk.size > max_chunk_size:
        return JsonResponse({"error": "video chunk too large"}, status=413)

    rel = iv.video.name or f"interview_videos/interview_{iv.pk}.webm"
    path = os.path.join(settings.MEDIA_ROOT, rel)
    os.makedirs(os.path.dirname(path), exist_ok=True)

    try:
        with open(path, "ab") as f:
            for part in chunk.chunks():
                f.write(part)
    except OSError:
        log.exception("Could not save video chunk for interview %s", pk)
        return JsonResponse({"error": "could not save video chunk"}, status=500)

    if not iv.video:
        iv.video.name = rel
        iv.save(update_fields=["video"])

    return JsonResponse({"ok": True})


#
#def finalize_interview(iv):
#    """Save the final report and retry Naukriyan sync when needed."""
#    iv.refresh_from_db()
#
#    # Generate the report only once.
#    if not iv.report:
#        qs = list(iv.questions.all().order_by("order"))
#
#        total_time = sum(q.time_taken_seconds for q in qs) or 1
#        total_distracted = sum(q.distracted_seconds for q in qs)
#
#        ratio = min(total_distracted / total_time, 1)
#        integrity = round(10 * (1 - ratio), 1)
#
#        rows = []
#        for i, q in enumerate(qs, 1):
#            rows.append(
#                f"Q{i}: {q.text}\n"
#                f"Answer: {q.answer}\n"
#                f"Answer score: {q.score}/10\n"
#                f"Time taken: {q.time_taken_seconds}s | "
#                f"Distraction indicators: "
#                f"{round(q.distracted_seconds, 1)}s | "
#                f"Warning events: {q.distraction_count}"
#            )
#
#        interview_data = "\n\n".join(rows)
#
#        result = llm.rate_candidate(
#            iv.candidate_name,
#            iv.jd,
#            interview_data,
#            total_time,
#            round(total_distracted, 1),
#            integrity,
#            iv.events.count(),
#        )
#
#        iv.total_distracted_seconds = round(total_distracted, 1)
#        iv.integrity_score = integrity
#        iv.overall_score = result["overall_score"]
#        iv.ai_recommendation = result["recommendation"]
#        iv.report = result["report"]
#        iv.status = "pending"
#        iv.hr_note = 'null'
#
#        # Save report and scores before calling the external API.
#        iv.save(
#            update_fields=[
#                "total_distracted_seconds",
#                "integrity_score",
#                "overall_score",
#                "ai_recommendation",
#                "report",
#                "status",
#                "hr_note",
#            ]
#        )
#
#    # Do not send the same successfully synced interview again.
#    iv.refresh_from_db()
#
#    if iv.synced_at:
#        log.info(
#            "Interview %s already synced to Naukriyan.",
#            iv.pk,
#        )
#        return
#
#    try:
#        # push_interview should read the latest saved report and HR data.
#        naukriyan.push_interview(iv)
#
#    except Exception:
#        log.exception(
#            "Naukriyan push failed for interview %s. "
#            "Report remains saved; sync can be retried.",
#            iv.pk,
#        )
#        return
#
#    # Mark synced only after push_interview succeeds.
#    iv.synced_at = timezone.now()
#    iv.save(update_fields=["synced_at"])
#
#    log.info("Interview %s synced successfully.", iv.pk)

def finalize_interview(iv):
    """Save the final interview report and retry Naukriyan sync if needed."""

    iv.refresh_from_db()

    # Generate the report only once.
    if not iv.report:
        qs = list(iv.questions.all().order_by("order"))

        total_time = sum(
            q.time_taken_seconds or 0 for q in qs
        ) or 1

        total_distracted = sum(
            q.distracted_seconds or 0 for q in qs
        )

        ratio = min(total_distracted / total_time, 1)
        integrity = round(10 * (1 - ratio), 1)

        rows = []

        for i, q in enumerate(qs, start=1):
            rows.append(
                f"Q{i}: {q.text}\n"
                f"Answer: {q.answer}\n"
                f"Answer score: "
                f"{q.score if q.score is not None else 'N/A'}/10\n"
                f"Feedback: {q.feedback or 'N/A'}\n"
                f"Time taken: {q.time_taken_seconds}s | "
                f"Distraction indicators: "
                f"{round(q.distracted_seconds or 0, 1)}s | "
                f"Warning events: {q.distraction_count}\n"
                f"Typed characters: {q.typed_chars} | "
                f"Voice characters: {q.voice_chars} | "
                f"Paste attempts: {q.paste_attempts} | "
                f"Tab switches: {q.tab_switches} | "
                f"Suspicious input: {q.suspicious_input}"
            )

        interview_data = "\n\n".join(rows)

        # Generate the AI report.
        result = llm.rate_candidate(
            iv.candidate_name,
            iv.jd,
            interview_data,
            total_time,
            round(total_distracted, 1),
            integrity,
            iv.events.count(),
        )

        # Save all report fields defined in Interview model.
        iv.total_distracted_seconds = round(total_distracted, 1)
        iv.integrity_score = integrity
        iv.overall_score = result["overall_score"]
        iv.ai_recommendation = result["recommendation"]
        iv.report = result["report"]
        iv.status = "pending"

        # hr_note is a TextField; empty string is preferable
        # to the literal string "null".
        if not iv.hr_note:
            iv.hr_note = "Null"

        iv.save(
            update_fields=[
                "total_distracted_seconds",
                "integrity_score",
                "overall_score",
                "ai_recommendation",
                "report",
                "status",
                "hr_note",
            ]
        )

        log.info(
            "Interview %s report saved successfully.",
            iv.pk,
        )

    # Reload saved data before syncing.
    iv.refresh_from_db()

    # Already synced successfully: do not send again.
    if iv.synced_at:
        log.info(
            "Interview %s already synced to Naukriyan.",
            iv.pk,
        )
        return

    try:
        # push_interview should use the latest saved Interview data.
        naukriyan.push_interview(iv)

    except Exception:
        log.exception(
            "Naukriyan sync failed for interview %s. "
            "Report is saved; sync can be retried.",
            iv.pk,
        )
        return

    # Mark synced only after the API call succeeds.
    iv.synced_at = timezone.now()
    iv.save(update_fields=["synced_at"])

    log.info(
        "Interview %s synced successfully to Naukriyan.",
        iv.pk,
    )    


