from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.response import Response
from rest_framework import status
from django.conf import settings 
from django.contrib.auth.models import Group, User
from rest_framework.views import APIView
from django.utils import timezone
from django.db import transaction
from django.core.files.storage import FileSystemStorage
from admin_users.models import ImplementerDetails
from .models import TblClient, TblDepartment, TblVendor, TblJobDescription, TblImpDepartment, TblClientDocument, TblVendorDocument, AssignJd, EmailOTPUser, LinkedInJobPost, ImplementerPortalConfig, TblCandidateProfile, TblCandidateResume, career, JobApplication, TblMatchedProfiles, CareerJobApplication, L0InterviewList, L0InterviewSessions, PublicLink, TblMatchedProfilesCandidate, JobEmailShare
from .serializers import ClientSerializer, TblDepartmentSerializer, VendorSerializer, TblJobDescriptionSerializer, TblImpDepartmentSerializer, ClientDocumentSerializer, VendorDocumentSerializer, ImplementerPortalConfigSerializer, TblCandidateProfileSerializer, TblCandidateResumeSerializer, careerSerializer, ResumeBasicSerializer, L0InterviewListSerializer, L0InterviewSessionsSerializer, TblMatchedProfilesSerializer, TblMatchedProfilesCandidateSerializer, AuditLogSerializer
import imaplib
import email
from email.header import decode_header
from email.utils import parseaddr, parsedate_to_datetime
from datetime import datetime, timedelta
import os
import json
from html import unescape
import re
from ai_settings.ai_gen_gcs_ext import AIEditLLMGCS as AIEditLLM
import traceback, os, shutil, base64
import pyotp
from masters.utils import generate_password, generate_apply_link, delete_linkedin_post, LlmPrompts, normalize_resume_title, highlight_inline_diff, check_ai_title_similarity_and_notify, send_resume_ack_email, send_shortlist_email, DocumentTextExtraction, send_email_safe, notify_recruiter_multiple_jds, process_shortlist_reply, send_interview_calendar_invite, DBAuthFilters, get_email_template
from django.core.mail import EmailMessage
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, Q, F, Value
from .linkedin_service import post_to_linkedin
from rest_framework.viewsets import ViewSet
from django.shortcuts import redirect, get_object_or_404, render
import requests
from urllib.parse import urlencode
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from rest_framework.decorators import action
import re
import difflib
from rest_framework.exceptions import ValidationError
from django.db import models
from django.utils.dateparse import parse_datetime
from datetime import timezone as dt_timezone
from rest_framework.decorators import api_view, permission_classes
from pgvector.django import CosineDistance
import unicodedata
import math
from django.contrib.postgres.search import TrigramSimilarity
from rest_framework.test import APIRequestFactory
from django.utils.timezone import now
import subprocess
# import whisper
import tempfile
import speech_recognition as sr
# import pyttsx3
from gtts import gTTS
from django.core.files import File
from django.core import signing
from django.http import FileResponse, HttpResponseForbidden, HttpResponse
from django.urls import reverse
import secrets
import string
from django.core.mail import send_mail
from django.contrib.contenttypes.models import ContentType
from auditlog.models import LogEntry
from .scrapper import run_scraper

# whisper_model = whisper.load_model("base")

class ClientViewSet(ModelViewSet):
    queryset = TblClient.objects.all().order_by("-created_date")
    serializer_class = ClientSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = (MultiPartParser, FormParser)

    def get_queryset(self):
        user = self.request.user
        query_set = super().get_queryset() 
        client_ids = DBAuthFilters.get_client_by_auth_user(user)
        if client_ids :
            return query_set.filter(client_id__in=client_ids)
        else:
            return []

    def create(self, request, *args, **kwargs):
        data = request.data.copy()

        if not data.get("client_type"):
            data["client_type"] = "End"
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            client = serializer.save(
                created_by=request.user.username
            )

            documents = request.FILES.getlist("documents")
            for doc in documents:
                TblClientDocument.objects.create(
                    client=client,
                    document=doc
                )

            return Response(
                {
                    "message": "Client created successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_201_CREATED
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )

    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(
            instance,
            data=request.data,
            partial=True
        )

        if serializer.is_valid():
            client = serializer.save(
                updated_by=request.user.username
            )

            documents = request.FILES.getlist("documents")
            for doc in documents:
                TblClientDocument.objects.create(
                    client=client,
                    document=doc
                )

            return Response(
                {
                    "message": "Client updated successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_200_OK
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )
class ClientDocumentViewSet(ReadOnlyModelViewSet):
    permission_classes = [IsAuthenticated]
    serializer_class = ClientDocumentSerializer
    queryset = TblClientDocument.objects.all()

    def get_queryset(self):
        queryset = super().get_queryset()
        client_id = self.request.query_params.get("client")

        if client_id:
            queryset = queryset.filter(client_id=int(client_id))

        return queryset
class DepartmentViewSet(ModelViewSet):
    queryset = TblDepartment.objects.all().order_by("-created_date")
    serializer_class = TblDepartmentSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = (JSONParser, MultiPartParser, FormParser)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            serializer.save(
                created_by=request.user.username
            )
            return Response(
                {
                    "message": "Department created successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_201_CREATED
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )

    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(
            instance,
            data=request.data,
            partial=True  
        )

        if serializer.is_valid():
            serializer.save(
                updated_by=request.user.username
            )
            return Response(
                {
                    "message": "Department updated successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_200_OK
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )
    
class VendorsViewSet(ModelViewSet):
    queryset = TblVendor.objects.all().order_by("-created_date")
    serializer_class = VendorSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = (MultiPartParser, FormParser)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            vendor = serializer.save(
                updated_by=request.user.username
            )

            documents = request.FILES.getlist("documents")
            for doc in documents:
                TblVendorDocument.objects.create(
                    vendor=vendor,
                    document=doc
                )
            return Response(
                {
                    "message": "Vendor created successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_201_CREATED
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )

    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(
            instance,
            data=request.data,
            partial=True  
        )

        if serializer.is_valid():
            vendor = serializer.save(
                updated_by=request.user.username
            )

            documents = request.FILES.getlist("documents")
            for doc in documents:
                TblVendorDocument.objects.create(
                    vendor=vendor,
                    document=doc
                )
            return Response(
                {
                    "message": "Vendor updated successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_200_OK
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )
    
class VendorDocumentViewSet(ReadOnlyModelViewSet):
    permission_classes = [IsAuthenticated]
    serializer_class = VendorDocumentSerializer
    queryset = TblVendorDocument.objects.all()

    def get_queryset(self):
        queryset = super().get_queryset()
        vendor_id = self.request.query_params.get("vendor")

        if vendor_id:
            queryset = queryset.filter(vendor_id=int(vendor_id))

        return queryset
class CountryCodeListView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response(settings.COUNTRY_CODES)
class TblJobDescriptionView(ModelViewSet):
    queryset = (
        TblJobDescription.objects
        .annotate(linkedin_posts_count=Count("linkedin_posts"))
        .order_by("-jd_id")
    )
    serializer_class = TblJobDescriptionSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = (JSONParser, MultiPartParser, FormParser)

    def get_queryset(self):
        qs = super().get_queryset().filter(client_id__status="Active")
        params = self.request.query_params
        user = self.request.user

        jd_ids = DBAuthFilters.get_jd_by_auth_user(user)
        if jd_ids:
            qs = qs.filter(jd_id__in=jd_ids)
        else:
            qs = []

        if params.get("job_type"):
            qs = qs.filter(job_type=params["job_type"])

        if params.get("status"):
            qs = qs.filter(status=params["status"])

        if params.get("client_id"):
            qs = qs.filter(client_id=params["client_id"])

        if params.get("department_id"):
            qs = qs.filter(department_id=params["department_id"])

        if params.get("min_exp"):
            qs = qs.filter(years_of_experience__gte=params["min_exp"])

        if params.get("max_exp"):
            qs = qs.filter(years_of_experience__lte=params["max_exp"])

        if params.get("location"):
            qs = qs.filter(job_location__icontains=params["location"])

        if params.get("start_date") and params.get("end_date"):
            qs = qs.filter(
                jd_date__range=[params["start_date"], params["end_date"]]
            )

        return qs

    def validate_jd_data(jd_data, jd_date, client_data):
        errors = []
        # -------- Required fields --------
        if not jd_data.get("job_title"):
            errors.append("Missing Job Title")

        if not jd_data.get("job_summary"):
            errors.append("Missing Job Summary")

        if not jd_data.get("responsibilities"):
            errors.append("Missing Responsibilities")

        if not jd_data.get("years_of_experience"):
            errors.append("Missing Years of Experience")

        # -------- Date validation --------
        if not jd_date:
            errors.append("Invalid or missing Job Description Date")

        # -------- Client validation --------
        if not client_data:
            errors.append("Client Data not found for sender domain")

        return errors
    
    def format_code(text: str, length: int) -> str:
        if not text:
            return "X" * length

        # Clean input
        clean = re.sub(r"[^A-Za-z ]", "", text).strip().upper()
        words = clean.split()

        # Step 1: take first letter of each word
        result = "".join(word[0] for word in words)

        # Step 2: if short, take remaining letters from LAST word
        if len(result) < length and words:
            last_word = words[-1]
            needed = length - len(result)

            # Skip first letter of last word (already used)
            extra = last_word[1:1 + needed]
            result += extra

        # Step 3: pad with X if still short
        return (result + ("X" * length))[:length]
    
    def normalize_responsibilities_text(self, text):
        if not text:
            return ""

        text = unicodedata.normalize("NFKC", text)

        text = text.replace("\r\n", "\n").replace("\r", "\n")

        text = re.sub(r"[ \t]+", " ", text)

        text = "\n".join(line.strip() for line in text.split("\n"))

        return text.strip()
    
    def extract_json_from_llm(self, text: str) -> dict:
        # Remove ```json and ```
        cleaned = re.sub(r"```json|```", "", text, flags=re.IGNORECASE).strip()

        return json.loads(cleaned)
    
    def normalize_db_text(self, value):
        if not value:
            return ""
        value = value.strip().lower()
        value = value.replace(",", "").replace(".", "")
        value = re.sub(r"\s+", " ", value)
        return value

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            jd_data = serializer.validated_data
            jd_date = timezone.now().date()

            client_data = jd_data.get("client_id")
            department_data = jd_data.get("department_id")

            validation_errors = TblJobDescriptionView.validate_jd_data(
                jd_data, jd_date, client_data
            )
            has_error = bool(validation_errors)
            error_desc = "; ".join(validation_errors) if validation_errors else None
            status_value = "Error" if has_error else "Open"
            implemntor_data = (
                ImplementerDetails.objects.first()
            )

            with transaction.atomic():
                jd_display_id = None
                date_str = timezone.now().strftime("%y%m%d")
                if client_data:
                    client_data = TblClient.objects.select_for_update().get(pk=client_data.pk)
                    client_code = str(client_data.client_id).zfill(3)
                    job_code = TblJobDescriptionView.format_code(jd_data.get("job_title"), 4)
                    current_serial = int(client_data.client_jd_serial_number or 0)
                    serial_str = str(current_serial).zfill(3)

                    jd_display_id = f"{client_code}_{job_code}_{date_str}_{serial_str}"

                    client_data.client_jd_serial_number = current_serial + 1
                    client_data.save(update_fields=["client_jd_serial_number"])
                else:
                    implemntor_data = (
                        ImplementerDetails.objects.get(pk=implemntor_data.pk)
                    )

                    implementor_code = TblJobDescriptionView.format_code(implemntor_data.display_name, 3)
                    job_code = TblJobDescriptionView.format_code(jd_data.get("job_title"), 4)
                    
                    current_serial = int(implemntor_data.implementor_jd_serial_number or 0)
                    serial_str = str(current_serial).zfill(3)

                    jd_display_id = f"{implementor_code}_{job_code}_{date_str}_{serial_str}"

                    implemntor_data.implementor_jd_serial_number = current_serial + 1
                    implemntor_data.save(update_fields=["implementor_jd_serial_number"])

            airecruit_llm = AIEditLLM()
            responsibilities_embedding = airecruit_llm.generate_embeddings(jd_data.get("responsibilities"))

            jd_instance = serializer.save(
                client_id=client_data,
                jd_display_id=jd_display_id,
                department_id=department_data,
                implementor_id=implemntor_data,
                jd_date=jd_date,
                status=status_value,
                jd_stage="Created",
                jd_responsibilities_embedding=responsibilities_embedding if responsibilities_embedding is not None else None,
                has_error=has_error,
                error_desc=error_desc,
                created_by=request.user.username
            )

            llm_sending_data = {
                "jd_id": jd_instance.jd_id,
                "jd_display_id": jd_instance.jd_display_id,
                "job_title": jd_instance.job_title,
                "job_type": jd_instance.job_type,
                "years_of_experience": jd_instance.years_of_experience,
                "about_company": jd_instance.about_company,
                "job_summary": jd_instance.job_summary,
                "responsibilities": jd_instance.responsibilities,
                "domain_requirements": jd_instance.domain_requirements,
                "certification_requirements": jd_instance.certification_requirements,
                "security_clearance_requirements": jd_instance.security_clearance_requirements,
                "onsite_job": jd_instance.onsite_job,
                "job_location": jd_instance.job_location,
                "required_qualifications": jd_instance.required_qualifications,
                "preferred_qualifications": jd_instance.preferred_qualifications
            }

            jd_search_pattern_prompt = LlmPrompts.jd_search_pattern_prompt([llm_sending_data])
            generation_config = {
                "temperature": 0.2,
                "thinking_budget": 32768,
            }
            airecruit_llm.create_chat(
                generation_config["temperature"], generation_config["thinking_budget"]
            )
            jd_llm_response = airecruit_llm.send_message(jd_search_pattern_prompt)
            llm_jd_data = self.extract_json_from_llm(jd_llm_response.text)

            print(jd_llm_response.text)

            if llm_jd_data:
                jd_instance.search_pattern = llm_jd_data[0].get("search_pattern")
                jd_instance.save(update_fields=["search_pattern"])

            return Response(
                {
                    "message": "Job Description created successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_201_CREATED
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )

    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(
            instance,
            data=request.data,
            partial=True  
        )

        if serializer.is_valid():
            jd_data = serializer.validated_data
            jd_date = jd_data.get("jd_date", instance.jd_date)
            client_data = jd_data["client_id"]

            validation_input = {
                "job_title": jd_data.get("job_title", instance.job_title),
                "job_summary": jd_data.get("job_summary", instance.job_summary),
                "responsibilities": jd_data.get("responsibilities", instance.responsibilities),
                "years_of_experience": jd_data.get("years_of_experience", instance.years_of_experience),
            }

            validation_errors = TblJobDescriptionView.validate_jd_data(
                validation_input, jd_date, client_data
            )

            responsibilities_embedding = instance.jd_responsibilities_embedding

            new_resp = self.normalize_responsibilities_text(jd_data.get("responsibilities"))
            old_resp = self.normalize_responsibilities_text(instance.responsibilities)

            airecruit_llm = AIEditLLM()

            if new_resp != old_resp:
                responsibilities_embedding = airecruit_llm.generate_embeddings(
                    jd_data.get("responsibilities")
                )

            has_error = bool(validation_errors)
            error_desc = "; ".join(validation_errors) if validation_errors else None
            current_status = instance.status

            duplicate_jds = []
            if client_data:
                existing_jds = TblJobDescription.objects.filter(
                    client_id=client_data,
                    status__in=["Open", "Duplicate"]
                ).exclude(jd_id=instance.jd_id)
                
                incoming_job_title = self.normalize_db_text(jd_data.get("job_title"))
                incoming_location = self.normalize_db_text(jd_data.get("job_location"))
                incoming_job_type = self.normalize_db_text(jd_data.get("job_type"))
                incoming_exp = self.normalize_db_text(jd_data.get("years_of_experience"))

                for jd in existing_jds:
                    db_job_title = self.normalize_db_text(jd.job_title)
                    db_location = self.normalize_db_text(jd.job_location)
                    db_job_type = self.normalize_db_text(jd.job_type)
                    db_exp = self.normalize_db_text(jd.years_of_experience)

                    if (
                        db_job_title == incoming_job_title
                        and db_location == incoming_location
                        and db_job_type == incoming_job_type
                        and db_exp == incoming_exp
                    ):
                        duplicate_jds.append(jd)
                    else:
                        currect_duplicate_ids = jd.duplicated_jd_id or []
                        if instance.jd_display_id in currect_duplicate_ids:
                            currect_duplicate_ids.remove(instance.jd_display_id)
                            jd.duplicated_jd_id = currect_duplicate_ids
                            if not currect_duplicate_ids:
                                jd.status = "Open"
                            else:
                                jd.status = jd.status
                            jd.save(update_fields=["duplicated_jd_id", "status"])


            if duplicate_jds:
                for jd in duplicate_jds:
                    if jd.status == "Duplicate":
                        currect_duplicate_ids = jd.duplicated_jd_id or []
                        if instance.jd_display_id not in currect_duplicate_ids:
                            currect_duplicate_ids.append(instance.jd_display_id)
                            jd.duplicated_jd_id = currect_duplicate_ids
                            jd.save(update_fields=["duplicated_jd_id"])

            incoming_status = jd_data.get("status", instance.status)

            if has_error:
                status_value = "Error"
            else:
                status_value = incoming_status

            jd_display_id = instance.jd_display_id

            if client_data != instance.client_id:
                date_str = timezone.now().strftime("%y%m%d")
                client_code = str(client_data.client_id).zfill(3)
                job_code = TblJobDescriptionView.format_code(jd_data.get("job_title"), 4)
                current_serial = int(client_data.client_jd_serial_number or 0)
                serial_str = str(current_serial).zfill(3)

                jd_display_id = f"{client_code}_{job_code}_{date_str}_{serial_str}"

                client_data.client_jd_serial_number = current_serial + 1
                client_data.save(update_fields=["client_jd_serial_number"])

            jd_instance = serializer.save(
                updated_by=request.user.username,
                has_error=has_error,
                error_desc=error_desc,
                status=status_value,
                jd_display_id=jd_display_id,
                jd_responsibilities_embedding=responsibilities_embedding if responsibilities_embedding is not None else None
,
            )

            if request.data.get("search_pattern_required"):
                llm_sending_data = {
                    "jd_id": jd_instance.jd_id,
                    "jd_display_id": jd_instance.jd_display_id,
                    "job_title": jd_instance.job_title,
                    "job_type": jd_instance.job_type,
                    "years_of_experience": jd_instance.years_of_experience,
                    "about_company": jd_instance.about_company,
                    "job_summary": jd_instance.job_summary,
                    "responsibilities": jd_instance.responsibilities,
                    "domain_requirements": jd_instance.domain_requirements,
                    "certification_requirements": jd_instance.certification_requirements,
                    "security_clearance_requirements": jd_instance.security_clearance_requirements,
                    "onsite_job": jd_instance.onsite_job,
                    "job_location": jd_instance.job_location,
                    "required_qualifications": jd_instance.required_qualifications,
                    "preferred_qualifications": jd_instance.preferred_qualifications
                }

                jd_search_pattern_prompt = LlmPrompts.jd_search_pattern_prompt([llm_sending_data])
                generation_config = {
                    "temperature": 0.2,
                    "thinking_budget": 32768,
                }
                airecruit_llm.create_chat(
                    generation_config["temperature"], generation_config["thinking_budget"]
                )
                jd_llm_response = airecruit_llm.send_message(jd_search_pattern_prompt)
                llm_jd_data = self.extract_json_from_llm(jd_llm_response.text)

                print(jd_llm_response.text)

                if llm_jd_data:
                    jd_instance.search_pattern = llm_jd_data[0].get("search_pattern")
                    jd_instance.save(update_fields=["search_pattern"])

            return Response(
                {
                    "message": "Job Description updated successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_200_OK
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )
    
    def destroy(self, request, pk=None):
        try:
            jd_obj = TblJobDescription.objects.get(pk=pk)
            deleted_display_id = jd_obj.jd_display_id

            existing_jds = TblJobDescription.objects.filter(
                client_id=jd_obj.client_id,
                duplicated_jd_id__contains=[deleted_display_id]
            )

            for jd in existing_jds:
                duplicated_ids = jd.duplicated_jd_id or []

                if deleted_display_id in duplicated_ids:
                    duplicated_ids = [
                        x for x in duplicated_ids if x != deleted_display_id
                    ]

                    jd.duplicated_jd_id = duplicated_ids
                    jd.save(update_fields=["duplicated_jd_id"])

            jd_obj.delete()

            
            return Response({"status": "success", "message": "Job Description deleted successfully"}, status=200)
        except Exception as e:
            return Response({"status": "failed", "error": str(e)}, status=500)
    
    def post_jd_via_existing_linkedin_flow(jd, user, force_repost=False):
        implementer = jd.implementor_id

        config = ImplementerPortalConfig.objects.filter(
            implementer=implementer,
            portal="linkedin",
            is_connected=True,
            is_active=True
        ).first()

        if not config:
            return {"skipped": True}

        access_token = config.access_token
        linkedin_urn = config.portal_user_urn

        last_post = LinkedInJobPost.objects.filter(
            jd=jd, status="Posted"
        ).order_by("-posted_at").first()

        changed_fields = {}

        IMPORTANT_JD_FIELDS = [
            "job_title",
            "job_location",
            "years_of_experience",
            "job_summary",
            "salary_range",
            "responsibilities"
        ]

        def normalize(val):
            if val is None:
                return ""
            return str(val).strip()

        if last_post:
            previous_payload = last_post.posted_payload or {}

            if not previous_payload and not force_repost:
                force_repost = True

            current_payload = {
                "job_title": jd.job_title,
                "job_location": jd.job_location,
                "years_of_experience": jd.years_of_experience,
                "job_summary": jd.job_summary,
                "salary_range": jd.salary_range,
                "responsibilities": jd.responsibilities
            }

            for key in IMPORTANT_JD_FIELDS:
                old = normalize(previous_payload.get(key))
                new = normalize(current_payload.get(key))

                if old != new:
                    changed_fields[key] = {"old": old, "new": new}

            if not changed_fields and not force_repost:
                return {"skipped": True}

        if last_post and (changed_fields or force_repost):
            if last_post.linkedin_post_urn:
                delete_linkedin_post(
                    access_token=access_token,
                    post_urn=last_post.linkedin_post_urn
                )

            last_post.status = "Deleted"
            last_post.save(update_fields=["status"])

        if jd.jd_stage == "Created" and not last_post:
            force_repost = True

        lines = ["🚀 Hiring Now!"]
        lines.append(f"📌 Role: {jd.job_title}")
        lines.append(f"📍 Location: {jd.job_location}")
        lines.append(f"🧠 Experience: {jd.years_of_experience}")

        if jd.job_summary:
            lines.append("📝 Job Summary:")
            lines.append(jd.job_summary)

        if jd.responsibilities:
            lines.append("🎯 Responsibilities:")
            for r in jd.responsibilities.splitlines():
                r = r.strip("-• ").strip()
                if r:
                    lines.append(f"• {r}")

        if jd.salary_range:
            lines.append(f"💰 Salary: {jd.salary_range}")

        apply_url = generate_apply_link(jd)
        lines.append(f"👉 Apply here: {apply_url}")
        lines.append(f"🆔 JD Ref: {jd.jd_display_id}")
        lines.append(f"⏰ {timezone.now().strftime('%d-%m-%Y %H:%M')}")

        text = "\n\n".join(lines)

        res = post_to_linkedin(access_token, linkedin_urn, text)

        if res.status_code in (200, 201):
            jd.jd_stage = "Posted"
            jd.status = "Open"
            jd.save(update_fields=["jd_stage", "status"])

            LinkedInJobPost.objects.create(
                jd=jd,
                posted_by=user,
                linkedin_post_urn=res.json().get("id"),
                apply_url=apply_url,
                status="Posted",
                posted_payload={
                    **current_payload,
                    "reposted": bool(last_post),
                }
            )

            return {"skipped": False, "posted": True}

        LinkedInJobPost.objects.create(
            jd=jd,
            posted_by=user,
            linkedin_post_urn=None,
            apply_url=apply_url,
            status="Failed",
            posted_payload={"bulk": True}
        )

        return {"skipped": True, "posted": False}
    
    @action(detail=False, methods=["post"], url_path="post-by-time-range")
    def post_by_time_range(self, request):
        from_dt_raw = request.data.get("from_datetime")
        to_dt_raw = request.data.get("to_datetime")

        if not from_dt_raw or not to_dt_raw:
            return Response(
                {"detail": "from_datetime and to_datetime are required"},
                status=status.HTTP_400_BAD_REQUEST
            )

        from_dt = parse_datetime(from_dt_raw)
        to_dt = parse_datetime(to_dt_raw)

        if not from_dt or not to_dt:
            return Response(
                {"detail": "Invalid datetime format"},
                status=status.HTTP_400_BAD_REQUEST
            )

        local_tz = timezone.get_current_timezone()

        if timezone.is_naive(from_dt):
            from_dt = timezone.make_aware(from_dt, local_tz)

        if timezone.is_naive(to_dt):
            to_dt = timezone.make_aware(to_dt, local_tz)

        from_dt_utc = from_dt.astimezone(dt_timezone.utc)
        to_dt_utc = to_dt.astimezone(dt_timezone.utc)

        print("FROM LOCAL:", from_dt)
        print("TO LOCAL:", to_dt)
        print("FROM UTC:", from_dt_utc)
        print("TO UTC:", to_dt_utc)

        jds = TblJobDescription.objects.filter(
            status="Open",
            jd_stage__in=["Created", "Assigned", "Posted"]
        ).filter(
            Q(created_dt__range=(from_dt_utc, to_dt_utc)) |
            Q(updated_dt__range=(from_dt_utc, to_dt_utc))
        ).distinct()

        posted = 0
        skipped = 0

        for jd in jds:
            result = TblJobDescriptionView.post_jd_via_existing_linkedin_flow(
                jd=jd,
                user=request.user,
                force_repost=False
            )

            if result.get("posted"):
                posted += 1
            else:
                skipped += 1

        return Response({
            "from_datetime": from_dt,
            "to_datetime": to_dt,
            "total_jds": jds.count(),
            "posted": posted,
            "skipped": skipped
        })
    
    def linkedin_search_candidates(self, jd_obj, limit=25):
        profiles = []
        for i in range(limit):
            profiles.append({
                "linkedin_id": f"ln_{jd_obj.jd_display_id}_{i+1}",
                "first_name": "LinkedIn",
                "last_name": f"Candidate{i+1}",
                "email": "", 
                "title": jd_obj.job_title or "Candidate",
                "location": jd_obj.job_location or "India",
                "about": jd_obj.job_summary or "",
                "skills_text": jd_obj.search_pattern or "",
                "experience_text": jd_obj.responsibilities or "",
            })
        return profiles
    
    @action(detail=False, methods=["post"], url_path="fetch-linkedin-candidates")
    def fetch_linkedin_candidates(self, request):
        jd_id = request.data.get("jd_id")
        jd_display_id = request.data.get("jd_display_id")
        limit = int(request.data.get("limit", 25))

        if not jd_id and not jd_display_id:
            return Response(
                {"detail": "jd_id or jd_display_id is required"},
                status=400
            )

        if jd_id:
            jd_obj = get_object_or_404(TblJobDescription, jd_id=jd_id)
        else:
            jd_obj = get_object_or_404(TblJobDescription, jd_display_id=jd_display_id)

        portal = ImplementerPortalConfig.objects.filter(
            implementer=jd_obj.implementor_id,
            portal="linkedin",
            is_connected=True,
            is_active=True
        ).first()

        if not portal:
            return Response(
                {"detail": "LinkedIn portal not connected. Please connect in Portal Config."},
                status=400
            )
        
        profiles = self.linkedin_search_candidates(jd_obj, limit=limit)

        if not profiles:
            return Response(
                {"detail": "LinkedIn did not return any candidate profiles. Please check LinkedIn integration."},
                status=200
            )

        saved = 0
        duplicates = 0

        for p in profiles:
            first_name = (p.get("first_name") or "").strip()
            last_name = (p.get("last_name") or "").strip()

            email_value = (p.get("email") or None)

            cand, created = TblCandidateProfile.objects.get_or_create(
                email=email_value,
                defaults={
                    "first_name": first_name,
                    "last_name": last_name,
                    "mobile_number": p.get("mobile_number") or None,
                    "coutry_code": p.get("country_code") or None,
                    "source": "linkedin",
                    "candidate_status": "Active",
                    "created_by": request.user.username
                }
            )

            resume_text = " ".join([
                p.get("title", ""),
                p.get("location", ""),
                p.get("about", ""),
                p.get("skills_text", ""),
                p.get("experience_text", ""),
            ]).strip()

            already = TblCandidateResume.objects.filter(
                candidate_id=cand,
                parsed_text=resume_text
            ).exists()

            if already:
                duplicates += 1
                continue

            resume_obj = TblCandidateResume.objects.create(
                candidate_id=cand,
                email=email_value,
                title=p.get("title", ""),
                location=p.get("location", ""),
                primary_skills=p.get("skills_text", ""),
                parsed_text=resume_text,
                created_by=request.user.username
            )

            saved += 1

            try:
                match_response = run_matching_engine_candidate(resume_obj.resume_id)
                matches = match_response.get("matches", {})
            except Exception as e:
                matches = {}
            
            if matches:
                manager_sending_emails = list(
                    User.objects
                    .filter(groups__name="Delivery Manager (DM)", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                super_admin_cc_emails = list(
                    User.objects
                    .filter(groups__name="Implementer Super Admin", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                subject, body = get_email_template("candidate_jd_match_notification", is_html=False)

                candidate_name = f"{cand.first_name} {cand.last_name}"

                subject = subject.format(candidate_name=candidate_name)

                body = body.format(
                    resume_id=resume_obj.resume_id,
                    candidate_name=candidate_name,
                    upload_date=resume_obj.created_dt if hasattr(resume_obj, "created_dt") else "N/A",
                    match_count=len(matches)
                )

                for to_mail in manager_sending_emails:
                    send_email_safe(
                        subject,
                        body,
                        to_mail,
                        settings.EMAIL_HOST_USER,
                        cc_emails=super_admin_cc_emails,
                        is_html=False
                    )

        return Response({
            "jd_id": jd_obj.jd_id,
            "jd_display_id": jd_obj.jd_display_id,
            "jd_title": jd_obj.job_title,
            "portal": "linkedin",
            "saved": saved,
            "duplicates": duplicates,
            "total_fetched": len(profiles)
        })

class MailReaderGoogle(APIView):
    permission_classes = (IsAuthenticated,)
    def decode_mime_words(self, value):
        if not value:
            return ""
        decoded = decode_header(value)
        result = ""
        for text, encoding in decoded:
            if isinstance(text, bytes):
                result += text.decode(encoding or "utf-8", errors="ignore")
            else:
                result += text
        return result

    def html_to_text(self, html):
        # Remove script & style
        html = re.sub(r'<(script|style).*?>.*?</\1>', '', html, flags=re.S)

        # Remove all HTML tags
        text = re.sub(r'<[^>]+>', '', html)

        # Convert HTML entities
        text = unescape(text)

        # Normalize whitespace
        text = re.sub(r'\s+', ' ', text).strip()

        return text
    
    def extract_json_from_llm(self, text: str) -> dict:
        # Remove ```json and ```
        cleaned = re.sub(r"```json|```", "", text, flags=re.IGNORECASE).strip()

        return json.loads(cleaned)
    
    def validate_jd_data(self, jd_data, jd_date, client_data):
        errors = []

        # -------- Required fields --------
        if not jd_data.get("job_title"):
            errors.append("Missing Job Title")

        if not jd_data.get("job_summary"):
            errors.append("Missing Job Summary")

        if not jd_data.get("responsibilities"):
            errors.append("Missing Responsibilities")

        if not jd_data.get("years_of_experience"):
            errors.append("Missing Years of Experience")

        # -------- Date validation --------
        if not jd_date:
            errors.append("Invalid or missing Job Description Date")

        # -------- Client validation --------
        if not client_data:
            errors.append("Client Data not found for sender domain")

        return errors
    
    def send_notify_mail(self, subject, message, sending_mails, cc_mails=[]):
        try:
            email = EmailMessage(
                subject=subject,
                body=message,
                from_email=settings.EMAIL_HOST_USER,
                to=sending_mails,
                cc=cc_mails,
            )
            email.send()

            return True

        except Exception as e:
            traceback.print_exc()
            return False
        
    def normalize_text(self, txt: str) -> str:
        return re.sub(
            r"\s+",
            " ",
            txt.replace("_", " ").strip().lower()
        )
    
    def is_jd_mail(self, subject: str) -> bool:
        JD_KEYWORDS = [
            "job description",
            "jd",
            "job requirement",
            "job opening",
            "job opportunity",
            "hiring",
            "requirement",
            "position open",
            "vacancy",
            "career opportunity",
            "opening for",
            "looking for",
        ]
                        
        subject = self.normalize_text(subject)

        if not subject:
            return False

        for keyword in JD_KEYWORDS:
            if keyword in subject:
                return True

        return False
    
    def is_resume_attachment(self, filename: str) -> bool:
        if not filename:
            return False
        
        RESUME_EXTENSIONS = [".pdf", ".doc", ".docx"]
        filename = filename.lower()

        for ext in RESUME_EXTENSIONS:
            if filename.endswith(ext):
                return True

        return False
    
    def is_resume_mail(self, subject: str, attachments: list) -> bool:
        RESUME_KEYWORDS = [
            "resume",
            "cv",
            "curriculum vitae",
            "candidate profile",
            "profile for",
            "application for",
            "job application",
            "applying for",
            "my profile",
            "my cv",
            "my resume",
            "submission for",
            "profile attached",
            "resume attached",
            "candidate details",
            "candidate cv",
            "candidate resume",
        ]
        
        if not subject:
            return False

        subject = self.normalize_text(subject)

        subject_match = any(keyword in subject for keyword in RESUME_KEYWORDS)

        if not subject_match:
            return False

        if not attachments:
            return False

        for attachment in attachments:
            filename = attachment.get("filename") if isinstance(attachment, dict) else str(attachment)

            if self.is_resume_attachment(filename):
                return True

        return False
    
    def normalize_db_text(self, value):
        if not value:
            return ""
        value = value.strip().lower()
        value = value.replace(",", "").replace(".", "")
        value = re.sub(r"\s+", " ", value)
        return value

    def format_code(self, text: str, length: int) -> str:
        if not text:
            return "X" * length

        # Clean input
        clean = re.sub(r"[^A-Za-z ]", "", text).strip().upper()
        words = clean.split()

        # Step 1: take first letter of each word
        result = "".join(word[0] for word in words)

        # Step 2: if short, take remaining letters from LAST word
        if len(result) < length and words:
            last_word = words[-1]
            needed = length - len(result)

            # Skip first letter of last word (already used)
            extra = last_word[1:1 + needed]
            result += extra

        # Step 3: pad with X if still short
        return (result + ("X" * length))[:length]
    
    def normalize_header(self, header: str) -> str:
        return re.sub(
            r"\s+",
            " ",
            header.replace("_", " ").strip().lower()
        )
    
    def flatten_data_for_db(self, data):
        """
        Maps the Complex Nested JSON from the Prompt to the Flat DB Structure in TblCandidateResume
        """
        personal = data.get("personal_information", {})
        skills = data.get("skills", {})
        work_exp = data.get("work_experience", [])
        education = data.get("education", [])
        certifications = data.get("certifications", [])
        projects = data.get("projects", [])

        company_details = []
        if work_exp and len(work_exp) > 0:
            company_details = work_exp

        education_details = []
        if education and len(education) > 0:
            education_details = education

        certification_details = []
        if certifications and len(certifications) > 0:
            certification_details = certifications

        projects_details = []
        if projects and len(projects) > 0:
            projects_details = projects

        return {
            "first_name": personal.get("first_name", ""),
            "last_name": personal.get("last_name", ""),
            "email": personal.get("email", ""),
            "mobile": personal.get("mobile", ""),
            "country_code": "Not Specified",
            "location": personal.get("location", ""),
            "total_exp": data.get("total_exp", ""),
            "salary": data.get("salary", ""),
            "company": company_details,
            "title": data.get("title", ""),
            "primary_skills": data.get("primary_skills", []),
            'technical_skills': skills.get("technical_skills", []),
            'tools_and_frameworks': skills.get("tools_and_frameworks", []),
            "soft_skills": skills.get("soft_skills", []),
            "domain_skills": data.get("domain_skills", []),
            "education": education_details,
            "certifications": certification_details,
            "projects": projects_details,
            "responsibility_summary": data.get("responsibility_summary", ""),
            "age": personal.get("age", ""),
            "gender": personal.get("gender", ""),
            "notice_period": personal.get("notice_period", ""),
            "employment_type": personal.get("employment_type", "")
        }

    def get(self, request):
        try:
            airecruit_llm = AIEditLLM()
            generation_config = {
                "temperature": 0.2,  # Lower temperature for more deterministic, less random output
                "thinking_budget": 32768,
            }
            
            os.makedirs(settings.MEDIA_ROOT, exist_ok=True)
            jd_attachments_dir = os.path.join(settings.MEDIA_ROOT, "JD_files")
            os.makedirs(jd_attachments_dir, exist_ok=True)

            jd_mail_attachments = os.path.join(jd_attachments_dir, "mail_attachments")
            os.makedirs(jd_mail_attachments, exist_ok=True)

            resume_base_dir = os.path.join(settings.MEDIA_ROOT, "Resumes")
            os.makedirs(resume_base_dir, exist_ok=True)

            implemntor_data = (
                ImplementerDetails.objects.first()
            )

            for mail_detail in implemntor_data.email_details:
                if not mail_detail["email_app_password"]:
                    print(f"{mail_detail["email"]} doesn't have app password to read the mailbox")
                    continue
                
                imap_host = "imap.gmail.com" # "outlook.office365.com" # 
                # email_user = "selvasivachandran0002@gmail.com"
                # email_password = "ykra ntdd dwuh guqr"

                email_user = mail_detail["email"]
                email_password = mail_detail["email_app_password"]

                mail = imaplib.IMAP4_SSL(imap_host)
                mail.login(email_user, email_password)
                mail.select("INBOX")

                last_read_str = mail_detail.get("last_read")
                if last_read_str:
                    last_read_dt = datetime.fromisoformat(last_read_str)
                else:
                    last_read_dt = timezone.now() - timedelta(days=settings.MAIL_LAST_READ_DAYS_INITIAL)

                since_date_str = last_read_dt.strftime("%d-%b-%Y")

                # Search emails since date
                status, data = mail.search(None, f'(SINCE "{since_date_str}")')
                if status != "OK":
                    print("Failed to search emails")
                    return

                all_mails = data[0].split()
                email_ids = all_mails

                # status, data = mail.search(None, "ALL")
                # if status != "OK":
                #     print("Failed to search emails")
                #     raise Exception("Failed to Search emails")
                
                # all_mails = data[0].split()
                # email_ids = all_mails[-50:]

                latest_processed_dt = last_read_dt

                for eid in email_ids:
                    status, msg_data = mail.fetch(eid, "(RFC822)")
                    if status != "OK":
                        continue

                    raw_email = msg_data[0][1]
                    msg = email.message_from_bytes(raw_email)

                    message_id = msg.get("Message-ID")
                    subject = self.decode_mime_words(msg.get("Subject"))
                    sender = self.decode_mime_words(msg.get("From"))
                    receiver = self.decode_mime_words(msg.get("To"))
                    mail_date = msg.get("Date")

                    mail_date_time = parsedate_to_datetime(mail_date)
                    if mail_date_time and timezone.is_naive(mail_date_time):
                        mail_date_time = timezone.make_aware(mail_date_time)

                    if mail_date_time <= last_read_dt:
                        continue

                    if mail_date_time > latest_processed_dt:
                        latest_processed_dt = mail_date_time

                    print("\n-------------------------------")
                    print("Message-ID:", message_id)
                    print("Subject:", subject)
                    print("From:", sender)
                    print("To:", receiver)
                    print("Date:", mail_date)

                    attachments_meta = []
                    for part in msg.walk():
                        content_disposition = str(part.get("Content-Disposition") or "")
                        if "attachment" in content_disposition.lower():
                            filename = self.decode_mime_words(part.get_filename())
                            if filename:
                                attachments_meta.append(filename)

                    print("Subject:", subject)

                    sender_name, sender_email = parseaddr(sender)

                    body_text = None
                    body_html = None

                    for part in msg.walk():
                        content_type = part.get_content_type()
                        content_disposition = str(part.get("Content-Disposition"))

                        if content_type == "text/plain" and "attachment" not in content_disposition:
                            body_text = part.get_payload(decode=True).decode(errors="ignore")

                        elif content_type == "text/html" and "attachment" not in content_disposition:
                            body_html = part.get_payload(decode=True).decode(errors="ignore")

                    if not body_text and body_html:
                        body_text = self.html_to_text(body_html)

                    print("Body:", body_text)

                    tracking_match = None

                    if subject:
                        tracking_match = re.search(r"Ref:(\d+_\d+)", subject)

                    if not tracking_match and body_text:
                        tracking_match = re.search(r"Reference ID:\s*(\d+_\d+)", body_text)

                    if tracking_match:
                        tracking_token = tracking_match.group(1)
                        print("🎯 Shortlist reply detected:", tracking_token)

                        interested = False
                        interest_keywords = [
                            "yes",
                            "yes i am interested",
                            "yes, i am interested",
                            "interested",
                            "i am interested",
                            "i'm interested"
                        ]

                        if body_text:
                            body_lower = body_text.lower()

                            for keyword in interest_keywords:
                                if keyword in body_lower:
                                    interested = True
                                    break

                        process_shortlist_reply(
                            tracking_token,
                            body_text,
                            interested=interested
                        )

                        continue

                    is_jd_mail = self.is_jd_mail(subject)
                    is_resume_mail = self.is_resume_mail(subject, attachments_meta)

                    if not is_jd_mail and not is_resume_mail:
                        print("Skipping this mail not belongs to JD mail or Candidate Resume Mail")
                        continue

                    sender_name, sender_email = parseaddr(sender)

                    body_text = None
                    body_html = None
                    attachments = []
                    attachment_texts= []

                    for part in msg.walk():
                        content_type = part.get_content_type()
                        content_disposition = str(part.get("Content-Disposition"))

                        # Plain text body
                        if content_type == "text/plain" and "attachment" not in content_disposition:
                            body_text = part.get_payload(decode=True).decode(errors="ignore")

                        # HTML body
                        elif content_type == "text/html" and "attachment" not in content_disposition:
                            body_html = part.get_payload(decode=True).decode(errors="ignore")

                        # Attachments
                        elif "attachment" in content_disposition:
                            filename = self.decode_mime_words(part.get_filename())
                            if filename:
                                filepath = os.path.join(jd_mail_attachments, filename)
                                with open(filepath, "wb") as f:
                                    f.write(part.get_payload(decode=True))

                                attachments.append(filepath)

                                if filename.lower().endswith(".pdf"):
                                    print(f"\n--- Extracting text from PDF: {filename} ---")

                                    try:
                                        pdf_text = DocumentTextExtraction.process_pdf(filepath)
                                        if pdf_text:
                                            text_attachment = (f"\n--- Attachment Text ---\n" + pdf_text)
                                            attachment_texts.append(
                                                {
                                                    "file_type": "pdf",
                                                    "text": [text_attachment]
                                                }
                                            )
                                    except Exception as e:
                                        print(f"Failed to extract PDF text: {e}")
                                elif filename.lower().endswith((".docx", ".doc")):
                                    print(f"\n--- Extracting text from Word Document: {filename} ---")
                                    try:
                                        doc_text = DocumentTextExtraction.process_word(filepath)
                                        if doc_text:
                                            text_attachment = (f"\n--- Attachment Text ---\n" + doc_text)
                                            attachment_texts.append(
                                                {
                                                    "file_type": "docx",
                                                    "text": [text_attachment]
                                                }
                                            )

                                    except Exception as e:
                                        print(f"Word extraction failed for {filename}: {e}")     
                                elif filename.lower().endswith((".xlsx", ".xls", ".csv")):
                                    extension = os.path.splitext(filename)[1].lower()
                                    content = DocumentTextExtraction.process_excel_or_csv(filepath, extension)

                                    required_fields = ["job_title", "responsibilities"]
                                    normalized_rows = []
                                    for row in content:
                                        if not all(row.get(f) for f in required_fields):
                                            continue

                                        normalized_rows.append(row)

                                    attachment_texts.append(
                                        {
                                            "file_type": "xls",
                                            "text": normalized_rows
                                        }
                                    )

                    for attachment_text in attachment_texts:
                        if body_text:
                            final_body_text = body_text.strip()
                        elif body_html:
                            final_body_text = self.html_to_text(body_html)
                        else:
                            final_body_text = ""

                        if attachment_text["text"]:
                            if not attachment_text["file_type"] == "xls":
                                final_body_text += "\n\n" + "\n".join(attachment_text["text"])

                        # print("HTML Body:", body_html)
                        print("\nText Body:", final_body_text)
                        print("Attachments:", attachments)

                        airecruit_llm.create_chat(
                            generation_config["temperature"], generation_config["thinking_budget"]
                        )
                        if is_jd_mail:
                            jd_generate_prompt = LlmPrompts.jd_extraction_prompt(final_body_text)
                            try:
                                multiple_jd_data = []
                                if not attachment_text["file_type"] == "xls":
                                    jd_llm_response = airecruit_llm.send_message(jd_generate_prompt)

                                    print(jd_llm_response.text)
                                    jd_data = self.extract_json_from_llm(jd_llm_response.text)
                                    multiple_jd_data.append(jd_data)
                                else:
                                    for data in attachment_text["text"]:
                                        multiple_jd_data.append(data)

                                for jd_data in multiple_jd_data:
                                    if not jd_data.get("job_title") or jd_data.get("job_title") == "Not Specified":
                                        public_domain_subject = "Auto Recruiter: Job Description Received - Job Title Not Identified"
                                        job_title_missing_message = (
                                            "Hello Admin,\n\n"
                                            "We received a Job Description requirement email, but the Job Title was not specified in the content.\n\n"
                                            f"Received mail from: {sender_email}\n"
                                            f"Received on: {mail_date}\n\n"
                                            "Please review this and either identify the job title or manually add the JD through the application.\n\n"
                                            "Best regards,\n"
                                            "The Auto Recruiter Support Team"
                                        )

                                        manager_sending_emails = list(
                                            User.objects
                                            .filter(groups__name="Delivery Manager (DM)", is_active=True)
                                            .exclude(email__isnull=True)
                                            .exclude(email__exact="")
                                            .values_list("email", flat=True)
                                        )

                                        super_admin_cc_emails = list(
                                            User.objects
                                            .filter(groups__name="Implementer Super Admin", is_active=True)
                                            .exclude(email__isnull=True)
                                            .exclude(email__exact="")
                                            .values_list("email", flat=True)
                                        )
                                        self.send_notify_mail(public_domain_subject, job_title_missing_message, manager_sending_emails, super_admin_cc_emails)
                                        continue

                                    try:
                                        jd_date = parsedate_to_datetime(mail_date).date()
                                    except Exception:
                                        jd_date = None

                                    if not jd_date:
                                        jd_date = timezone.now().date()

                                    PUBLIC_EMAIL_DOMAINS = {
                                        "gmail.com",
                                        "yahoo.com",
                                        "yahoo.co.in",
                                        "outlook.com",
                                        "hotmail.com",
                                        "live.com",
                                        "icloud.com",
                                        "aol.com",
                                        "protonmail.com",
                                        "rediffmail.com",
                                    }

                                    client_data = TblClient.objects.filter(client_contact_email=sender_email).first()
                                    if not client_data:
                                        sender_domain = sender_email.split("@")[-1].lower()
                                        client_data = TblClient.objects.filter(
                                            client_contact_email__iendswith="@" + sender_domain
                                        ).first()

                                    duplicate_jds = []
                                    if client_data:
                                        existing_jds = TblJobDescription.objects.filter(
                                            client_id=client_data,
                                            status__in=["Open", "Duplicate"]
                                        )
                                        
                                        incoming_job_title = self.normalize_db_text(jd_data.get("job_title"))
                                        incoming_location = self.normalize_db_text(jd_data.get("job_location"))
                                        incoming_job_type = self.normalize_db_text(jd_data.get("job_type"))
                                        incoming_exp = self.normalize_db_text(jd_data.get("years_of_experience"))

                                        for jd in existing_jds:
                                            db_job_title = self.normalize_db_text(jd.job_title)
                                            db_location = self.normalize_db_text(jd.job_location)
                                            db_job_type = self.normalize_db_text(jd.job_type)
                                            db_exp = self.normalize_db_text(jd.years_of_experience)

                                            if (
                                                db_job_title == incoming_job_title
                                                and db_location == incoming_location
                                                and db_job_type == incoming_job_type
                                                and db_exp == incoming_exp
                                            ):
                                                duplicate_jds.append(jd)

                                        if duplicate_jds:
                                            print(f"Found {len(duplicate_jds)} duplicate JD(s)")
                                    else:
                                        sender_domain = sender_email.split("@")[-1].lower()
                                        if sender_domain in PUBLIC_EMAIL_DOMAINS:
                                            print("Sender Domain belongs to Public E-mail Domains")
                                            public_domain_subject = "Auto Recruiter: Job Description Received - Client Not Identified"

                                            public_domain_message = (
                                                "Hello Admin,\n\n"
                                                "We received a Job Description requirement from a public domain email, "
                                                "but could not determine the associated client.\n\n"
                                                f"Received mail from: {sender_email}\n"
                                                f"Received on: {mail_date}\n\n"
                                                "Please review this and either identify the client or manually add the JD through the application.\n\n"
                                                "Best regards,\n"
                                                "The Auto Recruiter Support Team"
                                            )

                                            manager_sending_emails = list(
                                                User.objects
                                                .filter(groups__name="Delivery Manager (DM)", is_active=True)
                                                .exclude(email__isnull=True)
                                                .exclude(email__exact="")
                                                .values_list("email", flat=True)
                                            )

                                            super_admin_cc_emails = list(
                                                User.objects
                                                .filter(groups__name="Implementer Super Admin", is_active=True)
                                                .exclude(email__isnull=True)
                                                .exclude(email__exact="")
                                                .values_list("email", flat=True)
                                            )
                                            self.send_notify_mail(public_domain_subject, public_domain_message, manager_sending_emails, super_admin_cc_emails)
                                            mail.logout()
                                            return Response(
                                                {
                                                    "status": "Got Public Mail Domain, Notified to Admin",
                                                },
                                                status=200
                                            )
                                            
                                        base_domain = sender_domain.split(".")[0].upper()

                                        client_data = TblClient.objects.create(
                                            client_name= base_domain,
                                            client_contact_email=sender_email,
                                            status="Active",
                                            client_jd_serial_number=1
                                        )

                                    validation_errors = self.validate_jd_data(jd_data, jd_date, client_data)

                                    has_error = bool(validation_errors)
                                    error_desc = "; ".join(validation_errors) if validation_errors else None

                                    if duplicate_jds:
                                        status_value = "Duplicate"
                                    elif has_error:
                                        status_value = "Error"
                                    else:
                                        status_value = "Open"

                                    jd_file_url = None
                                    if attachments:
                                        jd_file_name = os.path.basename(attachments[0])
                                        jd_relative_path = f"JD_files/mail_attachments/{jd_file_name}"
                                        jd_file_url = settings.MEDIA_URL + jd_relative_path

                                    responsibilities_embedding = airecruit_llm.generate_embeddings(jd_data.get("responsibilities"))

                                    jd_obj = TblJobDescription.objects.create(
                                        client_id=client_data,
                                        implementor_id=implemntor_data,
                                        jd_date=jd_date,
                                        job_title=jd_data.get("job_title"),
                                        ai_title=jd_data.get("ai_title"),
                                        job_type=jd_data.get("job_type"),
                                        client_job_id=jd_data.get("job_id"),
                                        years_of_experience=jd_data.get("years_of_experience"),
                                        duration=jd_data.get("duration"),
                                        about_company=jd_data.get("about_company"),
                                        job_summary=jd_data.get("job_summary"),
                                        responsibilities=jd_data.get("responsibilities"),
                                        domain_requirements=jd_data.get("domain_requirements"),
                                        certification_requirements=jd_data.get("certification_requirements"),
                                        security_clearance_requirements=jd_data.get("security_clearance_requirements"),
                                        job_location=jd_data.get("job_location"),
                                        required_qualifications=jd_data.get("required_qualifications"),
                                        preferred_qualifications=jd_data.get("preferred_qualifications"),
                                        onsite_job=jd_data.get("onsite_job"),
                                        working_hours=jd_data.get("working_hours"),
                                        benefits=jd_data.get("benefits"),
                                        requirement_priority=jd_data.get("requirement_priority"),
                                        salary_range=jd_data.get("salary_range"),
                                        search_pattern=jd_data.get("search_pattern"),
                                        no_of_open_positions=jd_data.get("no_of_open_positions"),
                                        status=status_value,
                                        has_error=has_error,
                                        error_desc=error_desc,
                                        jd_source = jd_file_url,
                                        jd_responsibilities_embedding = responsibilities_embedding if responsibilities_embedding is not None else None,
                                        changes = jd_data.get("changes")
                                    )

                                    check_ai_title_similarity_and_notify(jd_obj)

                                    generated_jd_file_name = f"{jd_obj.jd_display_id}_file.txt"
                                    generated_file_path = os.path.join(jd_mail_attachments, generated_jd_file_name)

                                    with open(generated_file_path, "w", encoding="utf-8") as file:
                                        file.write(final_body_text)

                                    generated_jd_relative_path = f"JD_files/mail_attachments/{generated_jd_file_name}"
                                    generated_jd_file_url = settings.MEDIA_URL + generated_jd_relative_path

                                    jd_obj.jd_source_txt = generated_jd_file_url
                                    jd_obj.save(update_fields=["jd_source_txt"])

                                    if duplicate_jds:
                                        jd_details_lines = []
                                        duplicate_jd_id = []
                                        for jd in duplicate_jds:
                                            jd_details_lines.append(
                                                f"- JD ID: {jd.jd_display_id}, Job Title: {jd.job_title}"
                                            )
                                            duplicate_jd_id.append(jd.jd_display_id)

                                        jd_obj.duplicated_jd_id = duplicate_jd_id
                                        jd_obj.save(update_fields=["duplicated_jd_id"])

                                        jd_details_str = "\n".join(jd_details_lines)

                                        duplicate_mail_subject = "Auto Recruiter: Duplicate Job Description Identified"
                                        duplicate_mail_message = (
                                            "Hello Admin,\n\n"
                                            "A new Job Description has been created, and one or more identical Job Description "
                                            "entries were identified in the system.\n\n"
                                            f"Newly Created Job Description ID: {jd_obj.jd_display_id}\n"
                                            f"Job Title: {jd_obj.job_title}\n\n"
                                            "Identical Job Description Details:\n"
                                            f"{jd_details_str}\n\n"
                                            "Please review these entries and take appropriate action.\n\n"
                                            "Best regards,\n"
                                            "The Auto Recruiter Support Team"
                                        )

                                        manager_sending_emails = list(
                                            User.objects
                                            .filter(groups__name="Delivery Manager (DM)", is_active=True)
                                            .exclude(email__isnull=True)
                                            .exclude(email__exact="")
                                            .values_list("email", flat=True)
                                        )

                                        super_admin_cc_emails = list(
                                            User.objects
                                            .filter(groups__name="Implementer Super Admin", is_active=True)
                                            .exclude(email__isnull=True)
                                            .exclude(email__exact="")
                                            .values_list("email", flat=True)
                                        )
                                        self.send_notify_mail(duplicate_mail_subject, duplicate_mail_message, manager_sending_emails, super_admin_cc_emails)
                            except Exception as e:
                                raise Exception("An Error Occurred during Gemini Response", e)
                        elif is_resume_mail:
                            resume_exract_prompt = LlmPrompts.resume_extraction_prompt(final_body_text)
                            try:
                                if not attachment_text["file_type"] == "xls":
                                    resume_llm_respons = airecruit_llm.send_message(resume_exract_prompt)

                                    print(resume_llm_respons.text)
                                    resume_data = self.extract_json_from_llm(resume_llm_respons.text)

                                    resume_db_data = self.flatten_data_for_db(resume_data)

                                    missing_fields = []
                                    def is_missing(value):
                                        return not value or str(value).strip() == "" or value == "Not Specified"
                                    
                                    if is_missing(resume_db_data.get("first_name")):
                                        missing_fields.append("First Name")

                                    if is_missing(resume_db_data.get("mobile")):
                                        missing_fields.append("Mobile Number")

                                    if is_missing(resume_db_data.get("email")):
                                        missing_fields.append("E-Mail")

                                    if is_missing(resume_db_data.get("title")):
                                        missing_fields.append("Job Title")
                                    
                                    if is_missing(resume_db_data.get("primary_skills")):
                                        missing_fields.append("Primary Skills")

                                    if missing_fields:
                                        missing_list_str = ", ".join(missing_fields)

                                        candidate_mandatory_subject = "Auto Recruiter: Missing Candidate Details in Resume Email"

                                        candidate_mandatory_message = (
                                            "Hello Admin,\n\n"
                                            "We received a candidate resume via email, but some mandatory details are missing. \n\n"
                                            f"Missing fields: {missing_list_str}\n\n"
                                            f"Received mail from: {sender_email}\n"
                                            f"Received on: {mail_date}\n\n"
                                            "Please review the email content or attachment and update the missing details "
                                            "manually in the application before proceeding.\n\n"
                                            "Best regards,\n"
                                            "The Auto Recruiter Support Team"
                                        )

                                        manager_sending_emails = list(
                                            User.objects
                                            .filter(groups__name="Delivery Manager (DM)", is_active=True)
                                            .exclude(email__isnull=True)
                                            .exclude(email__exact="")
                                            .values_list("email", flat=True)
                                        )

                                        super_admin_cc_emails = list(
                                            User.objects
                                            .filter(groups__name="Implementer Super Admin", is_active=True)
                                            .exclude(email__isnull=True)
                                            .exclude(email__exact="")
                                            .values_list("email", flat=True)
                                        )
                                        self.send_notify_mail(candidate_mandatory_subject, candidate_mandatory_message, manager_sending_emails, super_admin_cc_emails)
                                        continue
                                    
                                    resume_file_url = None
                                    if attachments:
                                        resume_file_name = os.path.basename(attachments[0])
                                        resume_relative_path = f"Resumes/{resume_file_name}"
                                        resume_file_url = settings.MEDIA_URL + resume_relative_path

                                    with transaction.atomic():
                                        email_id = resume_db_data["email"].strip().lower()
                                        profile = TblCandidateProfile.objects.filter(email__iexact=email_id).first()
                                        
                                        if not profile:
                                            profile = TblCandidateProfile.objects.create(
                                                first_name=resume_db_data["first_name"],
                                                last_name=resume_db_data["last_name"],
                                                email=resume_db_data["email"],
                                                mobile_number=resume_db_data["mobile"],
                                                coutry_code=resume_db_data["country_code"] if resume_db_data["country_code"] else "",
                                                age=resume_db_data["age"],
                                                gender=resume_db_data["gender"],
                                                notice_period=resume_db_data["notice_period"],
                                                employment_type=resume_db_data["employment_type"],
                                                candidate_status="Active",
                                                created_by=request.user.username
                                            )
                                        else:
                                            profile.first_name = resume_db_data["first_name"]
                                            profile.last_name = resume_db_data["last_name"]
                                            profile.mobile_number = resume_db_data["mobile"]
                                            profile.coutry_code = resume_db_data["country_code"] or ""
                                            profile.age = resume_db_data["age"]
                                            profile.gender = resume_db_data["gender"]
                                            profile.notice_period = resume_db_data["notice_period"]
                                            profile.employment_type = resume_db_data["employment_type"]
                                            profile.updated_by = request.user.username

                                            profile.save()

                                        attachment_content = " ".join(
                                            text.replace("\n--- Attachment Text ---\n", "").strip()
                                            for text in attachment_text["text"]
                                        )

                                        existing_resumes = TblCandidateResume.objects.filter(candidate_id=profile)

                                        matched_resume = None

                                        if resume_db_data["title"] and existing_resumes.exists():
                                            normalize_title_text = normalize_resume_title(resume_db_data["title"])

                                            for resume in existing_resumes:
                                                existing_title = normalize_resume_title(resume.title)
                                                if existing_title == normalize_title_text:
                                                    matched_resume = resume
                                                    break

                                        resume_responsibilities_embedding = airecruit_llm.generate_embeddings(resume_db_data.get("responsibility_summary"))

                                        if matched_resume:
                                            candidate_edit_subject = "Auto Recruiter: Existing Resume Updated from Email"

                                            candidate_edit_message = (
                                                "Hello Admin,\n\n"
                                                "A candidate resume received via email matches an existing resume record in the system based on the resume title. \n\n"
                                                "The system has updated the existing candidate data using the information from the received email and attachment. \n"
                                                "No new candidate record has been created. \n"
                                                f"Received mail from: {sender_email}\n"
                                                f"Received on: {mail_date}\n\n"

                                                "Please review the updated candidate information in the application if required. \n"

                                                "Best regards,\n"
                                                "The Auto Recruiter Support Team"
                                            )

                                            manager_sending_emails = list(
                                                User.objects
                                                .filter(groups__name="Delivery Manager (DM)", is_active=True)
                                                .exclude(email__isnull=True)
                                                .exclude(email__exact="")
                                                .values_list("email", flat=True)
                                            )

                                            super_admin_cc_emails = list(
                                                User.objects
                                                .filter(groups__name="Implementer Super Admin", is_active=True)
                                                .exclude(email__isnull=True)
                                                .exclude(email__exact="")
                                                .values_list("email", flat=True)
                                            )
                                            self.send_notify_mail(candidate_edit_subject, candidate_edit_message, manager_sending_emails, super_admin_cc_emails)

                                            matched_resume.totalexp = resume_db_data.get("total_exp")
                                            matched_resume.company = resume_db_data.get("company")
                                            matched_resume.salary = resume_db_data.get("salary")
                                            matched_resume.primary_skills = resume_db_data.get("primary_skills")
                                            matched_resume.technical_skills = resume_db_data.get("technical_skills")
                                            matched_resume.tools_and_frameworks = resume_db_data.get("tools_and_frameworks")
                                            matched_resume.soft_skills = resume_db_data.get("soft_skills")
                                            matched_resume.domain_skills = resume_db_data.get("domain_skills")
                                            matched_resume.education = resume_db_data.get("education")
                                            matched_resume.certifications = resume_db_data.get("certifications")
                                            matched_resume.resume_file = resume_db_data.get("resume_file_url")
                                            matched_resume.parsed_text = resume_db_data.get("attachment_content")
                                            matched_resume.status = resume_db_data.get("status")
                                            matched_resume.responsibility_summary = resume_db_data.get("responsibility_summary")
                                            matched_resume.resume_responsibilities_summary_embedding = resume_responsibilities_embedding if resume_responsibilities_embedding else None
                                            matched_resume.updated_by = request.user.username
                                            matched_resume.resume_source="Our Internal DB",
                                            matched_resume.save()

                                            resume_obj = matched_resume
                                        else:
                                            resume_obj = TblCandidateResume.objects.create(
                                                candidate_id=profile,
                                                title=resume_db_data["title"],
                                                totalexp=resume_db_data["total_exp"],
                                                company=resume_db_data["company"],
                                                salary=resume_db_data["salary"],
                                                location=resume_db_data["location"],
                                                primary_skills=resume_db_data["primary_skills"],
                                                technical_skills = resume_db_data["technical_skills"],
                                                tools_and_frameworks = resume_db_data["tools_and_frameworks"],
                                                soft_skills = resume_db_data["soft_skills"],
                                                domain_skills = resume_db_data["domain_skills"],
                                                education=resume_db_data["education"],
                                                projects=resume_db_data["projects"],
                                                certifications=resume_db_data["certifications"],
                                                resume_file=resume_file_url,
                                                parsed_text=attachment_content,
                                                responsibility_summary = resume_db_data["responsibility_summary"],
                                                resume_responsibilities_summary_embedding = resume_responsibilities_embedding if resume_responsibilities_embedding else None,
                                                resume_source="Internal DB",
                                                created_by=request.user.username,
                                                status="Parsed"
                                            )

                                            try:
                                                match_response = run_matching_engine_candidate(resume_obj.resume_id)
                                                matches = match_response.get("matches", {})
                                            except Exception as e:
                                                matches = {}
                                            
                                            if matches:
                                                manager_sending_emails = list(
                                                    User.objects
                                                    .filter(groups__name="Delivery Manager (DM)", is_active=True)
                                                    .exclude(email__isnull=True)
                                                    .exclude(email__exact="")
                                                    .values_list("email", flat=True)
                                                )

                                                super_admin_cc_emails = list(
                                                    User.objects
                                                    .filter(groups__name="Implementer Super Admin", is_active=True)
                                                    .exclude(email__isnull=True)
                                                    .exclude(email__exact="")
                                                    .values_list("email", flat=True)
                                                )

                                                subject, body = get_email_template("candidate_jd_match_notification", is_html=False)

                                                candidate_name = f"{profile.first_name} {profile.last_name}"

                                                subject = subject.format(candidate_name=candidate_name)

                                                body = body.format(
                                                    resume_id=resume_obj.resume_id,
                                                    candidate_name=candidate_name,
                                                    upload_date=resume_obj.created_dt if hasattr(resume_obj, "created_dt") else "N/A",
                                                    match_count=len(matches)
                                                )

                                                for to_mail in manager_sending_emails:
                                                    send_email_safe(
                                                        subject,
                                                        body,
                                                        to_mail,
                                                        settings.EMAIL_HOST_USER,
                                                        cc_emails=super_admin_cc_emails,
                                                        is_html=False
                                                    )

                            except Exception as e:
                                raise Exception("An Error Occurred during Gemini Response", e)

                if latest_processed_dt > last_read_dt:
                    email_details = list(implemntor_data.email_details or [])
                    updated = False

                    for item in email_details:
                        if item.get("email") == email_user:
                            item["last_read"] = latest_processed_dt.isoformat()
                            updated = True
                            break

                    if updated:
                        implemntor_data.email_details = email_details
                        implemntor_data.save(update_fields=["email_details"])

                mail.logout()
            return Response(
                {
                    "status": "success",
                },
                status=200
            )

        except Exception as e:
            return Response(
                {
                    "status": "failed",
                    "error": str(e)
                },
                status=500
            )

class UploadJobDescriptionFile(APIView):
    permission_classes = (IsAuthenticated,)

    def extract_json_from_llm(self, text: str) -> dict:
        # Remove ```json and ```
        cleaned = re.sub(r"```json|```", "", text, flags=re.IGNORECASE).strip()

        return json.loads(cleaned)
    
    def validate_jd_data(self, jd_data, jd_date, client_data):
        errors = []
        # -------- Required fields --------
        if not jd_data.get("job_title"):
            errors.append("Missing Job Title")

        if not jd_data.get("job_summary"):
            errors.append("Missing Job Summary")

        if not jd_data.get("responsibilities"):
            errors.append("Missing Responsibilities")

        if not jd_data.get("years_of_experience"):
            errors.append("Missing Years of Experience")

        # -------- Date validation --------
        if not jd_date:
            errors.append("Invalid or missing Job Description Date")

        # -------- Client validation --------
        if not client_data:
            errors.append("Client Data not found for sender domain")

        return errors
    
    def normalize_db_text(self, value):
        if not value:
            return ""
        value = value.strip().lower()
        value = value.replace(",", "").replace(".", "")
        value = re.sub(r"\s+", " ", value)
        return value
    
    def send_notify_mail(self, subject, message, sending_mails, cc_mails=[]):
        try:
            email = EmailMessage(
                subject=subject,
                body=message,
                from_email=settings.EMAIL_HOST_USER,
                to=sending_mails,
                cc=cc_mails,
            )
            email.send()

            return True

        except Exception as e:
            traceback.print_exc()
            return False
        
    def format_code(self, text: str, length: int) -> str:
        if not text:
            return "X" * length

        # Clean input
        clean = re.sub(r"[^A-Za-z ]", "", text).strip().upper()
        words = clean.split()

        # Step 1: take first letter of each word
        result = "".join(word[0] for word in words)

        # Step 2: if short, take remaining letters from LAST word
        if len(result) < length and words:
            last_word = words[-1]
            needed = length - len(result)

            # Skip first letter of last word (already used)
            extra = last_word[1:1 + needed]
            result += extra

        # Step 3: pad with X if still short
        return (result + ("X" * length))[:length]
        
    def jd_response_handle(self, jd_generate_prompt, client_id, jd_file_url, file_content, base_dir, airecruit_llm):
        generation_config = {
            "temperature": 0.2,  # Lower temperature for more deterministic, less random output
            "thinking_budget": 32768,
        }
        airecruit_llm.create_chat(
            generation_config["temperature"], generation_config["thinking_budget"]
        )
        try:
            jd_llm_response = airecruit_llm.send_message(jd_generate_prompt)

            print(jd_llm_response.text)
            jd_data = self.extract_json_from_llm(jd_llm_response.text)

            if not jd_data.get("job_title") or jd_data.get("job_title") == "Not Specified":
                raise Exception("Job Title Missing from LLM Response")

            jd_date = timezone.now().date()

            client_data = TblClient.objects.get(client_id=client_id)
            duplicate_jds = []
            if client_data:
                existing_jds = TblJobDescription.objects.filter(
                    client_id=client_data,
                    status__in=["Open", "Duplicate"]
                )
                
                incoming_job_title = self.normalize_db_text(jd_data.get("job_title"))
                incoming_location = self.normalize_db_text(jd_data.get("job_location"))
                incoming_job_type = self.normalize_db_text(jd_data.get("job_type"))
                incoming_exp = self.normalize_db_text(jd_data.get("years_of_experience"))

                for jd in existing_jds:
                    db_job_title = self.normalize_db_text(jd.job_title)
                    db_location = self.normalize_db_text(jd.job_location)
                    db_job_type = self.normalize_db_text(jd.job_type)
                    db_exp = self.normalize_db_text(jd.years_of_experience)

                    if (
                        db_job_title == incoming_job_title
                        and db_location == incoming_location
                        and db_job_type == incoming_job_type
                        and db_exp == incoming_exp
                    ):
                        duplicate_jds.append(jd)

                if duplicate_jds:
                    print(f"Found {len(duplicate_jds)} duplicate JD(s)")

            validation_errors = self.validate_jd_data(jd_data, jd_date, client_data)

            has_error = bool(validation_errors)
            error_desc = "; ".join(validation_errors) if validation_errors else None

            if duplicate_jds:
                status_value = "Duplicate"
            elif has_error:
                status_value = "Error"
            else:
                status_value = "Open"

            implemntor_data = (
                ImplementerDetails.objects.first()
            )

            responsibilities_embedding = airecruit_llm.generate_embeddings(jd_data.get("responsibilities"))

            jd_obj = TblJobDescription.objects.create(
                client_id=client_data,
                implementor_id=implemntor_data,
                jd_date=jd_date,
                job_title=jd_data.get("job_title"),
                ai_title=jd_data.get("ai_title"),
                job_type=jd_data.get("job_type"),
                client_job_id=jd_data.get("job_id"),
                years_of_experience=jd_data.get("years_of_experience"),
                duration=jd_data.get("duration"),
                about_company=jd_data.get("about_company"),
                job_summary=jd_data.get("job_summary"),
                responsibilities=jd_data.get("responsibilities"),
                domain_requirements=jd_data.get("domain_requirements"),
                certification_requirements=jd_data.get("certification_requirements"),
                security_clearance_requirements=jd_data.get("security_clearance_requirements"),
                job_location=jd_data.get("job_location"),
                required_qualifications=jd_data.get("required_qualifications"),
                preferred_qualifications=jd_data.get("preferred_qualifications"),
                onsite_job=jd_data.get("onsite_job"),
                working_hours=jd_data.get("working_hours"),
                benefits=jd_data.get("benefits"),
                requirement_priority=jd_data.get("requirement_priority"),
                salary_range=jd_data.get("salary_range"),
                search_pattern=jd_data.get("search_pattern"),
                no_of_open_positions=jd_data.get("no_of_open_positions"),
                status=status_value,
                has_error=has_error,
                error_desc=error_desc,
                jd_source = jd_file_url,
                jd_responsibilities_embedding = responsibilities_embedding if responsibilities_embedding is not None else None,
                changes = jd_data.get("changes")
            )

            check_ai_title_similarity_and_notify(jd_obj)

            generated_jd_file_name = f"{jd_obj.jd_display_id}_file.txt"
            generated_file_path = os.path.join(base_dir, generated_jd_file_name)

            with open(generated_file_path, "w", encoding="utf-8") as file:
                file.write(file_content)

            generated_jd_relative_path = f"JD_files/uploaded_files/{generated_jd_file_name}"
            generated_jd_file_url = settings.MEDIA_URL + generated_jd_relative_path

            jd_obj.jd_source_txt = generated_jd_file_url
            jd_obj.save(update_fields=["jd_source_txt"])

            if duplicate_jds:
                jd_details_lines = []
                duplicate_jd_id = []
                for jd in duplicate_jds:
                    jd_details_lines.append(
                        f"- JD ID: {jd.jd_display_id}, Job Title: {jd.job_title}"
                    )
                    duplicate_jd_id.append(jd.jd_display_id)

                jd_obj.duplicated_jd_id = duplicate_jd_id
                jd_obj.save(update_fields=["duplicated_jd_id"])

                jd_details_str = "\n".join(jd_details_lines)

                duplicate_mail_subject, duplicate_mail_message = get_email_template(
                    "duplicate_jd_notification", is_html=False
                )

                duplicate_mail_message = duplicate_mail_message.format(
                    new_jd_id=jd_obj.jd_display_id,
                    job_title=jd_obj.job_title,
                    duplicate_jd_list=jd_details_str
                )

                manager_sending_emails = list(
                    User.objects
                    .filter(groups__name="Delivery Manager (DM)", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                super_admin_cc_emails = list(
                    User.objects
                    .filter(groups__name="Implementer Super Admin", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                self.send_notify_mail(
                    duplicate_mail_subject,
                    duplicate_mail_message,
                    manager_sending_emails,
                    super_admin_cc_emails
                )
        except Exception as e:
            raise Exception("An Error Occurred during Gemini Response", e)
        
    def build_jd_text_from_row(self, data):
        lines = []
        for key, value in data.items():
            if value:
                label = key.replace("_", " ").title()
                lines.append(f"{label}: {value}")
        return "\n".join(lines)

    def post(self, request):
        try:
            uploaded_file = request.FILES.get("file")
            client_id = request.data.get("client_id")
            if not uploaded_file:
                return Response(
                    {"status": "failed", "error": "No file provided"},
                    status=400
                )
            
            base_dir = os.path.join(settings.MEDIA_ROOT, "JD_files", "uploaded_files")
            os.makedirs(base_dir, exist_ok=True)

            fs = FileSystemStorage(location=base_dir)
            filename = fs.save(uploaded_file.name, uploaded_file)
            file_path = fs.path(filename)

            extension = os.path.splitext(filename)[1].lower()

            jd_relative_path = f"JD_files/uploaded_files/{filename}"
            jd_file_url = settings.MEDIA_URL + jd_relative_path

            airecruit_llm = AIEditLLM()

            if extension in [".xlsx", ".xls", ".csv"]:
                content = DocumentTextExtraction.process_excel_or_csv(file_path, extension)
                if content:
                    with transaction.atomic():
                        required_fields = ["job_title", "responsibilities"]
                        created_jd = []
                        for data in content:
                            if not all(data.get(f) for f in required_fields):
                                continue

                            jd_date = timezone.now().date()

                            client_data = TblClient.objects.get(client_id=client_id)
                            duplicate_jds = []
                            if client_data:
                                existing_jds = TblJobDescription.objects.filter(
                                    client_id=client_data,
                                    status__in=["Open", "Duplicate"]
                                )
                                
                                incoming_job_title = self.normalize_db_text(data.get("job_title"))
                                incoming_location = self.normalize_db_text(data.get("job_location"))
                                incoming_job_type = self.normalize_db_text(data.get("job_type"))
                                incoming_exp = self.normalize_db_text(data.get("years_of_experience"))

                                for jd in existing_jds:
                                    db_job_title = self.normalize_db_text(jd.job_title)
                                    db_location = self.normalize_db_text(jd.job_location)
                                    db_job_type = self.normalize_db_text(jd.job_type)
                                    db_exp = self.normalize_db_text(jd.years_of_experience)

                                    if (
                                        db_job_title == incoming_job_title
                                        and db_location == incoming_location
                                        and db_job_type == incoming_job_type
                                        and db_exp == incoming_exp
                                    ):
                                        duplicate_jds.append(jd)

                                if duplicate_jds:
                                    print(f"Found {len(duplicate_jds)} duplicate JD(s)")

                            validation_errors = self.validate_jd_data(data, jd_date, client_data)
                            has_error = bool(validation_errors)
                            error_desc = "; ".join(validation_errors) if validation_errors else None

                            if duplicate_jds:
                                status_value = "Duplicate"
                            elif has_error:
                                status_value = "Error"
                            else:
                                status_value = "Open"

                            implemntor_data = (
                                ImplementerDetails.objects.first()
                            )

                            responsibilities_embedding = airecruit_llm.generate_embeddings(data.get("responsibilities"))

                            jd_obj = TblJobDescription.objects.create(
                                client_id=client_data,
                                implementor_id=implemntor_data,
                                jd_date=jd_date,
                                job_title=data.get("job_title"),
                                job_type=data.get("job_type"),
                                years_of_experience=data.get("years_of_experience"),
                                about_company=data.get("about_company"),
                                client_job_id=data.get("job_id", ""),
                                job_summary=data.get("job_summary"),
                                responsibilities=data.get("responsibilities"),
                                domain_requirements=data.get("domain_requirements"),
                                certification_requirements=data.get("certification_requirements"),
                                security_clearance_requirements=data.get("security_clearance_requirements"),
                                job_location=data.get("job_location"),
                                required_qualifications=data.get("required_qualifications"),
                                preferred_qualifications=data.get("preferred_qualifications"),
                                onsite_job=data.get("onsite_job"),
                                working_hours=data.get("working_hours"),
                                benefits=data.get("benefits"),
                                requirement_priority=data.get("requirement_priority"),
                                salary_range=data.get("salary_range"),
                                no_of_open_positions=data.get("no_of_open_positions"),
                                status=status_value,
                                has_error=has_error,
                                error_desc=error_desc,
                                jd_responsibilities_embedding = responsibilities_embedding if responsibilities_embedding is not None else None,
                                jd_source = jd_file_url
                            )
                            created_jd.append(jd_obj)

                            check_ai_title_similarity_and_notify(jd_obj)

                            generated_jd_file_name = f"{jd_obj.jd_display_id}_file.txt"
                            generated_file_path = os.path.join(base_dir, generated_jd_file_name)

                            file_content = self.build_jd_text_from_row(data)

                            with open(generated_file_path, "w", encoding="utf-8") as file:
                                file.write(file_content)

                            generated_jd_relative_path = f"JD_files/uploaded_files/{generated_jd_file_name}"
                            generated_jd_file_url = settings.MEDIA_URL + generated_jd_relative_path

                            jd_obj.jd_source_txt = generated_jd_file_url
                            jd_obj.save(update_fields=["jd_source_txt"])

                            if duplicate_jds:
                                jd_details_lines = []
                                duplicate_jd_id = []
                                for jd in duplicate_jds:
                                    jd_details_lines.append(
                                        f"- JD ID: {jd.jd_display_id}, Job Title: {jd.job_title}"
                                    )
                                    duplicate_jd_id.append(jd.jd_display_id)

                                jd_obj.duplicated_jd_id = duplicate_jd_id
                                jd_obj.save(update_fields=["duplicated_jd_id"])

                                jd_details_str = "\n".join(jd_details_lines)

                                duplicate_mail_subject, duplicate_mail_message = get_email_template(
                                    "duplicate_jd_notification", is_html=False
                                )

                                duplicate_mail_message = duplicate_mail_message.format(
                                    new_jd_id=jd_obj.jd_display_id,
                                    job_title=jd_obj.job_title,
                                    duplicate_jd_list=jd_details_str
                                )

                                manager_sending_emails = list(
                                    User.objects
                                    .filter(groups__name="Delivery Manager (DM)", is_active=True)
                                    .exclude(email__isnull=True)
                                    .exclude(email__exact="")
                                    .values_list("email", flat=True)
                                )

                                super_admin_cc_emails = list(
                                    User.objects
                                    .filter(groups__name="Implementer Super Admin", is_active=True)
                                    .exclude(email__isnull=True)
                                    .exclude(email__exact="")
                                    .values_list("email", flat=True)
                                )

                                self.send_notify_mail(
                                    duplicate_mail_subject,
                                    duplicate_mail_message,
                                    manager_sending_emails,
                                    super_admin_cc_emails
                                )

                        llm_input_data = []
                        for jd in created_jd:
                            llm_sending_data = {
                                "jd_id": jd.jd_id,
                                "jd_display_id": jd.jd_display_id,
                                "job_title": jd.job_title,
                                "job_type": jd.job_type,
                                "years_of_experience": jd.years_of_experience,
                                "about_company": jd.about_company,
                                "job_summary": jd.job_summary,
                                "responsibilities": jd.responsibilities,
                                "domain_requirements": jd.domain_requirements,
                                "certification_requirements": jd.certification_requirements,
                                "security_clearance_requirements": jd.security_clearance_requirements,
                                "onsite_job": jd.onsite_job,
                                "job_location": jd.job_location,
                                "required_qualifications": jd.required_qualifications,
                                "preferred_qualifications": jd.preferred_qualifications
                            }
                            llm_input_data.append(llm_sending_data)

                        jd_search_pattern_prompt = LlmPrompts.jd_search_pattern_prompt(llm_input_data)
                        generation_config = {
                            "temperature": 0.2,
                            "thinking_budget": 32768,
                        }
                        airecruit_llm.create_chat(
                            generation_config["temperature"], generation_config["thinking_budget"]
                        )
                        jd_llm_response = airecruit_llm.send_message(jd_search_pattern_prompt)
                        llm_jd_data = self.extract_json_from_llm(jd_llm_response.text)

                        print(jd_llm_response.text)

                        for jd in llm_jd_data:
                            found_jd = next(
                                (item for item in created_jd if item.jd_id == jd["jd_id"]),
                                None
                            )

                            if found_jd:
                                found_jd.search_pattern = jd["search_pattern"]
                                found_jd.save(update_fields=["search_pattern"])


            elif extension == ".pdf":
                content = DocumentTextExtraction.process_pdf(file_path)  
                jd_generate_prompt = LlmPrompts.jd_extraction_prompt(content)
                self.jd_response_handle(jd_generate_prompt, client_id, jd_file_url, content, base_dir, airecruit_llm)
                
            elif extension in [".docx", ".doc"]:
                content = DocumentTextExtraction.process_word(file_path)
                jd_generate_prompt = LlmPrompts.jd_extraction_prompt(content)
                self.jd_response_handle(jd_generate_prompt, client_id, jd_file_url, content, base_dir, airecruit_llm)

            else:
                return Response(
                    {"status": "failed", "error": "Unsupported file type"},
                    status=400
                )

            return Response(
                {
                    "status": "success",
                    "filename": filename,
                },
                status=200
            )
        except Exception as e:
            return Response(
                {
                    "status": "failed",
                    "error": str(e)
                },
                status=500
            )

class ImpDepartmentViewSet(ModelViewSet):
    queryset = TblImpDepartment.objects.all().order_by("-created_date")
    serializer_class = TblImpDepartmentSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = (JSONParser, MultiPartParser, FormParser)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            serializer.save(
                created_by=request.user.username
            )
            return Response(
                {
                    "message": "Department created successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_201_CREATED
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )

    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(
            instance,
            data=request.data,
            partial=True  
        )

        if serializer.is_valid():
            serializer.save(
                updated_by=request.user.username
            )
            return Response(
                {
                    "message": "Department updated successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_200_OK
            )

        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )

class AssignJDView(APIView):
    permission_classes = [IsAuthenticated]
    def post(self, request):
        jd_ids = request.data.get("jds", [])
        recruiters = request.data.get("recruiters", [])

        if not jd_ids or not recruiters:
            return Response(
                {"detail": "JDs and recruiters required"},
                status=status.HTTP_400_BAD_REQUEST
            )

        if not isinstance(jd_ids, list):
            return Response(
                {"detail": "jds must be a list"},
                status=status.HTTP_400_BAD_REQUEST
            )

        new_assignments = []
        total_created = 0
        total_skipped = 0

        with transaction.atomic():

            for jd_id in jd_ids:
                jd_obj = TblJobDescription.objects.select_for_update().get(jd_id=jd_id)

                for recruiter_id in recruiters:

                    exists = AssignJd.objects.filter(
                        jd=jd_obj,
                        user_id=recruiter_id
                    ).exists()

                    if exists:
                        total_skipped += 1
                        continue

                    assignment = AssignJd.objects.create(
                        user_id=recruiter_id,
                        jd=jd_obj,
                        client=jd_obj.client_id,
                        status="Assigned"
                    )

                    new_assignments.append(assignment)
                    total_created += 1

                jd_obj.jd_stage = "Assigned"
                jd_obj.status = "Open"
                jd_obj.save(update_fields=["jd_stage", "status"])

        if new_assignments:
            notify_recruiter_multiple_jds(new_assignments)

        return Response(
            {
                "total_created": total_created,
                "total_skipped": total_skipped
            },
            status=200
        )

    def get(self, request):
        jd_id = request.query_params.get("jd_id")

        if not jd_id:
            return Response({"detail": "jd_id is required"}, status=400)

        jd_obj = get_object_or_404(TblJobDescription, jd_id=jd_id)

        assignments = (
            AssignJd.objects
            .filter(jd=jd_obj)
            .select_related("user")
            .order_by("-id")
        )

        result = []
        for a in assignments:
            result.append({
                "id": a.id,
                "recruiter_id": a.user_id,
                "recruiter_name": a.user.username if a.user else "",
            })

        return Response({
            "jd_id": jd_obj.jd_id,
            "jd_display_id": jd_obj.jd_display_id,
            "assigned_recruiters": result
        }, status=200)

    def put(self, request):
        jd_id = request.data.get("jd_id")
        recruiter_ids = request.data.get("recruiter_ids", [])

        if not jd_id:
            return Response({"detail": "jd_id is required"}, status=400)

        if not isinstance(recruiter_ids, list):
            return Response({"detail": "recruiter_ids must be list"}, status=400)

        jd_obj = get_object_or_404(TblJobDescription, jd_id=jd_id)

        existing_ids = set(
            AssignJd.objects.filter(jd=jd_obj).values_list("user_id", flat=True)
        )
        new_ids = set(recruiter_ids)

        new_assignments = []

        removed_ids = existing_ids - new_ids

        for user_id in removed_ids:
            try:
                recruiter = User.objects.get(id=user_id)

                if not recruiter.email:
                    continue

                subject, body = get_email_template("recruiter_removed_from_jd", is_html=False)

                subject = subject.format(
                    jd_id=jd_obj.jd_display_id
                )

                body = body.format(
                    recruiter_name=recruiter.get_full_name() or recruiter.username,
                    jd_id=jd_obj.jd_display_id
                )

                send_email_safe(
                    subject=subject,
                    body=body.strip(),
                    to_email=recruiter.email,
                    cc_emails=[],
                    from_email=settings.EMAIL_HOST_USER,
                    is_html=False
                )

            except User.DoesNotExist:
                continue

        with transaction.atomic():
            AssignJd.objects.filter(jd=jd_obj).exclude(user_id__in=new_ids).delete()

            for rid in new_ids - existing_ids:
                user_obj = get_object_or_404(User, id=rid)

                assignment = AssignJd.objects.create(
                    jd=jd_obj,
                    user=user_obj,
                    client=jd_obj.client_id,
                    status="Assigned"
                )

                new_assignments.append(assignment)

            if new_ids:
                jd_obj.jd_stage = "Assigned"
                jd_obj.status = "Open"
            else:
                jd_obj.jd_stage = "Created"
                jd_obj.status = "Open"

            jd_obj.save(update_fields=["jd_stage", "status"])

        if new_assignments:
            notify_recruiter_multiple_jds(new_assignments)

        return Response(
            {
                "message": "Recruiters updated successfully",
                "jd_stage": jd_obj.jd_stage,
                "status": jd_obj.status
            },
            status=200
        )
    
class generateKey:

    @staticmethod
    def returnValue(identifier):
        return f"{identifier}-Some-Strong-Static-Secret"

class ForgotPasswordMailOTP(APIView):
    permission_classes = [AllowAny]

    def get(self, request):
        try:
            user_name = request.GET.get('username', '')
            if not user_name:
                return Response({"status": False, "message": "Username is required"}, status=400)

            user = User.objects.get(username=user_name)
            if not user.email:
                return Response({"status": False, "message": "User has no email registered"}, status=400)

            otp_status = self.sendOTP(user)
            if otp_status:
                return Response({
                    'otp_status': True,
                    'message': f'Email OTP has been sent to {user.email}',
                    'id': user.id
                }, status=200)
            else:
                return Response({"status": False, "message": "Failed to send OTP"}, status=500)

        except User.DoesNotExist:
            return Response({"status": False, "message": "Please enter valid username"}, status=404)
        except Exception:
            traceback.print_exc()
            return Response({"status": False, "message": "Unexpected error"}, status=500)
        
    def sendOTP(self, user):
        from django.utils import timezone
        try:
            obj, created = EmailOTPUser.objects.get_or_create(user=user)
            obj.counter += 1
            obj.created_at = timezone.now()
            obj.save()

            keygen = generateKey()
            key = base64.b32encode(keygen.returnValue(user.id).encode())
            OTP = pyotp.TOTP(key, interval=settings.OTP_EXPIRY_SECONDS)

            otp_code = OTP.now()

            subject, message = get_email_template("send_otp_reset_password", is_html=False)

            subject = subject

            message = message.format(
                username=user.username,
                otp_code=otp_code,
                otp_minutes=settings.OTP_EXPIRY_MINUTES
            )

            email = EmailMessage( 
                subject=subject, 
                body=message, 
                from_email=settings.EMAIL_HOST_USER, 
                to=[user.email], cc=[], 
                ) 
            
            email.send()
            return True
        except Exception:
            traceback.print_exc()
            return False

    def post(self, request):
        from datetime import timedelta
        from django.utils import timezone
        user_id = request.data.get('id')
        otp_code = request.data.get('fp_code')

        if not user_id or not otp_code:
            return Response("User ID and OTP are required", status=400)

        try:
            obj = EmailOTPUser.objects.get(user_id=user_id)
        except ObjectDoesNotExist:
            return Response("User ID does not exist", status=404)

        if timezone.now() > obj.created_at + timedelta(minutes=settings.OTP_EXPIRY_MINUTES):
            return Response("Your OTP has expired", status=400)

        keygen = generateKey()
        key = base64.b32encode(keygen.returnValue(user_id).encode())
        OTP = pyotp.TOTP(key, interval=settings.OTP_EXPIRY_SECONDS)

        if OTP.verify(otp_code, valid_window=1):
            obj.is_verified = True
            obj.save()

            new_password = generate_password(8)
            user = User.objects.get(id=user_id)
            user.set_password(new_password)
            user.save()

            subject, message = get_email_template("password_reset_success", is_html=False)

            message = message.format(
                username=user.username,
                new_password=new_password
            )
            email = EmailMessage( 
                subject=subject, 
                body=message, 
                from_email=settings.EMAIL_HOST_USER, 
                to=[user.email], cc=[], 
                ) 
            email.send()

            return Response(f"New Password has been sent to {user.email}", status=200)

        return Response("OTP is wrong or expired", status=400)

    def patch(self, request):
        """
        Resend OTP to user's email.
        Requires: { "id": <user_id> }
        """
        user_id = request.data.get('id')
        if not user_id:
            return Response({"status": False, "message": "User ID is required"}, status=400)

        try:
            user = User.objects.get(id=user_id)
            if not user.email:
                return Response({"status": False, "message": "User has no email registered"}, status=400)

            otp_status = self.sendOTP(user)
            if otp_status:
                return Response({
                    "status": True,
                    "message": f"OTP resent to {user.email}"
                }, status=200)
            else:
                return Response({"status": False, "message": "Failed to resend OTP"}, status=500)

        except User.DoesNotExist:
            return Response({"status": False, "message": "Invalid User ID"}, status=404)
        except Exception:
            traceback.print_exc()
            return Response({"status": False, "message": "Unexpected error"}, status=500)
        
class RecruiterJDCountView(APIView):
    def get(self, request):
        data = (
            AssignJd.objects
            .filter(status="Assigned")
            .values("user__id")
            .annotate(jd_count=Count("jd"))
        )
        return Response(data)

class LinkedInCallback(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        code = request.GET.get("code")

        if not code:
            return Response({"error": "Missing authorization code"}, status=400)

        implementer = ImplementerDetails.objects.first()

        config = ImplementerPortalConfig.objects.get(
            implementer=implementer,
            portal="linkedin"
        )

        token_resp = requests.post(
            "https://www.linkedin.com/oauth/v2/accessToken",
            data={
                "grant_type": "authorization_code",
                "code": code,
                "client_id": config.client_id,
                "client_secret": config.client_secret,
                "redirect_uri": config.redirect_uri,
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"},
        )

        token_data = token_resp.json()

        if "access_token" not in token_data:
            return Response({"linkedin_error": token_data}, status=400)

        access_token = token_data["access_token"]

        # LinkedIn OpenID profile
        profile = requests.get(
            "https://api.linkedin.com/v2/userinfo",
            headers={
                "Authorization": f"Bearer {access_token}",
                "X-Restli-Protocol-Version": "2.0.0"
            }
        ).json()

        linkedin_urn = profile.get("sub")   # <-- THIS is the LinkedIn user ID

        if not linkedin_urn:
            return Response({
                "error": "LinkedIn OpenID did not return user id",
                "linkedin_response": profile
            }, status=400)

        # Save to DB
        config.access_token = access_token
        config.portal_user_urn = linkedin_urn
        config.expires_at = timezone.now() + timedelta(seconds=token_data.get("expires_in", 0))
        config.is_connected = True
        config.save()

        return Response({"status": "LinkedIn connected"})
class PostJDToLinkedIn(APIView):
    permission_classes = [IsAuthenticated]
    parser_classes = [JSONParser]

    def post(self, request):
        jd_display_id = request.data.get("jd_id")
        fields = request.data.get("fields", {})
        force_repost = request.data.get("force_repost", False)
        implementer = ImplementerDetails.objects.first()
        print("implementer", implementer)

        if not jd_display_id:
            return Response({"error": "JD ID required"}, status=400)

        jd = get_object_or_404(TblJobDescription, jd_display_id=jd_display_id)

        config = ImplementerPortalConfig.objects.get(
            implementer=implementer,
            portal="linkedin",
            is_connected=True
        )

        access_token = config.access_token
        linkedin_urn = config.portal_user_urn

        if not access_token or not linkedin_urn:
            return Response({"error": "LinkedIn token not configured"}, status=500)

        last_post = LinkedInJobPost.objects.filter(
            jd=jd, status="Posted"
        ).order_by("-posted_at").first()

        previous_payload = last_post.posted_payload if last_post else {}

        def current_value(key):
            return {
                "job_title": jd.job_title,
                "job_location": jd.job_location,
                "experience": jd.years_of_experience,
                "job_summary": jd.job_summary,
                "salary": jd.salary_range,
                "apply_link": True
            }.get(key)

        changed_fields = {}
        for key, enabled in fields.items():
            if enabled and current_value(key) != previous_payload.get(key):
                changed_fields[key] = current_value(key)

        if last_post and not changed_fields and not force_repost:
            return Response(
                {
                    "error": "No changes detected since last post",
                    "requires_confirmation": True
                },
                status=400
            )

        if last_post and force_repost:
            deleted = delete_linkedin_post(
                access_token,
                last_post.linkedin_post_urn
            )
            last_post.status = "Deleted" if deleted else "DeleteFailed"
            last_post.save(update_fields=["status"])

        lines = ["🚀 Hiring Now!"]

        if fields.get("job_title"):
            lines.append(f"📌 Role: {jd.job_title}")

        if fields.get("job_location"):
            lines.append(f"📍 Location: {jd.job_location}")

        if fields.get("experience"):
            lines.append(f"🧠 Experience: {jd.years_of_experience}")

        if fields.get("job_summary"):
            lines.append(jd.job_summary)

        if fields.get("salary") and jd.salary_range:
            lines.append(f"💰 Salary: {jd.salary_range}")

        apply_url = generate_apply_link(jd)
        if fields.get("apply_link"):
            lines.append(f"👉 Apply here: {apply_url}")

        if fields.get("custom_text"):
            lines.append(fields["custom_text"])

        lines.append(f"🆔 JD Ref: {jd.jd_display_id}")
        lines.append(f"⏰ {timezone.now().strftime('%d-%m-%Y %H:%M')}")

        text = "\n\n".join(lines)

        res = post_to_linkedin(access_token, linkedin_urn, text)

        merged_payload = {**previous_payload, **fields}

        post_success = res.status_code in (200, 201)

        LinkedInJobPost.objects.create(
            jd=jd,
            posted_by=request.user,
            linkedin_post_urn=res.json().get("id") if post_success else None,
            apply_url=apply_url,
            status="Posted" if post_success else "Failed",
            posted_payload=merged_payload
        )

        if post_success:
            jd.jd_stage = "Posted"
            jd.status = "Open"
            jd.save(update_fields=["jd_stage", "status"])

        return Response(
            {
                "status": "Posted to LinkedIn",
                "force_repost": force_repost,
                "changed_fields": list(changed_fields.keys())
            },
            status=200
        )

    
class LinkedInStatus(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        implementer = ImplementerDetails.objects.first()

        config = ImplementerPortalConfig.objects.filter(
            implementer=implementer,
            portal="linkedin",
            is_active=True,
            is_connected=True
        ).first()

        return Response({
            "connected": bool(config)
        })
    
class JobFilterOptionsAPIView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response({
            "clients": [
                {
                    "label": c.client_name,
                    "value": c.client_id
                }
                for c in TblClient.objects.all()
            ],
            "departments": [
                {
                    "label": d.department_name,
                    "value": d.department_id
                }
                for d in TblDepartment.objects.all()
            ],
            "job_type": [
                {
                    "label": jt,
                    "value": jt
                }
                for jt in TblJobDescription.objects
                    .values_list("job_type", flat=True)
                    .distinct()
                    .exclude(job_type__isnull=True)
            ],
            "status": [
                { "label": "Open", "value": "Open" },
                { "label": "Error", "value": "Error" }
            ]
        })

class ImplementerPortalConfigViewSet(ModelViewSet):
    permission_classes = [IsAuthenticated]
    serializer_class = ImplementerPortalConfigSerializer
    queryset = ImplementerPortalConfig.objects.all()

    def get_queryset(self):
        qs = super().get_queryset()
        implementer = self.request.query_params.get("implementer")
        if implementer:
            qs = qs.filter(implementer_id=implementer)
        return qs

    def perform_create(self, serializer):
        instance = serializer.save()

        if instance.portal == "linkedin" and instance.has_token:
            instance.is_connected = True
            instance.is_active = True
            instance.save(update_fields=["is_connected", "is_active"])


    def perform_update(self, serializer):
        instance = self.get_object()

        previous_active = instance.is_active

        instance = serializer.save()

        if previous_active and instance.is_active is False:
            instance.is_connected = False
            instance.save(update_fields=["is_connected"])

        if (
            instance.has_token
            and instance.is_active
            and instance.token
        ):
            instance.is_connected = True
            instance.save(update_fields=["is_connected"])

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()

        if instance.is_connected:
            instance.is_connected = False
            instance.save()

        return super().destroy(request, *args, **kwargs)

    
class LinkedInAuthStart(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        implementer = ImplementerDetails.objects.first()

        config = ImplementerPortalConfig.objects.get(
            implementer=implementer,
            portal="linkedin"
        )

        params = {
            "response_type": "code",
            "client_id": config.client_id,
            "redirect_uri": config.redirect_uri,
            "scope": config.scope,
            "state": "auto_recruiter"
        }

        url = "https://www.linkedin.com/oauth/v2/authorization?" + urlencode(params)

        return Response({"auth_url": url})
    
class careerViewSet(ModelViewSet):
    queryset = career.objects.all().order_by("-created_dt")
    serializer_class = careerSerializer

    def get_queryset(self):
        implementer = self.request.query_params.get("implementer")
        qs = career.objects.all()

        if implementer:
            qs = qs.filter(implementer_id=implementer)

        return qs

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)

# views.py 

# 1. Update your existing ConnectFounditPortal view
class ConnectFounditPortal(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        implementer_id = request.data.get("implementer_id")
        
        if not implementer_id:
            return Response({"error": "implementer_id is required"}, status=400)

        # 1. Search for a config that matches either "foundit" or "monster"
        portal_config = ImplementerPortalConfig.objects.filter(
            Q(portal="foundit") | Q(portal="monster"),
            implementer_id=implementer_id
        ).first()

        # 2. If no record exists, provide a helpful error message instead of a generic 404
        if not portal_config:
            return Response({
                "error": "No configuration found. Please add your Foundit/Monster credentials in the table first."
            }, status=status.HTTP_404_NOT_FOUND)

        # 3. Check for credentials
        if not portal_config.portal_username or not portal_config.portal_password:
            return Response(
                {"error": "Foundit credentials (Username/Password) are missing. Please edit the configuration."},
                status=400
            )

        # 4. Mark as connected
        portal_config.is_connected = True
        portal_config.connected_date = timezone.now()
        portal_config.save()

        return Response({
            "message": "Foundit connected successfully",
            "is_connected": True
        })

class PostToFounditView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        jd_id = request.data.get("jd_id")
        jd = get_object_or_404(TblJobDescription, pk=jd_id)
        
        config = ImplementerPortalConfig.objects.filter(
            implementer=jd.implementer, 
            portal="foundit", 
            is_connected=True
        ).first()

        if not config:
            return Response({"error": "Foundit not connected"}, status=400)

        text = f"We are hiring: {jd.job_title}\nLocation: {jd.job_location}"

        try:
            from .utils import post_to_foundit
            result = post_to_foundit(config, jd, text)
            
            # Record the post
            from .models import FounditJobPost
            FounditJobPost.objects.create(
                jd=jd,
                job_id=result.get("foundit_job_id"),
                posted_by=request.user
            )
            return Response({"message": "Successfully posted to Foundit"})
        except Exception as e:
            return Response({"error": str(e)}, status=400)
        
class CandidateProfileViewSet(ModelViewSet):
    queryset = TblCandidateProfile.objects.all().order_by("-id")
    serializer_class = TblCandidateProfileSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = (MultiPartParser, FormParser)
 
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
 
        if serializer.is_valid():
            candidate = serializer.save(
                created_by=request.user.username
            )
 
            return Response(
                {
                    "message": "Candidate Profile created successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_201_CREATED
            )
 
        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )
 
    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(
            instance,
            data=request.data,
            partial=True
        )
 
        if serializer.is_valid():
            candidate = serializer.save(
                updated_by=request.user.username
            )
 
            return Response(
                {
                    "message": "Candidate Profile Updated successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_200_OK
            )
 
        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )
   
class CandidateResumeViewSet(ModelViewSet):
    queryset = TblCandidateResume.objects.all().order_by("-resume_id")
    serializer_class = TblCandidateResumeSerializer
    permission_classes = [IsAuthenticated]
 
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
 
        if serializer.is_valid():
            candidate = serializer.save(
                created_by=request.user.username
            )
 
            return Response(
                {
                    "message": "Candidate Resume created successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_201_CREATED
            )
 
        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )
 
    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(
            instance,
            data=request.data,
            partial=True
        )
 
        if serializer.is_valid():
            candidate = serializer.save(
                updated_by=request.user.username
            )
 
            return Response(
                {
                    "message": "Candidate Resume updated successfully",
                    "data": serializer.data,
                },
                status=status.HTTP_200_OK
            )
 
        return Response(
            serializer.errors,
            status=status.HTTP_400_BAD_REQUEST
        )

class ResumeUploadViewSet(ViewSet):
    permission_classes = (IsAuthenticated,)
    parser_classes = (MultiPartParser, FormParser)

    def process_file_content(self, file_path, extension):
        if extension == ".pdf":
            try:
                pdf_text = DocumentTextExtraction.process_pdf(file_path)  
                return pdf_text
            except Exception as e:
                raise Exception(f"PDF Error: {str(e)}")
        elif extension in [".docx", ".doc"]:
            try:
                doc_text = DocumentTextExtraction.process_word(file_path)
                return doc_text
            except Exception as e:
                raise Exception(f"Word Doc Error: {str(e)}")
        elif extension == ".txt":
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                return f.read()
        return ""

    def extract_json(self, text):
        cleaned = re.sub(r"```json|```", "", text, flags=re.IGNORECASE).strip()
        return json.loads(cleaned)

    def flatten_data_for_db(self, data):
        """
        Maps the Complex Nested JSON from the Prompt to the Flat DB Structure in TblCandidateResume
        """
        personal = data.get("personal_information", {})
        skills = data.get("skills", {})
        work_exp = data.get("work_experience", [])
        education = data.get("education", [])
        certifications = data.get("certifications", [])
        projects = data.get("projects", [])

        company_details = []
        if work_exp and len(work_exp) > 0:
            company_details = work_exp

        education_details = []
        if education and len(education) > 0:
            education_details = education

        certification_details = []
        if certifications and len(certifications) > 0:
            certification_details = certifications

        projects_details = []
        if projects and len(projects) > 0:
            projects_details = projects

        return {
            "first_name": personal.get("first_name", ""),
            "last_name": personal.get("last_name", ""),
            "email": personal.get("email", ""),
            "mobile": personal.get("mobile", ""),
            "country_code": "Not Specified",
            "location": personal.get("location", ""),
            "total_exp": data.get("total_exp", ""),
            "salary": data.get("salary", ""),
            "company": company_details,
            "title": data.get("title", ""),
            "primary_skills": data.get("primary_skills", []),
            'technical_skills': skills.get("technical_skills", []),
            'tools_and_frameworks': skills.get("tools_and_frameworks", []),
            "soft_skills": skills.get("soft_skills", []),
            "domain_skills": data.get("domain_skills", []),
            "education": education_details,
            "certifications": certification_details,
            "projects": projects_details,
            "responsibility_summary": data.get("responsibility_summary", ""),
            "age": personal.get("age", ""),
            "gender": personal.get("gender", ""),
            "notice_period": personal.get("notice_period", ""),
            "employment_type": personal.get("employment_type", "")
        }
    
    # @action(detail=False, methods=["post"], parser_classes=[MultiPartParser, FormParser])
    # def check_resume_similarity(self, request):
    #     """
    #     Upload file -> extract text -> detect candidate(email) -> compare with latest resume
    #     Returns similarity % and existing resume_id if found
    #     """
    #     uploaded_file = request.FILES.get("file")
    #     if not uploaded_file:
    #         return Response({"detail": "file is required"}, status=400)

    #     resume_base_dir = os.path.join(settings.MEDIA_ROOT, "Resumes")
    #     os.makedirs(resume_base_dir, exist_ok=True)
    #     fs = FileSystemStorage(location=resume_base_dir)
    #     filename = fs.save(uploaded_file.name, uploaded_file)
    #     file_path = fs.path(filename)
    #     extension = os.path.splitext(filename)[1].lower()

    #     content = self.process_file_content(file_path, extension)
    #     if not content:
    #         return Response({"detail": "Could not extract text"}, status=400)

    #     airecruit_llm = AIEditLLM()
    #     generation_config = {
    #         "temperature": 0.2,
    #         "thinking_budget": 32768,
    #     }
    #     airecruit_llm.create_chat(
    #         generation_config["temperature"], generation_config["thinking_budget"]
    #     )
        
    #     resume_exract_prompt = LlmPrompts.resume_extraction_prompt(content)
    #     resume_llm_response = airecruit_llm.send_message(resume_exract_prompt)
    #     print("LLM Response:", resume_llm_response.text)
        
    #     raw_json = self.extract_json(resume_llm_response.text)
        
    #     db_data = self.flatten_data_for_db(raw_json)
    #     if not db_data.get("first_name") or db_data.get("first_name") == "Not Specified":
    #         raise Exception("First Name of Candidate is Missing from LLM Response")
        
    #     if not db_data.get("mobile") or db_data.get("mobile") == "Not Specified":
    #         raise Exception("Mobile Number of Candidate is Missing from LLM Response")
        
    #     if not db_data.get("email") or db_data.get("email") == "Not Specified":
    #         raise Exception("E-Mail of Candidate is Missing from LLM Response")

    #     email = (db_data.get("email") or "").strip().lower()
    #     if not email:
    #         return Response({"detail": "Email not found in resume"}, status=400)

    #     profile = TblCandidateProfile.objects.filter(email__iexact=email).first()
    #     if not profile:
    #         return Response({
    #             "candidate_exists": False,
    #             "similarity_percent": 0,
    #             "message": "New candidate. No existing resume found.",
    #             "db_data": db_data,
    #             "temp_file_name": filename,
    #             "email": email
    #         })

    #     existing_resumes = TblCandidateResume.objects.filter(candidate_id=profile).order_by("-created_dt")
    #     if not existing_resumes.exists():
    #         return Response({
    #             "candidate_exists": True,
    #             "has_existing_resume": False,
    #             "similarity_percent": 0,
    #             "message": "Candidate exists but no resumes found.",
    #             "db_data": db_data,
    #             "temp_file_name": filename,
    #             "email": email,
    #             "candidate_id": profile.pk
    #         })

    #     latest_resume = existing_resumes.first()
    #     existing_text = latest_resume.parsed_text or ""
    #     new_text = content or ""

    #     vectorizer = TfidfVectorizer(stop_words="english")
    #     tfidf = vectorizer.fit_transform([existing_text, new_text])
    #     sim = cosine_similarity(tfidf[0:1], tfidf[1:2])[0][0]
    #     similarity_percent = round(sim * 100, 2)

    #     return Response({
    #         "candidate_exists": True,
    #         "has_existing_resume": True,
    #         "candidate_id": profile.pk,
    #         "existing_resume_id": latest_resume.pk,
    #         "similarity_percent": similarity_percent,
    #         "temp_file_name": filename,
    #         "email": email,
    #         "db_data": db_data,
    #         "parsed_text": content,
    #         "message": "Existing resume found"
    #     }, status=status.HTTP_200_OK)

    @action(detail=False, methods=["post"], parser_classes=[MultiPartParser, FormParser])
    def check_resume_similarity(self, request):
        """
        Upload file -> extract text -> detect candidate(email) -> compare with latest resume
        New Logic:
        1) If title different => force NEW resume
        2) If similarity very low => ask confirmation (update or new)
        """
        uploaded_file = request.FILES.get("file")
        if not uploaded_file:
            return Response({"detail": "file is required"}, status=400)

        resume_base_dir = os.path.join(settings.MEDIA_ROOT, "Resumes")
        os.makedirs(resume_base_dir, exist_ok=True)

        fs = FileSystemStorage(location=resume_base_dir)
        filename = fs.save(uploaded_file.name, uploaded_file)
        file_path = fs.path(filename)

        extension = os.path.splitext(filename)[1].lower()

        content = self.process_file_content(file_path, extension)
        if not content:
            return Response({"detail": "Could not extract text"}, status=400)

        airecruit_llm = AIEditLLM()
        generation_config = {"temperature": 0.2, "thinking_budget": 32768}
        airecruit_llm.create_chat(
            generation_config["temperature"],
            generation_config["thinking_budget"]
        )

        resume_extract_prompt = LlmPrompts.resume_extraction_prompt(content)
        resume_llm_response = airecruit_llm.send_message(resume_extract_prompt)

        raw_json = self.extract_json(resume_llm_response.text)
        print("LLM Response", resume_llm_response.text)
        db_data = self.flatten_data_for_db(raw_json)

        if not db_data.get("first_name") or db_data.get("first_name") == "Not Specified":
            return Response({"detail": "First Name missing"}, status=400)

        if not db_data.get("mobile") or db_data.get("mobile") == "Not Specified":
            return Response({"detail": "Mobile missing"}, status=400)

        if not db_data.get("email") or db_data.get("email") == "Not Specified":
            return Response({"detail": "Email missing"}, status=400)
        
        if not db_data.get("primary_skills"):
            return Response({"detail": "Primary Skills"}, status=400)

        email = (db_data.get("email") or "").strip().lower()
        if not email:
            return Response({"detail": "Email not found in resume"}, status=400)

        profile = TblCandidateProfile.objects.filter(email__iexact=email).first()

        if not profile:
            return Response({
                "candidate_exists": False,
                "similarity_percent": 0,
                "message": "New candidate. No existing resume found.",
                "db_data": db_data,
                "temp_file_name": filename,
                "email": email
            }, status=200)

        existing_resumes = TblCandidateResume.objects.filter(candidate_id=profile).order_by("-created_dt")
        if not existing_resumes.exists():
            return Response({
                "candidate_exists": True,
                "has_existing_resume": False,
                "similarity_percent": 0,
                "message": "Candidate exists but no resumes found.",
                "db_data": db_data,
                "temp_file_name": filename,
                "email": email,
                "candidate_id": profile.pk
            }, status=200)
        
        new_title_raw = db_data.get("title") or ""
        new_title = normalize_resume_title(new_title_raw)

        matched_resume = next(
            (
                resume for resume in existing_resumes
                if normalize_resume_title(resume.title or "") == new_title
            ),
            None
        )

        if not matched_resume:
            return Response({
                "candidate_exists": True,
                "has_existing_resume": True,
                "candidate_id": profile.pk,
                "similarity_percent": 0,
                "temp_file_name": filename,
                "email": email,
                "db_data": db_data,
                "message": "Resume title is different. Create NEW resume.",
                "action_required": "new",
                "new_title_raw": new_title_raw,
                "new_title_normalized": new_title,
            }, status=200)

        existing_text = matched_resume.parsed_text or ""
        new_text = content or ""

        vectorizer = TfidfVectorizer(stop_words="english")
        tfidf = vectorizer.fit_transform([existing_text, new_text])
        sim = cosine_similarity(tfidf[0:1], tfidf[1:2])[0][0]
        similarity_percent = round(sim * 100, 2)

        LOW_SIMILARITY_THRESHOLD = 30 

        if similarity_percent < LOW_SIMILARITY_THRESHOLD:
            return Response({
                "candidate_exists": True,
                "has_existing_resume": True,
                "candidate_id": profile.pk,
                "existing_resume_id": matched_resume.pk,
                "similarity_percent": similarity_percent,
                "temp_file_name": filename,
                "email": email,
                "db_data": db_data,
                "message": "Similarity score is very low. Ask confirmation: Update existing OR Create new.",
                "action_required": "confirm"
            }, status=200)

        return Response({
            "candidate_exists": True,
            "has_existing_resume": True,
            "candidate_id": profile.pk,
            "existing_resume_id": matched_resume.pk,
            "similarity_percent": similarity_percent,
            "temp_file_name": filename,
            "email": email,
            "db_data": db_data,
            "parsed_text": content,
            "message": "Existing resume found (similar).",
            "action_required": "duplicate"
        }, status=200)
    
    @action(detail=False, methods=["post"], parser_classes=[JSONParser, FormParser, MultiPartParser])
    def upload_resume_confirm(self, request):
        temp_file_name = request.data.get("temp_file_name")
        choice = request.data.get("choice")
        db_data = request.data.get("db_data") 

        if not temp_file_name or not choice or not db_data:
            return Response({"detail": "temp_file_name, choice and db_data required"}, status=400)

        file_path = os.path.join(settings.MEDIA_ROOT, "Resumes", temp_file_name)
        if not os.path.exists(file_path):
            return Response({"detail": "Temp file not found"}, status=400)

        extension = os.path.splitext(temp_file_name)[1].lower()

        content = self.process_file_content(file_path, extension)
        if not content:
            return Response({"detail": "Could not extract text"}, status=400)

        email = (db_data.get("email") or "").strip().lower()
        profile = TblCandidateProfile.objects.filter(email__iexact=email).first()

        resume_relative_path = f"Resumes/{temp_file_name}"
        resume_file_url = settings.MEDIA_URL + resume_relative_path

        if not profile:
            profile = TblCandidateProfile.objects.create(
                first_name=db_data["first_name"],
                last_name=db_data["last_name"],
                email=db_data["email"],
                mobile_number=db_data["mobile"],
                coutry_code=db_data["country_code"] if db_data["country_code"] else "",
                age=db_data["age"],
                gender=db_data["gender"],
                notice_period=db_data["notice_period"],
                employment_type=db_data["employment_type"],
                candidate_status="Active",
                created_by=request.user.username
            )
        else:
            profile.first_name = db_data["first_name"]
            profile.last_name = db_data["last_name"]
            profile.mobile_number = db_data["mobile"]
            profile.coutry_code = db_data["country_code"] or ""
            profile.age = db_data["age"]
            profile.gender = db_data["gender"]
            profile.notice_period = db_data["notice_period"]
            profile.employment_type = db_data["employment_type"]
            profile.updated_by = request.user.username

            profile.save()

        airecruit_llm = AIEditLLM()

        resume_responsibilities_embedding = airecruit_llm.generate_embeddings(db_data.get("responsibility_summary"))
        if choice == "update":
            existing_resume = TblCandidateResume.objects.filter(candidate_id=profile).order_by("-created_dt").first()
            if existing_resume:
                existing_resume.title = db_data["title"]
                existing_resume.totalexp = db_data["total_exp"]
                existing_resume.company = db_data["company"]
                existing_resume.salary = db_data["salary"]
                existing_resume.location = db_data["location"]
                existing_resume.primary_skills = db_data["primary_skills"]
                existing_resume.technical_skills = db_data["technical_skills"]
                existing_resume.tools_and_frameworks = db_data["tools_and_frameworks"]
                existing_resume.soft_skills = db_data["soft_skills"]
                existing_resume.domain_skills = db_data["domain_skills"]
                existing_resume.education = db_data["education"]
                existing_resume.projects = db_data["projects"]
                existing_resume.certifications = db_data["certifications"]
                existing_resume.responsibility_summary = db_data["responsibility_summary"]
                existing_resume.resume_responsibilities_summary_embedding = resume_responsibilities_embedding if resume_responsibilities_embedding else None
                existing_resume.parsed_text = content
                existing_resume.resume_file = resume_file_url
                existing_resume.updated_by = request.user.username
                existing_resume.save()

                return Response({
                    "status": "success",
                    "message": "Existing resume updated successfully",
                    "resume_id": existing_resume.pk,
                    "candidate_id": profile.pk,
                    "choice": "update"
                }, status=200)

        new_resume = TblCandidateResume.objects.create(
            candidate_id=profile,
            title=db_data["title"],
            totalexp=db_data["total_exp"],
            company=db_data["company"],
            salary=db_data["salary"],
            location=db_data["location"],
            email=db_data["email"],
            mobile_number=db_data["mobile"],
            primary_skills=db_data["primary_skills"],
            technical_skills=db_data["technical_skills"],
            tools_and_frameworks=db_data["tools_and_frameworks"],
            soft_skills=db_data["soft_skills"],
            domain_skills=db_data["domain_skills"],
            education=db_data["education"],
            projects=db_data["projects"],
            certifications=db_data["certifications"],
            responsibility_summary=db_data["responsibility_summary"],
            resume_responsibilities_summary_embedding=resume_responsibilities_embedding if resume_responsibilities_embedding else None,
            resume_file=resume_file_url,
            resume_source="Internal DB",
            parsed_text=content,
            created_by=request.user.username,
            status="Parsed"
        )

        try:
            match_response = run_matching_engine_candidate(new_resume.resume_id)
            matches = match_response.get("matches", {})
        except Exception as e:
            matches = {}
        
        if matches:
            manager_sending_emails = list(
                User.objects
                .filter(groups__name="Delivery Manager (DM)", is_active=True)
                .exclude(email__isnull=True)
                .exclude(email__exact="")
                .values_list("email", flat=True)
            )

            super_admin_cc_emails = list(
                User.objects
                .filter(groups__name="Implementer Super Admin", is_active=True)
                .exclude(email__isnull=True)
                .exclude(email__exact="")
                .values_list("email", flat=True)
            )

            subject, body = get_email_template("candidate_jd_match_notification", is_html=False)

            candidate_name = f"{profile.first_name} {profile.last_name}"

            subject = subject.format(candidate_name=candidate_name)

            body = body.format(
                resume_id=new_resume.resume_id,
                candidate_name=candidate_name,
                upload_date=new_resume.created_dt if hasattr(new_resume, "created_dt") else "N/A",
                match_count=len(matches)
            )

            for to_mail in manager_sending_emails:
                send_email_safe(
                    subject,
                    body,
                    to_mail,
                    settings.EMAIL_HOST_USER,
                    cc_emails=super_admin_cc_emails,
                    is_html=False
                )

        return Response({
            "status": "success",
            "message": "New resume uploaded successfully",
            "resume_id": new_resume.pk,
            "candidate_id": profile.pk,
            "choice": "new"
        }, status=201)
    
    @action(detail=False, methods=["post"], parser_classes=[JSONParser])
    def compare_temp_with_existing(self, request):
        existing_resume_id = request.data.get("existing_resume_id")
        temp_file_name = request.data.get("temp_file_name")

        if not existing_resume_id or not temp_file_name:
            return Response({"detail": "existing_resume_id and temp_file_name required"}, status=400)

        resume1 = get_object_or_404(TblCandidateResume, pk=existing_resume_id)

        file_path = os.path.join(settings.MEDIA_ROOT, "Resumes", temp_file_name)
        if not os.path.exists(file_path):
            return Response({"detail": "Temp file not found"}, status=400)

        extension = os.path.splitext(temp_file_name)[1].lower()
        text2 = self.process_file_content(file_path, extension) or ""
        text1 = resume1.parsed_text or ""

        lines1 = text1.splitlines()
        lines2 = text2.splitlines()

        lower1 = [l.strip().lower() for l in lines1]
        lower2 = [l.strip().lower() for l in lines2]

        diff = difflib.SequenceMatcher(None, lower1, lower2)

        v1_html = []
        v2_html = []

        def wrap(text, bg):
            return f"""
            <div style="
                background:{bg};
                padding:4px 8px;
                border-radius:6px;
                margin:2px 0;
                font-size:13px;
                line-height:1.5;
                color:#111;
                white-space:pre-wrap;
            ">{text}</div>
            """

        for tag, i1, i2, j1, j2 in diff.get_opcodes():
            if tag == "equal":
                for a, b in zip(lines1[i1:i2], lines2[j1:j2]):
                    v1_html.append(wrap(a, "#ffffff"))
                    v2_html.append(wrap(b, "#ffffff"))

            elif tag == "replace":
                old_block = lines1[i1:i2]
                new_block = lines2[j1:j2]

                max_len = max(len(old_block), len(new_block))

                for k in range(max_len):
                    old_line = old_block[k] if k < len(old_block) else ""
                    new_line = new_block[k] if k < len(new_block) else ""

                    old_html, new_html = highlight_inline_diff(old_line, new_line)

                    v1_html.append(wrap(old_html, "#ffffff"))
                    v2_html.append(wrap(new_html, "#ffffff"))

            elif tag == "delete":
                for line in lines1[i1:i2]:
                    v1_html.append(wrap(line, "#dbeafe"))
                for _ in lines1[i1:i2]:
                    v2_html.append(wrap("&nbsp;", "#ffffff"))

            elif tag == "insert":
                for _ in lines2[j1:j2]:
                    v1_html.append(wrap("&nbsp;", "#ffffff"))
                for line in lines2[j1:j2]:
                    v2_html.append(wrap(line, "#dcfce7"))

        return Response({
            "v1_html": "".join(v1_html),
            "v2_html": "".join(v2_html),
        })
        
    parser_classes = [JSONParser, FormParser, MultiPartParser]

    @action(detail=True, methods=["post"])
    def get_side_by_side_diff(self, request, pk=None):
        v1_id = request.data.get("v1_id")
        v2_id = request.data.get("v2_id")

        if not v1_id or not v2_id:
            return Response({"detail": "v1_id and v2_id are required"}, status=400)

        resume1 = get_object_or_404(TblCandidateResume, pk=v1_id)
        resume2 = get_object_or_404(TblCandidateResume, pk=v2_id)

        text1 = resume1.parsed_text or ""
        text2 = resume2.parsed_text or ""

        lines1 = text1.splitlines()
        lines2 = text2.splitlines()

        lower1 = [l.strip().lower() for l in lines1]
        lower2 = [l.strip().lower() for l in lines2]

        diff = difflib.SequenceMatcher(None, lower1, lower2)

        v1_html = []
        v2_html = []

        def wrap(text, bg):
            return f"""
            <div style="
                background:{bg};
                padding:4px 8px;
                border-radius:6px;
                margin:2px 0;
                font-size:13px;
                line-height:1.5;
                color:#111;
                white-space:pre-wrap;
            ">{text}</div>
            """

        for tag, i1, i2, j1, j2 in diff.get_opcodes():
            if tag == "equal":
                for a, b in zip(lines1[i1:i2], lines2[j1:j2]):
                    v1_html.append(wrap(a, "#ffffff"))
                    v2_html.append(wrap(b, "#ffffff"))

            elif tag == "replace":
                old_block = lines1[i1:i2]
                new_block = lines2[j1:j2]

                max_len = max(len(old_block), len(new_block))

                for k in range(max_len):
                    old_line = old_block[k] if k < len(old_block) else ""
                    new_line = new_block[k] if k < len(new_block) else ""

                    old_html, new_html = highlight_inline_diff(old_line, new_line)

                    v1_html.append(wrap(old_html, "#ffffff"))
                    v2_html.append(wrap(new_html, "#ffffff"))

            elif tag == "delete":
                for line in lines1[i1:i2]:
                    v1_html.append(wrap(line, "#dbeafe"))
                for _ in lines1[i1:i2]:
                    v2_html.append(wrap("&nbsp;", "#ffffff"))

            elif tag == "insert":
                for _ in lines2[j1:j2]:
                    v1_html.append(wrap("&nbsp;", "#ffffff"))
                for line in lines2[j1:j2]:
                    v2_html.append(wrap(line, "#dcfce7"))

        return Response({
            "v1_html": "".join(v1_html),
            "v2_html": "".join(v2_html)
        })
        
    def destroy(self, request, pk=None):
        try:
            profile = get_object_or_404(TblCandidateProfile, pk=pk)
            
            resumes = TblCandidateResume.objects.filter(candidate_id=profile)
            for resume in resumes:
                if resume.resume_file:
                    file_path = os.path.join(settings.MEDIA_ROOT, str(resume.resume_file))
                    if os.path.exists(file_path):
                        os.remove(file_path)
                resume.delete()
            
            profile.delete()
            
            return Response({"status": "success", "message": "Candidate deleted successfully"}, status=200)
        except Exception as e:
            return Response({"status": "failed", "error": str(e)}, status=500)
        
@api_view(["GET"])
@permission_classes([AllowAny])
def public_job_detail(request, token):
    print("apply_token_url", token)
    jd = get_object_or_404(
        TblJobDescription,
        apply_token_url=token,
        status="Open"
    )

    serializer = TblJobDescriptionSerializer(jd)
    return Response(serializer.data)

@api_view(["POST"])
@permission_classes([AllowAny])
def public_apply_job(request):
    try:
        jd_display_id = request.data.get("jd_id")
        full_name = request.data.get("name")
        email = request.data.get("email")
        mobile = request.data.get("mobile")
        resume_file = request.FILES.get("resume")

        if not all([jd_display_id, full_name, email, mobile, resume_file]):
            return Response(
                {"error": "All fields are required"},
                status=400
            )

        jd = get_object_or_404(
            TblJobDescription,
            jd_display_id=jd_display_id,
            status__in=["Open", "Created", "Posted"]
        )

        application = JobApplication.objects.create(
            jd=jd,
            full_name=full_name,
            email=email,
            mobile=mobile,
            resume=resume_file,
            source="Public"
        )

        resume_path = application.resume.path
        resume_url = application.resume.url
        extracted_text = ""

        if resume_path.lower().endswith(".pdf"):
            extracted_text = DocumentTextExtraction.process_pdf(resume_path)

        elif resume_path.lower().endswith((".docx", ".doc")):
            extracted_text = DocumentTextExtraction.process_word(resume_path)

        if not extracted_text.strip():
            return Response(
                {"error": "Could not extract resume text"},
                status=400
            )

        airecruit_llm = AIEditLLM()
        generation_config = {"temperature": 0.2, "thinking_budget": 32768}
        airecruit_llm.create_chat(
            generation_config["temperature"],
            generation_config["thinking_budget"]
        )

        prompt = LlmPrompts.resume_extraction_prompt(extracted_text)
        ai_response = airecruit_llm.send_message(prompt)

        parsed_json = MailReaderGoogle().extract_json_from_llm(ai_response.text)

        flat_data = MailReaderGoogle().flatten_data_for_db(parsed_json)

        candidate, _ = TblCandidateProfile.objects.get_or_create(
            email=email,
            defaults={
                "first_name": flat_data.get("first_name") or full_name.split(" ")[0],
                "last_name": flat_data.get("last_name", ""),
                "mobile_number": mobile,
                "source": "public",
                "candidate_status": "Active",
            }
        )

        existing_resumes = TblCandidateResume.objects.filter(candidate_id=candidate)

        matched_resume = None

        if flat_data.get("title") and existing_resumes.exists():
            normalize_title_text = normalize_resume_title(flat_data["title"])

            for resume in existing_resumes:
                existing_title = normalize_resume_title(resume.title)
                if existing_title == normalize_title_text:
                    matched_resume = resume
                    break

        resume_responsibilities_embedding = airecruit_llm.generate_embeddings(
            flat_data.get("responsibility_summary")
        )

        if matched_resume:
            matched_resume.location = flat_data.get("location", "")
            matched_resume.primary_skills = flat_data.get("primary_skills", [])
            matched_resume.technical_skills = flat_data.get("technical_skills", [])
            matched_resume.tools_and_frameworks = flat_data.get("tools_and_frameworks", [])
            matched_resume.soft_skills = flat_data.get("soft_skills", [])
            matched_resume.domain_skills = flat_data.get("domain_skills", [])
            matched_resume.education = flat_data.get("education", [])
            matched_resume.certifications = flat_data.get("certifications", [])
            matched_resume.projects = flat_data.get("projects", [])
            matched_resume.parsed_text = extracted_text
            matched_resume.resume_file = resume_url
            matched_resume.responsibility_summary = flat_data.get("responsibility_summary")
            matched_resume.resume_responsibilities_summary_embedding = (
                resume_responsibilities_embedding if resume_responsibilities_embedding else None
            )
            matched_resume.resume_source = "public"
            matched_resume.updated_by = "public_apply"
            matched_resume.save()

        else:
            resume_obj = TblCandidateResume.objects.create(
                candidate_id=candidate,
                email=email,
                title=flat_data.get("title", ""),
                location=flat_data.get("location", ""),
                primary_skills=flat_data.get("primary_skills", []),
                technical_skills=flat_data.get("technical_skills", []),
                tools_and_frameworks=flat_data.get("tools_and_frameworks", []),
                soft_skills=flat_data.get("soft_skills", []),
                domain_skills=flat_data.get("domain_skills", []),
                parsed_text=extracted_text,
                education=flat_data.get("education", []),
                certifications=flat_data.get("certifications", []),
                projects=flat_data.get("projects", []),
                resume_file=resume_url,
                responsibility_summary=flat_data.get("responsibility_summary"),
                resume_responsibilities_summary_embedding=(
                    resume_responsibilities_embedding if resume_responsibilities_embedding else None
                ),
                resume_source="public",
                status="Parsed",
                created_by="public_apply"
            )

            try:
                match_response = run_matching_engine_candidate(resume_obj.resume_id)
                matches = match_response.get("matches", {})
            except Exception as e:
                matches = {}
            
            if matches:
                manager_sending_emails = list(
                    User.objects
                    .filter(groups__name="Delivery Manager (DM)", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                super_admin_cc_emails = list(
                    User.objects
                    .filter(groups__name="Implementer Super Admin", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                subject, body = get_email_template("candidate_jd_match_notification", is_html=False)

                candidate_name = f"{candidate.first_name} {candidate.last_name}"

                subject = subject.format(candidate_name=candidate_name)

                body = body.format(
                    resume_id=resume_obj.resume_id,
                    candidate_name=candidate_name,
                    upload_date=resume_obj.created_dt if hasattr(resume_obj, "created_dt") else "N/A",
                    match_count=len(matches)
                )

                for to_mail in manager_sending_emails:
                    send_email_safe(
                        subject,
                        body,
                        to_mail,
                        settings.EMAIL_HOST_USER,
                        cc_emails=super_admin_cc_emails,
                        is_html=False
                    )

        send_resume_ack_email(
            to_email=email,
            candidate_name=full_name,
            job_title=jd.job_title
        )

        return Response({"success": True}, status=201)

    except Exception as e:
        import traceback
        traceback.print_exc()
        return Response(
            {"error": "Resume processing failed"},
            status=500
        )
    
class IdentifyMatchingProfiles(APIView):
    def normalize_jd_experience(self, text):
        """
        Normalize JD experience into (min_years, max_years)
        """
        if not text:
            return None, None

        text = text.lower().strip()
        text = re.sub(r"(years|year|yrs|yr)", "", text).strip()

        # 1️⃣ Range: 2-4
        match = re.search(r"(\d+(\.\d+)?)\s*-\s*(\d+(\.\d+)?)", text)
        if match:
            return float(match.group(1)), float(match.group(3))

        # 2️⃣ Plus: 2+
        match = re.search(r"(\d+(\.\d+)?)\s*\+", text)
        if match:
            return float(match.group(1)), math.inf

        # 3️⃣ Over / More than
        match = re.search(r"(over|more than)\s*(\d+(\.\d+)?)", text)
        if match:
            return float(match.group(2)), math.inf

        # 4️⃣ Within / Up to
        match = re.search(r"(within|upto|up to)\s*(\d+(\.\d+)?)", text)
        if match:
            return 0.0, float(match.group(2))

        # 5️⃣ Single value: 2 or 2.4 → (2,3)
        match = re.search(r"(\d+(\.\d+)?)", text)
        if match:
            min_val = int(float(match.group(1)))
            return min_val, min_val + 1

        return None, None
    
    def normalize_candidate_experience(self, text):
        """
        Normalize candidate experience to a SINGLE value
        """
        if not text:
            return None

        text = text.lower().strip()

        if any(keyword in text for keyword in ["fresher", "entry level", "entry-level", "junior"]):
            return 0
        
        if any(keyword in text for keyword in ["not specified"]):
            return None

        text = re.sub(r"(years|year|yrs|yr)", "", text)
        text = re.sub(r"[^\d\.\+\-\s]", "", text)
        text = text.strip()

        # Extract first number only
        match = re.search(r"(\d+(\.\d+)?)", text)
        if not match:
            return None

        value = float(match.group(1))
        return int(value)
    
    def is_years_match(self, jd_exp_text, candidate_exp_text):
        jd_min, jd_max = self.normalize_jd_experience(jd_exp_text)
        candidate_years = self.normalize_candidate_experience(candidate_exp_text)

        if jd_min is None or candidate_years is None:
            return False

        if candidate_years < jd_min:
            return False

        if jd_max is not None and jd_max != math.inf and candidate_years > jd_max:
            return False

        return True
    
    def normalize_skill(self, skill):
        return skill.lower().strip()
    
    def skill_match(self, jd_skill, candidate_skills, skill_matrix_data, TRIGRAM_THRESHOLD=0.4):
        jd_skill = self.normalize_skill(jd_skill)
        print(f"\n[JD SKILL] Checking JD skill → '{jd_skill}'")
        print(f"[CANDIDATE SKILLS] {candidate_skills}")

        for cand_skill in candidate_skills:
            cand_skill = self.normalize_skill(cand_skill)
            for skill_name, alias_data in skill_matrix_data.items():
                for alias in alias_data:
                    if cand_skill == self.normalize_skill(alias):
                        cand_skill = skill_name
                        break

            print(f"  ↳ Comparing with candidate skill → '{cand_skill}'")

            # 1️⃣ Exact match
            if cand_skill == jd_skill:
                print("    ✅ EXACT MATCH FOUND")
                return True

            # 3️⃣ Trigram similarity
            similarity_qs = (
                TblCandidateResume.objects
                .annotate(
                    sim=TrigramSimilarity(
                        Value(cand_skill),
                        Value(jd_skill)
                    )
                )
                .values("sim")[:1]
            )

            similarity_value = similarity_qs[0]["sim"] if similarity_qs else None

            print(
                f"    🔍 TRIGRAM SCORE between "
                f"'{jd_skill}' and '{cand_skill}' = {similarity_value}"
            )

            if similarity_value is not None and similarity_value >= TRIGRAM_THRESHOLD:
                print(
                    f"    ✅ TRIGRAM MATCH ACCEPTED "
                    f"(>= {TRIGRAM_THRESHOLD})"
                )
                return True
            else:
                print(
                    f"    ❌ TRIGRAM MATCH REJECTED "
                    f"(< {TRIGRAM_THRESHOLD})"
                )

        print("  ❌ NO MATCH FOUND FOR THIS JD SKILL")
        return False
    
    def jd_skill_matches_candidate(self, jd_skill, candidate, skill_matrix_data):
        print(
            f"\n[CHECKING JD SKILL AGAINST CANDIDATE] "
            f"Resume ID: {candidate.resume_id}"
        )

        print("▶ Trying PRIMARY skills")
        if self.skill_match(jd_skill, candidate.primary_skills, skill_matrix_data):
            print("✅ MATCH FOUND IN PRIMARY SKILLS")
            return True

        print("▶ Trying TECHNICAL skills (fallback)")
        if self.skill_match(jd_skill, candidate.technical_skills, skill_matrix_data):
            print("✅ MATCH FOUND IN TECHNICAL SKILLS")
            return True
        
        print("▶ Trying SOFT skills (fallback)")
        if self.skill_match(jd_skill, candidate.soft_skills, skill_matrix_data):
            print("✅ MATCH FOUND IN TECHNICAL SKILLS")
            return True

        print("❌ NO MATCH IN PRIMARY OR TECHNICAL SKILLS")
        return False
    
    def calculate_skill_score(self, jd_skills, candidate, skill_matrix_data):
        print(
            f"\n=============================="
            f"\nResume ID: {candidate.resume_id}"
            f"\nJD Skills: {jd_skills}"
            f"\n=============================="
        )

        if not jd_skills:
            return {
                "matched": 0,
                "total": 0,
                "percentage": 0.0,
                "score": 0.0
            }

        matched = 0

        for jd_skill in jd_skills:
            print(f"\n➡️ Evaluating JD Skill: '{jd_skill}'")

            normalized_jd_skill = self.normalize_skill(jd_skill)
            for skill_name, alias_data in skill_matrix_data.items():
                for alias in alias_data:
                    if normalized_jd_skill == self.normalize_skill(alias):
                        jd_skill = skill_name
                        break

            if self.jd_skill_matches_candidate(jd_skill, candidate, skill_matrix_data):
                matched += 1
                print(f"✅ JD Skill '{jd_skill}' MATCHED")
            else:
                print(f"❌ JD Skill '{jd_skill}' NOT MATCHED")

        total = len(jd_skills)
        percentage = (matched / total) * 100
        score = matched / total

        # print(
        #     f"\n📊 FINAL SKILL SCORE for Resume {candidate.resume_id}"
        #     f"\nMatched: {matched}/{total}"
        #     f"\nPercentage: {percentage:.2f}%"
        #     f"\nScore: {score:.4f}"
        # )

        return {
            "matched": matched,
            "total": total,
            "percentage": round(percentage, 2),
            "score": round(score, 4)
        }
    
    def calculate_overall_score(self, match_data, base_weights):
        valid_fields = {
            field: weight
            for field, weight in base_weights.items()
            if weight is not None and match_data.get(field) is not None
        }

        if not valid_fields:
            return 0.0

        total_weight = sum(valid_fields.values())
        normalized_weights = {
            field: weight / total_weight
            for field, weight in valid_fields.items()
        }

        overall_score = 0.0
        for field, weight in normalized_weights.items():
            value = match_data[field]

            if isinstance(value, bool):
                score_value = 1.0 if value else 0
            else:
                score_value = float(value)

            overall_score += score_value * weight

        return round(overall_score, 4)

    def parse_salary(self, text):
        if not text:
            return (None, None)

        text = text.lower().strip()

        # Ignore junk cases
        if text in ["not specified", "negotiable", "as per company standards"]:
            return (None, None)

        # Remove commas and currency symbols
        text = re.sub(r"[₹,$,]", "", text)

        multiplier = 1

        # Detect LPA / lakh
        if "lpa" in text or "lakh" in text or "lakhs" in text:
            multiplier = 100000

        # Detect monthly
        elif "per month" in text or "monthly" in text or "pm" in text:
            multiplier = 12

        # Detect thousand (k)
        elif "k" in text:
            multiplier = 1000

        # Extract numbers (supports decimals)
        numbers = re.findall(r"\d+\.?\d*", text)

        if not numbers:
            return (None, None)

        values = [float(n) * multiplier for n in numbers]

        # Handle "above" / "minimum"
        if "above" in text or "minimum" in text or "+" in text:
            return (values[0], float("inf"))

        # Handle "upto" / "max"
        if "upto" in text or "up to" in text or "maximum" in text:
            return (0, values[0])

        # Range case
        if len(values) >= 2:
            return (min(values), max(values))

        # Single salary
        return (values[0], values[0])
    
    def salary_match(self, jd_salary, candidate_salary):
        jd_min, jd_max = self.parse_salary(jd_salary)
        c_min, c_max = self.parse_salary(candidate_salary)

        if jd_min is None or c_min is None:
            return None

        return not (c_max < jd_min or c_min > jd_max)
    
    def trigram_match(self, jd_data, candidate_data, TRIGRAM_THRESHOLD=0.4):
        jd_data = self.normalize_skill(jd_data)
        candidate_data = self.normalize_skill(candidate_data)

        # 1️⃣ Exact match
        if candidate_data == jd_data:
            print("    ✅ EXACT MATCH FOUND")
            return True

        # 3️⃣ Trigram similarity
        similarity_qs = (
            TblCandidateResume.objects
            .annotate(
                sim=TrigramSimilarity(
                    Value(candidate_data),
                    Value(jd_data)
                )
            )
            .values("sim")[:1]
        )

        similarity_value = similarity_qs[0]["sim"] if similarity_qs else None

        if similarity_value is not None and similarity_value >= TRIGRAM_THRESHOLD:
            print(
                f"    ✅ TRIGRAM MATCH ACCEPTED "
                f"(>= {TRIGRAM_THRESHOLD})"
            )
            return True
        else:
            print(
                f"    ❌ TRIGRAM MATCH REJECTED "
                f"(< {TRIGRAM_THRESHOLD})"
            )

        print("  ❌ NO MATCH FOUND FOR THIS JD SKILL")
        return False
    
    def parse_notice_period_to_days(self, value):
        if not value:
            return None

        text = str(value).lower().strip()

        # ✅ Candidate already serving notice → eligible directly
        if "serving" in text or "on notice" in text:
            return 0  # treat as immediate

        if text in ["not specified", "negotiable", "n/a"]:
            return None

        if "immediate" in text:
            return 0

        match = re.search(r"\d+", text)
        if not match:
            return None

        num = int(match.group())

        if "month" in text:
            return num * 30

        if "week" in text:
            return num * 7

        return num
    
    def keyword_match_score(self, jd_keywords, responsibilities_text):
        if not jd_keywords or not responsibilities_text:
            return 0.0

        text = responsibilities_text.lower()
        text = re.sub(r"[^a-z0-9\s]", " ", text)

        matched = 0

        for kw in jd_keywords:
            kw_clean = kw.strip().lower()

            if kw_clean and kw_clean in text:
                matched += 1

        total = len(jd_keywords)

        return round(matched / total, 3)

    def post(self, request):
        jd_id = request.data.get("jd_id")
        eligible_search_params = request.data.get("eligible_search_params", [])

        if not jd_id:
            return Response({"error": "jd_id is required"}, status=400)

        results = {}

        jd_data = TblJobDescription.objects.get(jd_id=jd_id)
        jd_responsibilities_embedding = jd_data.jd_responsibilities_embedding
        jd_keywords = jd_data.search_pattern["keywords"]
        jd_primary_skills = jd_data.search_pattern["primary_skills"]
        jd_tools_and_frameworks = jd_data.search_pattern["tools_and_frameworks"]
        jd_secondary_skills = jd_data.search_pattern["secondary_skills"]
        jd_education = jd_data.search_pattern["education"]
        jd_domain_requirements = (
            jd_data.domain_requirements.split(",")
            if jd_data.domain_requirements and jd_data.domain_requirements != "Not Specified"
            else []
        )

        jd_domain_requirements = [d.strip() for d in jd_domain_requirements if d.strip()]
        jd_location = jd_data.search_pattern.get("location", None)
        jd_salary_range = jd_data.search_pattern.get("salary_range", None)
        jd_age_range = jd_data.search_pattern.get("age_range", None)
        jd_gender_filter = jd_data.search_pattern.get("gender", None)
        jd_employment_type = jd_data.search_pattern.get("employment_type", None)
        jd_notice_period = jd_data.search_pattern.get("notice_period", None)
        jd_last_modified = jd_data.search_pattern.get("last_modified", None)

        candidates = TblCandidateResume.objects.all()

        skill_matrix_data = {}
        json_path = os.path.join(settings.BASE_DIR, "masters", "skill_matrix.json")
        with open(json_path, "r", encoding="utf-8") as f:
            skill_matrix_data = json.load(f)

        eligible_resume_ids = []

        for c in candidates:
            resume_id = str(c.resume_id)

            experience_match = self.is_years_match(
                jd_data.years_of_experience,
                c.totalexp
            )

            if experience_match:
                eligible_resume_ids.append(resume_id)

                results[resume_id] = {
                    "experience": True,
                }

        if eligible_resume_ids:
            if not jd_primary_skills or jd_primary_skills == "Not Specified" or "primary_skills" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["primary_skills"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    primary_skill_score = self.calculate_skill_score(
                        jd_primary_skills,
                        candidate_obj,
                        skill_matrix_data
                    )

                    print(primary_skill_score)
                    results[resume_id]["primary_skills"] = primary_skill_score["score"]

        if eligible_resume_ids:
            if not jd_keywords or jd_keywords == "Not Specified" or "keywords" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["keywords"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_responsibilities = candidate_obj.responsibility_summary

                    keywords_score = self.keyword_match_score(jd_keywords, candidate_responsibilities)

                    results[resume_id]["keywords"] = keywords_score

        for c in eligible_resume_ids:
            resume_id = str(c)
            if resume_id not in results:
                results[resume_id] = {}
            results[resume_id]["responsibilities"] = None

        if eligible_resume_ids and "responsibilities" in eligible_search_params:
            similarity_candidates = (
                TblCandidateResume.objects
                .filter(resume_id__in=eligible_resume_ids)
                .exclude(resume_responsibilities_summary_embedding=None)
                .annotate(
                    distance=CosineDistance(
                        F("resume_responsibilities_summary_embedding"),
                        jd_responsibilities_embedding
                    )
                )
                .annotate(
                    responsibilities_similarity=1 - F("distance")
                )
                .filter(responsibilities_similarity__isnull=False)
            )

            for c in similarity_candidates:
                resume_id = str(c.resume_id)
                similarity = c.responsibilities_similarity or 0.0

                results[resume_id]["responsibilities"] = round(
                    float(similarity), 4
                )

        if eligible_resume_ids:
            if not jd_tools_and_frameworks or jd_tools_and_frameworks == "Not Specified" or "tools_and_frameworks" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["tools_and_frameworks"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    matched_tools_skill = 0
                    for skill in jd_tools_and_frameworks:
                        if self.skill_match(skill, candidate_obj.tools_and_frameworks, skill_matrix_data):
                            matched_tools_skill += 1

                    tools_total = len(jd_tools_and_frameworks)
                    tools_percentage = (matched_tools_skill / tools_total) * 100
                    tools_score = matched_tools_skill / tools_total

                    tools_skills_result = {
                        "matched": matched_tools_skill,
                        "total": tools_total,
                        "percentage": round(tools_percentage, 2),
                        "score": round(tools_score, 4)
                    }

                    print(tools_skills_result)
                    results[resume_id]["tools_and_frameworks"] = tools_skills_result["score"]

        if eligible_resume_ids:
            if not jd_secondary_skills or jd_secondary_skills == "Not Specified" or "secondary_skills" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["secondary_skills"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    secondary_skills_score = self.calculate_skill_score(
                        jd_secondary_skills,
                        candidate_obj,
                        skill_matrix_data
                    )

                    print(secondary_skills_score)
                    results[resume_id]["secondary_skills"] = secondary_skills_score["score"]

        if eligible_resume_ids:
            if not jd_domain_requirements or jd_domain_requirements == "Not Specified" or "domain_requirements" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["domain_requirements"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    matched_domain_skill = 0
                    for skill in jd_domain_requirements:
                        if self.skill_match(skill, candidate_obj.domain_skills, skill_matrix_data):
                            matched_domain_skill += 1

                    domain_total = len(jd_domain_requirements)
                    domain_percentage = (matched_domain_skill / domain_total) * 100
                    domain_score = matched_domain_skill / domain_total

                    domain_requirements_score = {
                        "matched": matched_domain_skill,
                        "total": domain_total,
                        "percentage": round(domain_percentage, 2),
                        "score": round(domain_score, 4)
                    }

                    print(domain_requirements_score)
                    results[resume_id]["domain_requirements"] = domain_requirements_score["score"]

        if eligible_resume_ids:
            if not jd_education or jd_education == "Not Specified" or "education" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["education"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_education = [
                        item.degree
                        for item in (candidate_obj.education or [])
                        if hasattr(item, "degree") and item.degree
                    ]

                    matched_education_skill = 0
                    for skill in jd_education:
                        if self.skill_match(skill, candidate_education, skill_matrix_data):
                            matched_education_skill += 1

                    education_total = len(jd_education)
                    education_percentage = (matched_education_skill / education_total) * 100
                    education_score = matched_education_skill / education_total

                    education_requirements_score = {
                        "matched": matched_education_skill,
                        "total": education_total,
                        "percentage": round(education_percentage, 2),
                        "score": round(education_score, 4)
                    }

                    print(education_requirements_score)
                    results[resume_id]["education"] = education_requirements_score["score"]

        if eligible_resume_ids:
            if not jd_location or jd_location == "Not Specified" or "location" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["location"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    location_match = False
                    if self.skill_match(jd_location, [candidate_obj.location], skill_matrix_data):
                        location_match = True

                    results[resume_id]["location"] = location_match

        if eligible_resume_ids:
            if not jd_salary_range or jd_salary_range == "Not Specified" or "salary_range" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["salary_range"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_salary = candidate_obj.salary
                    eligible = self.salary_match(jd_salary_range, candidate_salary)

                    results[resume_id]["salary_range"] = eligible

        if eligible_resume_ids:
            if not jd_age_range or jd_age_range == "Not Specified" or "age_range" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["age_range"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_data = candidate_obj.candidate_id
                    age_match = self.is_years_match(
                        jd_age_range,
                        candidate_data.age
                    )

                    results[resume_id]["age_range"] = age_match

        if eligible_resume_ids:
            if not jd_gender_filter or jd_gender_filter == "Not Specified" or "gender" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["gender"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_data = candidate_obj.candidate_id

                    jd_gender = jd_gender_filter.lower().strip()
                    candidate_gender = None
                    if candidate_data.gender:
                        candidate_gender = candidate_data.gender.lower().strip()

                    if jd_gender == candidate_gender:
                        results[resume_id]["gender"] = True
                    else:
                        results[resume_id]["gender"] = False

        if eligible_resume_ids:
            if not jd_employment_type or jd_employment_type == "Not Specified" or "employment_type" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["employment_type"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_data = candidate_obj.candidate_id

                    jd_employment_match = self.trigram_match(jd_employment_type, candidate_data.employment_type)

                    results[resume_id]["employment_type"] = jd_employment_match

        if eligible_resume_ids:
            if not jd_notice_period or jd_notice_period == "Not Specified" or "notice_period" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["notice_period"] = None
            else:
                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_data = candidate_obj.candidate_id

                    candidate_notice_raw = candidate_data.notice_period or ""
                    if "serving" in candidate_notice_raw.lower() or "on notice" in candidate_notice_raw.lower():
                        results[resume_id]["notice_period"] = True
                    else:
                        jd_days = self.parse_notice_period_to_days(jd_notice_period)
                        candidate_days = self.parse_notice_period_to_days(candidate_notice_raw)

                        if jd_days is None or candidate_days is None:
                            results[resume_id]["notice_period"] = None
                        else:
                            results[resume_id]["notice_period"] = candidate_days <= jd_days

        if eligible_resume_ids:
            if not jd_last_modified or jd_last_modified == "Not Specified" or "last_modified" not in eligible_search_params:
                for c in eligible_resume_ids:
                    resume_id = str(c)
                    if resume_id not in results:
                        results[resume_id] = {}
                    results[resume_id]["last_modified"] = None
            else:
                jd_days = int(jd_last_modified)
                cutoff_date = now() - timedelta(days=jd_days)

                for c in eligible_resume_ids:
                    resume_id = str(c)

                    if resume_id not in results:
                        results[resume_id] = {}

                    candidate_obj = TblCandidateResume.objects.get(resume_id=c)

                    candidate_data = candidate_obj.candidate_id

                    timestamps = [
                        candidate_obj.created_dt,
                        candidate_obj.updated_dt,
                        candidate_data.created_dt,
                        candidate_data.updated_dt,
                    ]
                    timestamps = [t for t in timestamps if t is not None]
                    latest_modified = max(timestamps) if timestamps else None

                    if not latest_modified:
                        results[resume_id]["last_modified"] = False
                    else:
                        results[resume_id]["last_modified"] = (
                            latest_modified >= cutoff_date
                        )

        if "gender" in eligible_search_params:
            results = {
                k: v for k, v in results.items()
                if v.get("gender") is not False and v.get("gender") is not None
            }

        if "notice_period" in eligible_search_params:
            results = {
                k: v for k, v in results.items()
                if v.get("notice_period") is not False and v.get("notice_period") is not None
            }

        if "age_range" in eligible_search_params:
            results = {
                k: v for k, v in results.items()
                if v.get("age_range") is not False and v.get("age_range") is not None
            }

        if "salary_range" in eligible_search_params:
            results = {
                k: v for k, v in results.items()
                if v.get("salary_range") is not False and v.get("salary_range") is not None
            }

        if "last_modified" in eligible_search_params:
            results = {
                k: v for k, v in results.items()
                if v.get("last_modified") is not False and v.get("last_modified") is not None
            }

        BASE_WEIGHTS = {
            "primary_skills": 0.5,
            "responsibilities": 0.25,
            "keywords": 0.1,
            "tools_and_frameworks": 0.025,
            "secondary_skills": 0.025,
            "domain_requirements": 0.025,
            "education": 0.025,
            "location": 0.025,
            "employment_type": 0.025,
        }

        for _, match_data in results.items():
            match_data["overall_score"] = self.calculate_overall_score(match_data, BASE_WEIGHTS)

        results = dict(
            sorted(
                results.items(),
                key=lambda item: item[1].get("overall_score", 0),
                reverse=True
            )
        )

        return Response({
            "jd_id": jd_id,
            "total_candidates": len(results),
            "matches": results
        })
    
class ApplyJobRedirect(APIView):
    permission_classes = [AllowAny]

    def get(self, request, token):
        jd = get_object_or_404(TblJobDescription, apply_token_url=token)

        frontend_url = f"https://career.itconnectus.com/job/{jd.jd_display_id}"
        return redirect(frontend_url)
    
@api_view(["GET"])
def resume_basic_details(request, resume_id):
    matched_id = request.query_params.get("matched_id")
    resume = TblCandidateResume.objects.select_related("candidate_id").get(resume_id=resume_id)

    l0_present = False
    l0_completed_time = None
    l0_result = None

    if matched_id:
        matched_obj = TblMatchedProfiles.objects.filter(
            id=matched_id,
            status__in=["matched"]
        ).first()

        if matched_obj:
            l0_obj = L0InterviewList.objects.filter(
                status="Completed",
                matched_profile__resume_id=resume_id,
                interview_score__isnull=False
            ).order_by("-created_at").first()

            if l0_obj and l0_obj.interview_score:
                final_recommendation = (
                    l0_obj.interview_score
                    .get("Final_Hiring_Recommendation")
                )

                if final_recommendation:
                    l0_present = True
                    l0_completed_time = l0_obj.created_at
                    l0_result = final_recommendation

    serializer = ResumeBasicSerializer(
        resume,
        context={
            "request": request,
            "l0_present": l0_present,
            "l0_completed_time": l0_completed_time,
            "l0_result": l0_result
        }
    )
    return Response(serializer.data)

def run_matching_engine(jd_id, eligible_search_params=None):

    view = IdentifyMatchingProfiles()
    payload = {
        "jd_id": jd_id,
        "eligible_search_params": eligible_search_params or []
    }
    fake_request = type("obj", (object,), {
        "data": payload
    })

    response = view.post(fake_request)
    return response.data

@api_view(['POST'])
def get_matched_profiles(request):
    jd_id = request.data.get("jd_id")
    eligible_search_params = request.data.get("eligible_search_params", [])
    regen_filter = request.data.get("regen_filter", [])

    slots_left = 20

    if not regen_filter:
        existing = TblMatchedProfiles.objects.filter(job_id=jd_id)

        for m in existing:
            if not TblCandidateResume.objects.filter(resume_id=m.resume_id).exists():
                m.delete()

        existing = TblMatchedProfiles.objects.filter(job_id=jd_id)

        if existing.exists():
            serializer = TblMatchedProfilesSerializer(existing, many=True)
            return Response({
                "source": "db",
                "data": serializer.data
            })

    else:
        TblMatchedProfiles.objects.filter(
            job_id=jd_id,
            status__iexact="matched"
        ).delete()

    jd = TblJobDescription.objects.get(jd_id=jd_id)

    match_response = run_matching_engine(jd_id, eligible_search_params)
    matches = match_response.get("matches", {})

    if not matches:
        existing = TblMatchedProfiles.objects.filter(job_id=jd_id)
        serializer = TblMatchedProfilesSerializer(existing, many=True)
        return Response({
            "source": "engine",
            "data": serializer.data
        })

    sorted_candidates = sorted(
        matches.items(),
        key=lambda x: x[1]["overall_score"],
        reverse=True
    )[:slots_left]

    for resume_id, d in sorted_candidates:

        resume = TblCandidateResume.objects.filter(resume_id=resume_id).first()
        if not resume:
            continue

        if TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).exists():
            continue

        TblMatchedProfiles.objects.create(
            resume_id=resume_id,
            job_id=jd_id,
            status="matched",
            match_score=d["overall_score"],
            primaryskill_score=d["primary_skills"],
            secondaryskill_score=d["secondary_skills"],
            responsibilities_score=d["responsibilities"],
            tech_framework_score=d["tools_and_frameworks"],
            experience_score=d["experience"],
        )

    if jd.jd_stage in ["Created", "Assigned", "Posted"]:
        jd.jd_stage = "Matched"
        jd.save(update_fields=["jd_stage"])

    final_queryset = TblMatchedProfiles.objects.filter(job_id=jd_id)

    jd.last_profile_search_time = timezone.now()
    jd.save()

    serializer = TblMatchedProfilesSerializer(final_queryset, many=True)

    return Response({
        "source": "engine",
        "data": serializer.data
    })


@api_view(['POST'])
def shortlist_profiles(request):
    resume_ids = request.data.get("resume_ids", [])
    jd_id = request.data.get("jd_id")
    consent_required = request.data.get("consent")

    profiles = TblMatchedProfiles.objects.filter(
        resume_id__in=resume_ids,
        job_id=jd_id
    )

    for p in profiles:
        p.status = "L0 completed" if consent_required else "short_listed"
        p.save()

        print("-----mail sending")
        send_shortlist_email(p.resume_id, jd_id, consent_required)
        print("-----mail sent successfully")

    return Response({"message": "Shortlisted"})

@api_view(['POST'])
def find_more_profiles(request):

    jd_id = request.data.get("jd_id")
    eligible_search_params = request.data.get("eligible_search_params", [])

    jd = TblJobDescription.objects.get(jd_id=jd_id)
    last_time = jd.last_profile_search_time

    match_response = run_matching_engine(jd_id, eligible_search_params)
    matches = match_response.get("matches", {})

    if not matches:
        return Response({"added": 0})

    sorted_candidates = sorted(
        matches.items(),
        key=lambda x: x[1]["overall_score"],
        reverse=True
    )

    added = 0
    updated = 0

    for resume_id, d in sorted_candidates:

        resume = TblCandidateResume.objects.get(resume_id=resume_id)

        resume = TblCandidateResume.objects.filter(resume_id=resume_id).first()

        if not resume:
            TblMatchedProfiles.objects.filter(
                resume_id=resume_id,
                job_id=jd_id
            ).delete()
            continue

        existing = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if existing:

            if last_time and (
                resume.created_dt > last_time or
                resume.updated_dt > last_time
            ):
                existing.match_score = d["overall_score"]
                existing.primaryskill_score = d["primary_skills"]
                existing.secondaryskill_score = d["secondary_skills"]
                existing.responsibilities_score = d["responsibilities"]
                existing.tech_framework_score = d["tools_and_frameworks"]
                existing.experience_score = d["experience"]
                existing.save()
                updated += 1

            continue

        if last_time:
            if resume.created_dt <= last_time and resume.updated_dt <= last_time:
                continue

        TblMatchedProfiles.objects.create(
            resume_id=resume_id,
            job_id=jd_id,
            status="matched",
            match_score=d["overall_score"],
            primaryskill_score=d["primary_skills"],
            secondaryskill_score=d["secondary_skills"],
            responsibilities_score=d["responsibilities"],
            tech_framework_score=d["tools_and_frameworks"],
            experience_score=d["experience"],
        )

        added += 1

        if added >= 20:
            break

    jd.last_profile_search_time = timezone.now()
    jd.save()

    return Response({
        "added": added,
        "updated": updated
    })

@api_view(["GET"])
@permission_classes([AllowAny])
def career_open_jobs(request):
    jobs = TblJobDescription.objects.filter(
        status="Open",
        jd_stage__in=["Assigned", "Posted"],
    ).values(
        "jd_id",
        "jd_display_id",
        "job_title",
        "job_location",
        "years_of_experience",
        "job_type",
        "public_token",
        "created_dt"
    ).order_by("-created_dt")

    return Response(jobs)

@api_view(["POST"])
@permission_classes([AllowAny])
def career_apply_job(request):
    try:
        jd_display_id = request.data.get("jd_id")
        full_name = request.data.get("name")
        email = request.data.get("email")
        mobile = request.data.get("mobile")
        resume_file = request.FILES.get("resume")

        if not all([jd_display_id, full_name, email, mobile, resume_file]):
            return Response(
                {"error": "All fields are required"},
                status=400
            )

        jd = get_object_or_404(
            TblJobDescription,
            jd_display_id=jd_display_id,
            status="Open",
            jd_stage__in=["Assigned", "Posted"],
        )

        application = CareerJobApplication.objects.create(
            jd=jd,
            full_name=full_name,
            email=email,
            mobile=mobile,
            resume=resume_file,
            source="InternalCareer"
        )

        resume_path = application.resume.path
        resume_url = application.resume.url
        extracted_text = ""

        if resume_path.lower().endswith(".pdf"):
            extracted_text = DocumentTextExtraction.process_pdf(resume_path)

        elif resume_path.lower().endswith((".docx", ".doc")):
            extracted_text = DocumentTextExtraction.process_word(resume_path)

        if not extracted_text.strip():
            return Response(
                {"error": "Could not extract resume text"},
                status=400
            )

        airecruit_llm = AIEditLLM()
        generation_config = {"temperature": 0.2, "thinking_budget": 32768}
        airecruit_llm.create_chat(
            generation_config["temperature"],
            generation_config["thinking_budget"]
        )

        prompt = LlmPrompts.resume_extraction_prompt(extracted_text)
        ai_response = airecruit_llm.send_message(prompt)

        parsed_json = MailReaderGoogle().extract_json_from_llm(ai_response.text)

        flat_data = MailReaderGoogle().flatten_data_for_db(parsed_json)

        candidate, _ = TblCandidateProfile.objects.get_or_create(
            email=email,
            defaults={
                "first_name": flat_data.get("first_name") or full_name.split(" ")[0],
                "last_name": flat_data.get("last_name", ""),
                "mobile_number": mobile,
                "source": "public",
                "candidate_status": "Active",
            }
        )

        existing_resumes = TblCandidateResume.objects.filter(candidate_id=candidate)

        matched_resume = None

        if flat_data.get("title") and existing_resumes.exists():
            normalize_title_text = normalize_resume_title(flat_data["title"])

            for resume in existing_resumes:
                existing_title = normalize_resume_title(resume.title)
                if existing_title == normalize_title_text:
                    matched_resume = resume
                    break

        resume_responsibilities_embedding = airecruit_llm.generate_embeddings(
            flat_data.get("responsibility_summary")
        )

        if matched_resume:
            matched_resume.location = flat_data.get("location", "")
            matched_resume.primary_skills = flat_data.get("primary_skills", [])
            matched_resume.technical_skills = flat_data.get("technical_skills", [])
            matched_resume.tools_and_frameworks = flat_data.get("tools_and_frameworks", [])
            matched_resume.soft_skills = flat_data.get("soft_skills", [])
            matched_resume.domain_skills = flat_data.get("domain_skills", [])
            matched_resume.education = flat_data.get("education", [])
            matched_resume.certifications = flat_data.get("certifications", [])
            matched_resume.projects = flat_data.get("projects", [])
            matched_resume.parsed_text = extracted_text
            matched_resume.resume_file = resume_url
            matched_resume.responsibility_summary = flat_data.get("responsibility_summary")
            matched_resume.resume_responsibilities_summary_embedding = (
                resume_responsibilities_embedding if resume_responsibilities_embedding else None
            )
            matched_resume.resume_source = "Career"
            matched_resume.updated_by = "Career_apply"
            matched_resume.save()

        else:
            resume_obj = TblCandidateResume.objects.create(
                candidate_id=candidate,
                email=email,
                title=flat_data.get("title", ""),
                location=flat_data.get("location", ""),
                primary_skills=flat_data.get("primary_skills", []),
                technical_skills=flat_data.get("technical_skills", []),
                tools_and_frameworks=flat_data.get("tools_and_frameworks", []),
                soft_skills=flat_data.get("soft_skills", []),
                domain_skills=flat_data.get("domain_skills", []),
                parsed_text=extracted_text,
                education=flat_data.get("education", []),
                certifications=flat_data.get("certifications", []),
                projects=flat_data.get("projects", []),
                resume_file=resume_url,
                responsibility_summary=flat_data.get("responsibility_summary"),
                resume_responsibilities_summary_embedding=(
                    resume_responsibilities_embedding if resume_responsibilities_embedding else None
                ),
                resume_source="Career",
                status="Parsed",
                created_by="Career_apply"
            )

            try:
                match_response = run_matching_engine_candidate(resume_obj.resume_id)
                matches = match_response.get("matches", {})
            except Exception as e:
                matches = {}
            
            if matches:
                manager_sending_emails = list(
                    User.objects
                    .filter(groups__name="Delivery Manager (DM)", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                super_admin_cc_emails = list(
                    User.objects
                    .filter(groups__name="Implementer Super Admin", is_active=True)
                    .exclude(email__isnull=True)
                    .exclude(email__exact="")
                    .values_list("email", flat=True)
                )

                subject, body = get_email_template("candidate_jd_match_notification", is_html=False)

                candidate_name = f"{candidate.first_name} {candidate.last_name}"

                subject = subject.format(candidate_name=candidate_name)

                body = body.format(
                    resume_id=resume_obj.resume_id,
                    candidate_name=candidate_name,
                    upload_date=resume_obj.created_dt if hasattr(resume_obj, "created_dt") else "N/A",
                    match_count=len(matches)
                )

                for to_mail in manager_sending_emails:
                    send_email_safe(
                        subject,
                        body,
                        to_mail,
                        settings.EMAIL_HOST_USER,
                        cc_emails=super_admin_cc_emails,
                        is_html=False
                    )

        send_resume_ack_email(
            to_email=email,
            candidate_name=full_name,
            job_title=jd.job_title
        )

        return Response({"success": True}, status=201)

    except Exception as e:
        import traceback
        traceback.print_exc()
        return Response(
            {"error": "Resume processing failed"},
            status=500
        )
    
@api_view(["GET"])
@permission_classes([AllowAny])
def career_job_detail(request, jd_id):
    jd = get_object_or_404(
        TblJobDescription,
        jd_id=jd_id,
        status="Open",
        jd_stage__in=["Created", "Assigned", "Posted"],
    )

    data = {
        "job_title": jd.job_title,
        "job_location": jd.job_location,
        "years_of_experience": jd.years_of_experience,
        "job_type": jd.job_type,
        "salary_range": jd.salary_range,
        "job_summary": jd.job_summary,
        "responsibilities": jd.responsibilities,
        "required_qualifications": jd.required_qualifications,
        "preferred_qualifications": jd.preferred_qualifications,
    }

    return Response(data)

class SearchPatternResetButton(APIView):
    def extract_json_from_llm(self, text: str) -> dict:
        # Remove ```json and ```
        cleaned = re.sub(r"```json|```", "", text, flags=re.IGNORECASE).strip()

        return json.loads(cleaned)
    
    def get(self, request):
        jd_id = request.query_params.get("jd_id")

        if not jd_id:
            return Response({"error": "jd_id is required"}, status=400)
        
        airecruit_llm = AIEditLLM()
        generation_config = {"temperature": 0.2, "thinking_budget": 32768}
        
        jd_instance = TblJobDescription.objects.get(pk=jd_id)

        llm_sending_data = {
            "jd_id": jd_instance.jd_id,
            "jd_display_id": jd_instance.jd_display_id,
            "job_title": jd_instance.job_title,
            "job_type": jd_instance.job_type,
            "years_of_experience": jd_instance.years_of_experience,
            "about_company": jd_instance.about_company,
            "job_summary": jd_instance.job_summary,
            "responsibilities": jd_instance.responsibilities,
            "domain_requirements": jd_instance.domain_requirements,
            "certification_requirements": jd_instance.certification_requirements,
            "security_clearance_requirements": jd_instance.security_clearance_requirements,
            "onsite_job": jd_instance.onsite_job,
            "job_location": jd_instance.job_location,
            "required_qualifications": jd_instance.required_qualifications,
            "preferred_qualifications": jd_instance.preferred_qualifications
        }

        jd_search_pattern_prompt = LlmPrompts.jd_search_pattern_prompt([llm_sending_data])

        airecruit_llm.create_chat(
            generation_config["temperature"], generation_config["thinking_budget"]
        )
        jd_llm_response = airecruit_llm.send_message(jd_search_pattern_prompt)
        llm_jd_data = self.extract_json_from_llm(jd_llm_response.text)

        print(jd_llm_response.text)

        if llm_jd_data:
            jd_instance.search_pattern = llm_jd_data[0].get("search_pattern")
            jd_instance.save(update_fields=["search_pattern"])

        return Response({
            "search_pattern": llm_jd_data[0].get("search_pattern"),
        }, status=200)
    
@api_view(["POST"])
@permission_classes([AllowAny])
def interview_job_candidate_detail(request):
    try:
        jd_id = request.data.get("jd_id")
        resume_id = request.data.get("resume_id")

        jd_data = get_object_or_404(
            TblJobDescription,
            jd_id=jd_id,
        )

        resume_data = get_object_or_404(
            TblCandidateResume,
            resume_id=resume_id,
        )

        data = {
            "job_title": jd_data.job_title,
            "job_location": jd_data.job_location,
            "years_of_experience": jd_data.years_of_experience,
            "job_type": jd_data.job_type,
            "salary_range": jd_data.salary_range,
            "job_summary": jd_data.job_summary,
            "responsibilities": jd_data.responsibilities,
            "candidate_name": resume_data.candidate_id.first_name + " " + resume_data.candidate_id.last_name,
            "candidate_mail": resume_data.candidate_id.email
        }

        return Response(data)

    except Exception:
        return Response({"error": "Data Extraction Failed"}, status=404)
    
@api_view(["POST"])
@permission_classes([AllowAny])
def interview_send_otp(request):
    try:
        candidate_name = request.data.get("candidate_name")
        candidate_mail = request.data.get("candidate_mail")
        resume_id = request.data.get("resume_id")

        if not candidate_mail or not resume_id:
            return Response({"error": "Email and Resume ID are required"}, status=400)

        key = base64.b32encode(str(resume_id).encode()).decode()
        OTP = pyotp.TOTP(key, interval=settings.INTERVIEW_OTP_EXPIRY_SECONDS)
        otp_code = OTP.now()

        subject, body = get_email_template("interview_otp_mail", is_html=False)

        body = body.format(
            candidate_name=candidate_name,
            otp_code=otp_code,
            otp_minutes=settings.INTERVIEW_OTP_EXPIRY_SECONDS // 60
        )

        otp_sent = send_email_safe(
            subject,
            body,
            candidate_mail,
            settings.EMAIL_HOST_USER,
            cc_emails=None,
            is_html=False
        )

        if otp_sent:
            return Response(
                {"message": "OTP sent successfully"},
                status=200
            )
        else:
            return Response(
                {"message": "OTP sent failed"},
                status=500
            )
    
    except Exception as e:
        import traceback
        traceback.print_exc()
        return Response(
            {"error": "Data Extraction Failed"},
            status=404
        )
    
@api_view(["POST"])
@permission_classes([AllowAny])
def interview_verify_otp(request):
    try:
        resume_id = request.data.get("resume_id")
        otp_entered = request.data.get("otp")

        if not resume_id or not otp_entered:
            return Response({"error": "Resume ID and OTP are required"}, status=400)

        key = base64.b32encode(str(resume_id).encode()).decode()
        totp = pyotp.TOTP(key, interval=settings.INTERVIEW_OTP_EXPIRY_SECONDS)

        if totp.verify(otp_entered, valid_window=1):
            return Response({"message": "OTP Verified Successfully"}, status=200)

        return Response({"error": "Invalid or Expired OTP"}, status=400)

    except Exception:
        return Response({"error": "OTP Verification Failed"}, status=500)

@api_view(["POST"])
@permission_classes([AllowAny])
def l0_interview_start(request):
    try:
        resume_id = request.data.get("resume_id")
        jd_id = request.data.get("jd_id")

        if not resume_id or not jd_id:
            return Response(
                {"error": "Resume ID and JD ID are required"},
                status=400
            )
        
        matched_profile = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if not matched_profile:
            return Response(
                {"error": "Matched Profile is Missing"},
                status=400
            )
        interview_obj, created = L0InterviewList.objects.get_or_create(
            matched_profile=matched_profile,
            defaults={"status": "Started"}
        )

        if not created:
            interview_obj.status = "Started"
            interview_obj.save(update_fields=["status"])

            matched_profile.status = "Start L0 Interview"
            matched_profile.save(update_fields=["status"])

        serializer = L0InterviewListSerializer(interview_obj)

        return Response(serializer.data, status=200)

    except Exception:
        return Response({"error": "L0 Interview Starting Failed"}, status=500)
    
@api_view(["GET"])
@permission_classes([AllowAny])
def shortlist_details(request, token):
    try:
        resume_id, jd_id = token.split("_")

        matched = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if not matched:
            return Response({"error": "Invalid token"}, status=400)

        resume = TblCandidateResume.objects.select_related("candidate_id").get(
            resume_id=resume_id
        )

        jd = TblJobDescription.objects.get(jd_id=jd_id)
        implementer = jd.implementor_id

        candidate_name = f"{resume.candidate_id.first_name} {resume.candidate_id.last_name}".strip()

        SUBMITTED_STATUSES = {
            "matched",
            "Interested"
        }

        SUBMITTED = {
            "matched",
            "Interested",
            "Additional Info"
        }

        return Response({
            "notice_period": matched.notice_period,
            "current_salary": matched.current_salary,
            "expected_salary": matched.expected_salary,
            "visa_status": matched.visa_status,
            "remarks": matched.remarks,
            "candidate_name": candidate_name,
            "job_title": jd.job_title,
            "job_location": jd.job_location,
            "years_of_experience": jd.years_of_experience,
            "country": implementer.country,
            "currency": implementer.currencies,
            "already_submitted": matched.status in SUBMITTED_STATUSES,
            "submitted": matched.status in SUBMITTED
        })

    except Exception as e:
        return Response({"error": str(e)}, status=400)

@api_view(["POST"])
@permission_classes([AllowAny])
def save_candidate_availability(request):
    try:
        token = request.data.get("token")
        date = request.data.get("date")
        time = request.data.get("time")

        if not token:
            return Response({"error": "Missing token"}, status=400)

        link = PublicLink.objects.filter(token=token).first()

        if link:
            resume_id = link.resume_id
            jd_id = link.jd_id

        else:
            if "_" not in token:
                return Response({"error": "Invalid token format"}, status=400)

            resume_id, jd_id = token.split("_")

        matched = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if not matched:
            return Response({"error": "Matched profile not found"}, status=400)

        interview_dt = datetime.strptime(
            f"{date} {time}",
            "%Y-%m-%d %H:%M"
        )

        matched.interview_datetime = interview_dt
        matched.status = "L0 Interview Scheduled"
        matched.save()

        send_interview_calendar_invite(matched)

        return Response({"message": "Availability saved & recruiter notified"})

    except Exception as e:
        return Response({"error": str(e)}, status=400)

@api_view(["POST"])
@permission_classes([AllowAny])
def submit_shortlist_details(request):
    try:
        token = request.data.get("token")

        if not token:
            return Response({"error": "Missing token"}, status=400)

        link = PublicLink.objects.filter(token=token).first()

        if link:
            resume_id = link.resume_id
            jd_id = link.jd_id

        else:
            if "_" not in token:
                return Response({"error": "Invalid token format"}, status=400)

            resume_id, jd_id = token.split("_")

        matched = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if not matched:
            return Response({"error": "Matched profile not found"}, status=400)

        is_first_submission = not any([
            matched.notice_period,
            matched.current_salary,
            matched.expected_salary,
            matched.visa_status,
            matched.remarks
        ])

        matched.notice_period = request.data.get("notice_period")
        matched.current_salary = request.data.get("current_salary")
        matched.expected_salary = request.data.get("expected_salary")
        matched.visa_status = request.data.get("visa_status")
        matched.remarks = request.data.get("remarks")

        resume_obj = TblCandidateResume.objects.get(resume_id=resume_id)
        candidate_obj = resume_obj.candidate_id

        candidate_obj.notice_period = request.data.get("notice_period")
        candidate_obj.current_salary = request.data.get("current_salary")
        candidate_obj.expected_salary = request.data.get("expected_salary")
        candidate_obj.visa_status = request.data.get("visa_status")
        candidate_obj.notes = request.data.get("remarks")

        if is_first_submission:
            matched.status = "Additional Info"

        matched.save()

        candidate_obj.save(update_fields=[
            "notice_period",
            "current_salary",
            "expected_salary",
            "visa_status",
            "notes"
        ])

        return Response({
            "message": "Details saved successfully",
            "is_edit": not is_first_submission
        })

    except Exception as e:
        return Response({"error": str(e)}, status=400)
    
@api_view(["GET"])
def country_currency_list(request):
    data = [
        {"country": "India", "currency": "INR"},
        {"country": "United States", "currency": "USD"},
        {"country": "United Kingdom", "currency": "GBP"},
        {"country": "Canada", "currency": "CAD"},
        {"country": "Australia", "currency": "AUD"},
        {"country": "Singapore", "currency": "SGD"},
        {"country": "Germany", "currency": "EUR"},
    ]
    return Response(data)

@api_view(["POST"])
@permission_classes([AllowAny])
def l0_interview_record_video(request):
    try:
        video_file = request.FILES.get("video")
        l0_interview_id = request.data.get("l0_interview")
        matched_profile_id = request.data.get("matched_profile")
        question_text = request.data.get("question")
        saving_type = request.data.get("saving_type")

        if not video_file:
            return Response(
                {"error": "Video file is required"},
                status=400
            )
        
        if not l0_interview_id or not matched_profile_id or not question_text:
            return Response(
                {"error": "Interview ID, Matched Profile, and Question are required"},
                status=400
            )
        
        interview_obj = L0InterviewList.objects.filter(id=l0_interview_id).first()
        if not interview_obj:
            return Response({"error": f"Interview not found"}, status=404)

        matched_profile_obj = TblMatchedProfiles.objects.filter(id=matched_profile_id).first()
        if not matched_profile_obj:
            return Response({"error": f"Matched Profile not found"}, status=404)
        
        with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as temp_video:
            for chunk in video_file.chunks():
                temp_video.write(chunk)

            temp_video_path = temp_video.name
        
        watermarked_video_path = temp_video_path.replace(".webm", "_wm.webm")
        temp_audio_path = temp_video_path.replace(".webm", ".wav")
        implementor_logo = os.path.join(settings.MEDIA_ROOT, "app_images", "implementor_logo.webp")

        extracted_answer_text = ""

        try:
            subprocess.run(
                [
                    "ffmpeg", "-y",
                    "-i", temp_video_path,
                    "-i", implementor_logo,
                    "-filter_complex",
                    f"[0:v]scale=640:-2[scaled];"
                    "[1:v]scale=100:-1[logo];"
                    "[scaled][logo]overlay=W-w-20:20[outv]",
                    "-map", "[outv]",
                    "-map", "0:a?",
                    "-c:v", "libvpx-vp9",
                    "-crf", "30",
                    "-b:v", "0",
                    "-row-mt", "1",
                    "-c:a", "libopus",
                    "-b:a", "96k",
                    watermarked_video_path
                ],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )

            subprocess.run(
                [
                    "ffmpeg", "-y",
                    "-i", watermarked_video_path,
                    "-vn",
                    "-acodec", "pcm_s16le",
                    "-ar", "16000",
                    "-ac", "1",
                    temp_audio_path
                ],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )

            # result = whisper_model.transcribe(temp_audio_path)
            # extracted_answer_text = result["text"]

            recognizer = sr.Recognizer()
            with sr.AudioFile(temp_audio_path) as source:
                audio_data = recognizer.record(source)

            try:
                extracted_answer_text = recognizer.recognize_google(audio_data).strip()
            except sr.UnknownValueError:
                extracted_answer_text = ""
            except sr.RequestError as e:
                return Response(
                    {"error": f"Audio Processing failed: {str(e)}"},
                    status=500
                )
            except Exception as e:
                return Response(
                    {"error": f"Audio Processing failed: {str(e)}"},
                    status=500
                )
            
            with open(watermarked_video_path, "rb") as f:
                interview_video_file = File(f, name=video_file.name)

                session_obj = L0InterviewSessions.objects.create(
                    matched_profile=matched_profile_obj,
                    l0_interview=interview_obj,
                    recorded_video=interview_video_file,
                    question=question_text,
                    answer=extracted_answer_text
                )
        except Exception as e:
            return Response(
                {"error": f"Audio Processing failed {e}"},
                status=500
            )
        finally:
            for path in [temp_video_path, watermarked_video_path, temp_audio_path]:
                if os.path.exists(path):
                    os.remove(path)

        if saving_type == "submit":
            interview_obj.status = "Completed"
            interview_obj.interview_completed_time = timezone.now()
            interview_obj.save(update_fields=["status", "interview_completed_time"])

            matched_profile_obj.status = "L0 completed"
            matched_profile_obj.save(update_fields=["status"])

        serializer = L0InterviewSessionsSerializer(session_obj)

        return Response(serializer.data, status=200)
    except Exception as e:
        import traceback
        traceback.print_exc()

        return Response(
            {"error": f"Video Upload Failed {e}"},
            status=500
        )
    
@api_view(["GET"])
@permission_classes([AllowAny])
def l0_interview_eligibility_check(request):
    try:
        jd_id = request.query_params.get("jd_id")
        resume_id = request.query_params.get("resume_id")

        matched_profile = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if not matched_profile:
            return Response({"eligible": False}, status=200)

        if matched_profile.status in [
            "L0 Interview Scheduled",
            "Additional Info",
            "Start L0 Interview"
        ]:

            l0_interview_data = L0InterviewList.objects.filter(
                matched_profile=matched_profile
            ).first()

            questions_completed = 0

            if not l0_interview_data:
                status_data = "Not Started"
            else:
                status_data = l0_interview_data.status
                questions_completed = L0InterviewSessions.objects.filter(
                    l0_interview=l0_interview_data
                ).count()

            return Response(
                {
                    "eligible": True,
                    "status_data": status_data,
                    "questions_completed": questions_completed
                },
                status=200
            )

        return Response({"eligible": False}, status=200)

    except Exception as e:
        import traceback
        traceback.print_exc()
        return Response(
            {"error": f"Eligibility Check API Failed {e}"},
            status=500
        )
    
class L0InterviewListViewSet(ModelViewSet):
    permission_classes = [IsAuthenticated]
    serializer_class = L0InterviewListSerializer
    queryset = L0InterviewList.objects.all()

    http_method_names = ["get", "delete", "patch"]

    def get_queryset(self):
        queryset = super().get_queryset()

        matched_profile = self.request.query_params.get("matched_profile")

        if matched_profile:
            queryset = queryset.filter(matched_profile=matched_profile)

        return queryset

class L0InterviewSessionsViewSet(ModelViewSet):
    permission_classes = [IsAuthenticated]
    serializer_class = L0InterviewSessionsSerializer
    queryset = L0InterviewSessions.objects.all()

    http_method_names = ["get", "delete"]

def convert_video_to_wav(video_path):
    base = os.path.splitext(video_path)[0]
    wav_path = base + "_temp.wav"

    cmd = [
        "ffmpeg", "-y",
        "-i", video_path,
        "-ac", "1",              # mono
        "-ar", "16000",          # 16kHz
        "-vn",                   # remove video
        "-c:a", "pcm_s16le",     # PCM WAV format
        wav_path
    ]

    subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)

    return wav_path


def get_speech_trim_points(video_path, buffer_seconds=2):
    # Step 1: Convert video to wav
    wav_path = convert_video_to_wav(video_path)

    # Step 2: Run silence detection on wav
    cmd = [
        "ffmpeg",
        "-i", wav_path,
        "-af", "silencedetect=n=-20dB:d=0.5",
        "-f", "null",
        "-"
    ]

    result = subprocess.run(
        cmd,
        stderr=subprocess.PIPE,
        stdout=subprocess.PIPE,
        text=True
    )

    output = result.stderr

    silence_starts = []
    silence_ends = []

    for line in output.split("\n"):
        if "silence_start" in line:
            silence_starts.append(float(line.split("silence_start:")[1].strip()))

        if "silence_end" in line:
            silence_ends.append(
                float(line.split("silence_end:")[1].split("|")[0].strip())
            )

    # Cleanup wav file
    if os.path.exists(wav_path):
        os.remove(wav_path)

    # Step 3: If no silence detected → keep full video
    if not silence_ends:
        return 0, None

    # Step 4: Speech starts after first silence_end
    speech_start = silence_ends[0]

    # Step 5: Speech ends before last silence_start
    speech_end = silence_starts[-1] if silence_starts else None

    # Step 6: Apply buffer
    trim_start = max(0, speech_start - buffer_seconds)
    trim_end = speech_end + buffer_seconds if speech_end else None

    return trim_start, trim_end

def generate_l0_final_interview_video(l0_interview_data, l0_interview_sessions, video_file_name):
    temp_dir = os.path.join(settings.MEDIA_ROOT, "temp_l0", str(l0_interview_data.matched_profile.id))
    os.makedirs(temp_dir, exist_ok=True)

    clip_paths = []

    ai_profile_image = os.path.join(settings.MEDIA_ROOT, "app_images", "AI_profile_image.png")
    implementor_logo = os.path.join(settings.MEDIA_ROOT, "app_images", "implementor_logo.webp")

    try:
        for idx, session in enumerate(l0_interview_sessions, start=1):
            question_audio_path = os.path.join(temp_dir, f"q{idx}.mp3")
            tts = gTTS(text=session.question, lang="en")
            tts.save(question_audio_path)
            question_video_path = os.path.join(temp_dir, f"q{idx}_video.mp4")

            subprocess.run([
                "ffmpeg", "-y",
                "-loop", "1",
                "-i", ai_profile_image,
                "-i", question_audio_path,
                "-i", implementor_logo,
                "-filter_complex",
                "[0:v]scale=500:500:force_original_aspect_ratio=decrease,"
                "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=white[bg];"
                "[2:v]scale=100:-1[logo];"
                "[bg][logo]overlay=W-w-20:20[outv]",
                "-map", "[outv]",
                "-map", "1:a",
                "-shortest",
                "-c:v", "libx264",
                "-preset", "medium",
                "-crf", "23",
                "-c:a", "aac",
                "-b:a", "128k",
                "-ar", "44100",
                "-ac", "2",
                "-pix_fmt", "yuv420p",
                "-movflags", "+faststart",
                question_video_path
            ])

            clip_paths.append(question_video_path)

            if session.recorded_video:

                trimmed_answer_path = os.path.join(
                    temp_dir, f"ans{idx}_trimmed.mp4"
                )

                trim_start, trim_end = get_speech_trim_points(
                    session.recorded_video.path,
                    buffer_seconds=settings.INTERVIEW_VIDEO_SILENCE_BUFFER_SECONDS
                )

                ffmpeg_trim_cmd = [
                    "ffmpeg", "-y",
                    "-i", session.recorded_video.path,
                    "-ss", str(trim_start),
                ]

                if trim_end:
                    ffmpeg_trim_cmd += ["-to", str(trim_end)]

                ffmpeg_trim_cmd += [
                    "-vf", "scale=1280:720,fps=30",
                    "-c:v", "libx264",
                    "-c:a", "aac",
                    "-ar", "44100",
                    "-ac", "2",
                    "-pix_fmt", "yuv420p",
                    trimmed_answer_path
                ]

                subprocess.run(ffmpeg_trim_cmd, check=True)

                clip_paths.append(trimmed_answer_path)
        final_video_name = video_file_name
        final_video_path = os.path.join(temp_dir, final_video_name)

        ffmpeg_inputs = []
        filter_parts = []

        for i, clip in enumerate(clip_paths):
            ffmpeg_inputs += ["-i", clip]
            filter_parts.append(f"[{i}:v:0][{i}:a:0]")

        concat_filter = (
            "".join(filter_parts)
            + f"concat=n={len(clip_paths)}:v=1:a=1[outv][outa]"
        )

        command = [
            "ffmpeg", "-y",
            *ffmpeg_inputs,
            "-filter_complex", concat_filter,
            "-map", "[outv]",
            "-map", "[outa]",
            "-c:v", "libx264",
            "-preset", "fast",
            "-crf", "30",
            "-c:a", "aac",
            "-b:a", "96k",
            "-ar", "44100",
            "-ac", "2",
            "-pix_fmt", "yuv420p",
            "-movflags", "+faststart",
            final_video_path
        ]

        subprocess.run(command)

        with open(final_video_path, "rb") as f:
            l0_interview_data.interview_video.save(final_video_name, File(f))

        l0_interview_data.save()

        return l0_interview_data.interview_video.url
    except Exception as e:
        raise e
    finally:
        if os.path.exists(temp_dir):
            shutil.rmtree(temp_dir, ignore_errors=True)

def generate_l0_summary_report(l0_interview_sessions):
    airecruit_llm = AIEditLLM()
    generation_config = {"temperature": 0.2, "thinking_budget": 32768}
    airecruit_llm.create_chat(
        generation_config["temperature"],
        generation_config["thinking_budget"]
    )

    interview_data = []

    for data in l0_interview_sessions:
        value = {
            "question": data.question,
            "candidate_answer": data.answer
        }
        interview_data.append(value)

    prompt = LlmPrompts.l0_interview_summary_prompt(interview_data)
    ai_response = airecruit_llm.send_message(prompt)

    parsed_json = MailReaderGoogle().extract_json_from_llm(ai_response.text)

    return parsed_json

@api_view(["GET"])
@permission_classes([AllowAny])
def l0_interview_final_summary(request):

    try:
        jd_id = request.query_params.get("jd_id")
        resume_id = request.query_params.get("resume_id")

        matched_profile = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if not matched_profile:
            return Response({"error": "Matched Profile not found"}, status=404)

        l0_interview_data = L0InterviewList.objects.filter(
            matched_profile=matched_profile
        ).first()

        if not l0_interview_data:
            return Response({"error": "Interview not found"}, status=404)

        l0_interview_sessions = L0InterviewSessions.objects.filter(
            l0_interview=l0_interview_data
        ).order_by("id")

        final_video_url = generate_l0_final_interview_video(
            l0_interview_data,
            l0_interview_sessions,
            f"L0_jd_{jd_id}_resume_{resume_id}_final_video.mp4"
        )

        final_summary_report = generate_l0_summary_report(l0_interview_sessions)
        l0_interview_data.interview_score = final_summary_report
        l0_interview_data.save(update_fields=["interview_score"])

        matched_profile.status = "Generated L0 report"
        matched_profile.l0_interview_summary = final_summary_report
        matched_profile.save(update_fields=["l0_interview_summary", "status"])

        candidate_resume = TblCandidateResume.objects.get(pk=resume_id)
        candidate_resume.l0_interview_data = l0_interview_data
        candidate_resume.l0_interview_summary = final_summary_report
        candidate_resume.save(update_fields=["l0_interview_data", "l0_interview_summary"])

        return Response({
            "summary_response": final_summary_report,
            "final_video": final_video_url
        })

    except Exception as e:
        import traceback
        traceback.print_exc()
        return Response({"error": str(e)}, status=500)
    
class TblMatchedProfilesViewSet(ModelViewSet):
    permission_classes = [IsAuthenticated]
    serializer_class = TblMatchedProfilesSerializer
    queryset = TblMatchedProfiles.objects.all()

    http_method_names = ["get", "delete"]

def secure_file_view(request):
    token = request.GET.get("token")
    entered_password = request.GET.get("password")

    if not token:
        return HttpResponseForbidden("""
            <h3>Invalid Access Link</h3>
            <p>The access link is missing or incomplete.</p>
            <p>Please use the original link shared with you or contact the administrator.</p>
        """)

    try:
        data = signing.loads(token)
    except signing.BadSignature:
        return HttpResponseForbidden("""
            <h3>Invalid or Tampered Link</h3>
            <p>This link appears to be invalid or has been modified.</p>
            <p>Please ensure you are using the correct link provided in the email.</p>
        """)

    if timezone.now().timestamp() > data["exp"]:
        return HttpResponseForbidden("""
            <h3>Link or Password Expired</h3>
            <p>This secure access link has expired.</p>
            <p>Please contact the administrator to request a new access link.</p>
        """)

    correct_password = data.get("pwd")

    if not entered_password:
        return HttpResponse(f"""
        <html>
        <head>
        <script>
            function askPassword(message) {{
                var pwd = prompt(message || "Enter password to access this file:");
                
                if (pwd === null || pwd.trim() === "") {{
                    alert("Incorrect password!");
                    askPassword("Enter password to access this file:");
                }} else {{
                    window.location.href = "?token={token}&password=" + encodeURIComponent(pwd);
                }}
            }}

            askPassword();
        </script>
        </head>
        </html>
        """)
    
    if entered_password != correct_password:
        return HttpResponse(f"""
        <html>
        <head>
        <script>
            function retry() {{
                var pwd = prompt("Incorrect password! Please try again:");

                if (pwd === null || pwd.trim() === "") {{
                    alert("Incorrect password!");
                    retry();
                }} else {{
                    window.location.href = "?token={token}&password=" + encodeURIComponent(pwd);
                }}
            }}

            retry();
        </script>
        </head>
        </html>
        """)

    file_path = data["file_path"]

    relative_path = file_path.replace(settings.MEDIA_URL, "").lstrip("/")
    full_path = os.path.join(settings.MEDIA_ROOT, relative_path)

    if not os.path.exists(full_path):
        return HttpResponseForbidden("File not found.")

    return FileResponse(open(full_path, "rb"))

class ClientSubmissionData(APIView):
    permission_classes = [IsAuthenticated]

    def generate_secure_file_link(self, request, file_obj, validity_days=15):
        if not file_obj:
            return "", ""

        if hasattr(file_obj, "url"):
            file_path = file_obj.url
        elif isinstance(file_obj, str):
            file_path = file_obj
        else:
            return "", ""

        password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(8))

        expiry = (timezone.now() + timedelta(days=validity_days)).timestamp()

        payload = {
            "file_path": file_path,
            "exp": expiry,
            "pwd": password,
        }

        token = signing.dumps(payload)
        secure_url = reverse("secure_file")

        return request.build_absolute_uri(f"{secure_url}?token={token}"), password

    def post(self, request):
        strengths = request.data.get("strengths") or []
        primary_skills = request.data.get("primary_skills") or []
        technical_skills = request.data.get("technical_skills") or []
        tools_frameworks = request.data.get("tools_frameworks") or []
        client_data = TblClient.objects.get(pk=request.data.get("client"))
        client_mail = client_data.client_contact_email
        client_name = client_data.client_name
        matched_id = request.data.get("matched_id")
        matched_profile_data = TblMatchedProfiles.objects.get(pk=matched_id)

        candidate_name = request.data.get("full_name", "")
        position_applied = request.data.get("position_applied", "")

        primary_skills_text = ", ".join(primary_skills)
        technical_skills_text = ", ".join(technical_skills)
        tools_text = ", ".join(tools_frameworks)
        validity_days = 15

        resume_file = TblCandidateResume.objects.get(
            pk=matched_profile_data.resume_id
        ).resume_file

        interview_obj = L0InterviewList.objects.filter(
            matched_profile=matched_profile_data
        ).first()

        interview_file = interview_obj.interview_video if interview_obj else None

        resume_link, resume_password = self.generate_secure_file_link(request, resume_file, validity_days=validity_days)
        interview_video_link, video_password = self.generate_secure_file_link(request, interview_file, validity_days=validity_days)

        strengths_html = "".join(
            f'<li style="margin-bottom:4px;">{strength}</li>'
            for strength in strengths
        )   

        summary_section = f"""
            <p>Dear,</p>
            <p>
                I am pleased to submit the profile of {candidate_name}
                for the position of {position_applied}.
            </p>
            <p>
                The candidate demonstrates strong capabilities with the following notable strengths:
            </p>
            <ul style="margin-top:5px; margin-bottom:15px;">
                {strengths_html}
            </ul>
            <p>
                The Primary Skills include {primary_skills_text}. 
            </p>
            <p>
                The Candidate also possesses Technical Expertise in {technical_skills_text}.
            </p>
            <p>
                The Candidate also has experience working with tools and frameworks such as {tools_text}.
            </p>
        """

        table_section = f"""
            <table border="1" cellpadding="4" cellspacing="0" width="100%" 
                style="border-collapse: collapse; font-size:14px;">
                
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Full Name</strong></td>
                    <td style="padding:6px;">{candidate_name}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Position Applied</strong></td>
                    <td style="padding:6px;">{position_applied}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Current Location</strong></td>
                    <td style="padding:6px;">{request.data.get("current_location", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Mobile Number</strong></td>
                    <td style="padding:6px;">{request.data.get("mobile_number", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Email</strong></td>
                    <td style="padding:6px;">{request.data.get("email", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Total Experience</strong></td>
                    <td style="padding:6px;">{request.data.get("total_experience", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Relevant Experience</strong></td>
                    <td style="padding:6px;">{request.data.get("relevant_experience", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Domain Experience</strong></td>
                    <td style="padding:6px;">{request.data.get("domain_experience", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Current Salary</strong></td>
                    <td style="padding:6px;">{request.data.get("current_salary", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Expected Salary</strong></td>
                    <td style="padding:6px;">{request.data.get("expected_salary", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Notice Period</strong></td>
                    <td style="padding:6px;">{request.data.get("notice_period", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Visa Status</strong></td>
                    <td style="padding:6px;">{request.data.get("visa_status", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>Nationality</strong></td>
                    <td style="padding:6px;">{request.data.get("nationality", "")}</td>
                </tr>
                <tr>
                    <td style="width:30%; padding:6px;"><strong>LinkedIn ID</strong></td>
                    <td style="padding:6px;">{request.data.get("linkedin_id", "")}</td>
                </tr>
            </table>
        """
        
        link_section = f"""
            <p>
                For your review, please find the candidate's detailed resume and pre-screening interview recording below:
            </p>

            <p style= font-size:14px;">
                <strong>Note:</strong> These secure links are password protected and will remain valid for 
                <strong>{validity_days} days</strong> from the date of this email.
            </p>

            <ul style="margin-top:10px; margin-bottom:15px;">
                <li>
                    <strong>Resume:</strong> 
                    <a href="{resume_link}" target="_blank" style="color:#1a73e8;">
                        Click here to view the resume
                    </a>
                    <br/>
                    <strong>Password:</strong> {resume_password}
                </li>

                <br/>

                <li>
                    <strong>Pre-Screening Interview Video:</strong> 
                    <a href="{interview_video_link}" target="_blank" style="color:#1a73e8;">
                        Click here to watch the interview
                    </a>
                    <br/>
                    <strong>Password:</strong> {video_password}
                </li>
            </ul>

            <p>
                Kindly review the above details and let us know your feedback or if any additional information is required.
            </p>
        """

        summary_email_body = f"""
            <html>
                <body>
                    {summary_section}
                    <br>
                    {table_section}
                    <br>
                    {link_section}
                </body>
            </html>
        """

        summary_email_subject = (
            f"Profile Submission: {candidate_name} for {position_applied} | {client_name}"
        )

        # dm_users = get_client_delivery_managers(client)

        # dm_emails = [
        #     dm.email for dm in dm_users
        #     if dm.email
        # ]

        mail_submission = send_email_safe(summary_email_subject, summary_email_body, client_mail, settings.EMAIL_HOST_USER, cc_emails=None, is_html=True)

        if mail_submission:
            matched_profile_data.status = "L0-Submission to Client"
            matched_profile_data.save(update_fields=["status"])

            jd = TblJobDescription.objects.get(jd_id=matched_profile_data.job_id)
            if jd.jd_stage in ["Matched", "Created", "Assigned", "Posted"]:
                jd.jd_stage = "Client Submition"
                jd.save(update_fields=["jd_stage"])

            return Response({
                "submit": True,
                "message": "Profile Submitted Successfully"
            }, status=200)
        else:
            return Response({
                "submit": False,
                "message": "Profile Submitted Failed"
            }, status=500)

@api_view(["GET"])
@permission_classes([AllowAny])
def resolve_public_token(request, token):
    link = PublicLink.objects.filter(token=token).first()

    if not link:
        return Response({"error": "Invalid token"}, status=404)

    return Response({
        "resume_id": link.resume_id,
        "jd_id": link.jd_id,
        "link_type": link.link_type
    })

class IdentifyMatchingProfilesCandidate(APIView):
    def normalize_jd_experience(self, text):
        """
        Normalize JD experience into (min_years, max_years)
        """
        if not text:
            return None, None

        text = text.lower().strip()
        text = re.sub(r"(years|year|yrs|yr)", "", text).strip()

        # 1️⃣ Range: 2-4
        match = re.search(r"(\d+(\.\d+)?)\s*-\s*(\d+(\.\d+)?)", text)
        if match:
            return float(match.group(1)), float(match.group(3))

        # 2️⃣ Plus: 2+
        match = re.search(r"(\d+(\.\d+)?)\s*\+", text)
        if match:
            return float(match.group(1)), math.inf

        # 3️⃣ Over / More than
        match = re.search(r"(over|more than)\s*(\d+(\.\d+)?)", text)
        if match:
            return float(match.group(2)), math.inf

        # 4️⃣ Within / Up to
        match = re.search(r"(within|upto|up to)\s*(\d+(\.\d+)?)", text)
        if match:
            return 0.0, float(match.group(2))

        # 5️⃣ Single value: 2 or 2.4 → (2,3)
        match = re.search(r"(\d+(\.\d+)?)", text)
        if match:
            min_val = int(float(match.group(1)))
            return min_val, min_val + 1

        return None, None
    
    def normalize_candidate_experience(self, text):
        """
        Normalize candidate experience to a SINGLE value
        """
        if not text:
            return None

        text = text.lower().strip()

        if any(keyword in text for keyword in ["fresher", "entry level", "entry-level", "junior"]):
            return 0
        
        if any(keyword in text for keyword in ["not specified"]):
            return None

        text = re.sub(r"(years|year|yrs|yr)", "", text)
        text = re.sub(r"[^\d\.\+\-\s]", "", text)
        text = text.strip()

        # Extract first number only
        match = re.search(r"(\d+(\.\d+)?)", text)
        if not match:
            return None

        value = float(match.group(1))
        return int(value)
    
    def is_years_match(self, jd_exp_text, candidate_exp_text):
        jd_min, jd_max = self.normalize_jd_experience(jd_exp_text)
        candidate_years = self.normalize_candidate_experience(candidate_exp_text)

        if jd_min is None or candidate_years is None:
            return False

        if candidate_years < jd_min:
            return False

        if jd_max is not None and jd_max != math.inf and candidate_years > jd_max:
            return False

        return True
    
    def normalize_skill(self, skill):
        return skill.lower().strip()
    
    def skill_match(self, candidate_skill, jd_skills, skill_matrix_data, TRIGRAM_THRESHOLD=0.4):
        candidate_skill = self.normalize_skill(candidate_skill)

        for skill in jd_skills:
            skill = self.normalize_skill(skill)
            for skill_name, alias_data in skill_matrix_data.items():
                for alias in alias_data:
                    if skill == self.normalize_skill(alias):
                        skill = skill_name
                        break

            # 1️⃣ Exact match
            if skill == candidate_skill:
                return True

            # 3️⃣ Trigram similarity
            similarity_qs = (
                TblCandidateResume.objects
                .annotate(
                    sim=TrigramSimilarity(
                        Value(skill),
                        Value(candidate_skill)
                    )
                )
                .values("sim")[:1]
            )

            similarity_value = similarity_qs[0]["sim"] if similarity_qs else None

            if similarity_value is not None and similarity_value >= TRIGRAM_THRESHOLD:
                print(
                    f"    ✅ TRIGRAM MATCH ACCEPTED "
                    f"(>= {TRIGRAM_THRESHOLD})"
                )
                return True
            else:
                print(
                    f"    ❌ TRIGRAM MATCH REJECTED "
                    f"(< {TRIGRAM_THRESHOLD})"
                )

        print("  ❌ NO MATCH FOUND FOR THIS JD SKILL")
        return False
    
    def candidate_skill_matches_candidate(self, candidate_skill, jd_search_pattern, skill_matrix_data):
        if self.skill_match(candidate_skill, jd_search_pattern["primary_skills"], skill_matrix_data):
            print("✅ MATCH FOUND IN PRIMARY SKILLS")
            return True

        if self.skill_match(candidate_skill, jd_search_pattern["secondary_skills"], skill_matrix_data):
            print("✅ MATCH FOUND IN SECONDARY SKILLS")
            return True
        
        if self.skill_match(candidate_skill, jd_search_pattern["keywords"], skill_matrix_data):
            print("✅ MATCH FOUND IN KEYWORDS")
            return True
        
        if self.skill_match(candidate_skill, jd_search_pattern["soft_skills"], skill_matrix_data):
            print("✅ MATCH FOUND IN SOFT SKILLS")
            return True

        print("❌ NO MATCH IN PRIMARY OR TECHNICAL SKILLS")
        return False
    
    def calculate_skill_score(self, candidate_skills, jd_obj, skill_matrix_data):
        if not candidate_skills:
            return {
                "matched": 0,
                "total": 0,
                "percentage": 0.0,
                "score": 0.0
            }

        matched = 0

        for candidate_skill in candidate_skills:
            normalized_candidate_skill = self.normalize_skill(candidate_skill)
            for skill_name, alias_data in skill_matrix_data.items():
                for alias in alias_data:
                    if normalized_candidate_skill == self.normalize_skill(alias):
                        candidate_skill = skill_name
                        break

            if self.candidate_skill_matches_candidate(candidate_skill, jd_obj.search_pattern, skill_matrix_data):
                matched += 1

        total = len(candidate_skills)
        percentage = (matched / total) * 100
        score = matched / total

        return {
            "matched": matched,
            "total": total,
            "percentage": round(percentage, 2),
            "score": round(score, 4)
        }
    
    def calculate_overall_score(self, match_data, base_weights):
        valid_fields = {
            field: weight
            for field, weight in base_weights.items()
            if weight is not None and match_data.get(field) is not None
        }

        if not valid_fields:
            return 0.0

        total_weight = sum(valid_fields.values())
        normalized_weights = {
            field: weight / total_weight
            for field, weight in valid_fields.items()
        }

        overall_score = 0.0
        for field, weight in normalized_weights.items():
            value = match_data[field]

            if isinstance(value, bool):
                score_value = 1.0 if value else 0
            else:
                score_value = float(value)

            overall_score += score_value * weight

        return round(overall_score, 4)

    def parse_salary(self, text):
        if not text:
            return (None, None)

        text = text.lower().strip()

        # Ignore junk cases
        if text in ["not specified", "negotiable", "as per company standards"]:
            return (None, None)

        # Remove commas and currency symbols
        text = re.sub(r"[₹,$,]", "", text)

        multiplier = 1

        # Detect LPA / lakh
        if "lpa" in text or "lakh" in text or "lakhs" in text:
            multiplier = 100000

        # Detect monthly
        elif "per month" in text or "monthly" in text or "pm" in text:
            multiplier = 12

        # Detect thousand (k)
        elif "k" in text:
            multiplier = 1000

        # Extract numbers (supports decimals)
        numbers = re.findall(r"\d+\.?\d*", text)

        if not numbers:
            return (None, None)

        values = [float(n) * multiplier for n in numbers]

        # Handle "above" / "minimum"
        if "above" in text or "minimum" in text or "+" in text:
            return (values[0], float("inf"))

        # Handle "upto" / "max"
        if "upto" in text or "up to" in text or "maximum" in text:
            return (0, values[0])

        # Range case
        if len(values) >= 2:
            return (min(values), max(values))

        # Single salary
        return (values[0], values[0])
    
    def salary_match(self, jd_salary, candidate_salary):
        jd_min, jd_max = self.parse_salary(jd_salary)
        c_min, c_max = self.parse_salary(candidate_salary)

        if jd_min is None or c_min is None:
            return None

        return not (c_max < jd_min or c_min > jd_max)

    def post(self, request):
        resume_id = request.data.get("resume_id")

        if not resume_id:
            return Response({"error": "resume_id is required"}, status=400)

        results = {}

        resume_data = TblCandidateResume.objects.get(resume_id=resume_id)
        resume_responibilities_embeddings = resume_data.resume_responsibilities_summary_embedding
        resume_technical_skills = resume_data.technical_skills
        resume_primary_skills = resume_data.primary_skills
        resume_tools_and_frameworks = resume_data.tools_and_frameworks
        resume_domain_skills = resume_data.domain_skills
        resume_education = resume_data.education
        resume_location = resume_data.location
        resume_salary = resume_data.salary

        matched_profiles = TblMatchedProfiles.objects.filter(resume_id=resume_id)
        processed_job_ids = set()
        for profile in matched_profiles:
            if profile.status != "matched":
                jd_id = str(profile.job_id)
                results[jd_id] = {
                    "status": profile.status,
                }
                if profile.status == "Generated L0 report":
                    results[jd_id]["l0_result"] = profile.l0_interview_summary.get("Final_Hiring_Recommendation", "")
                else:
                    results[jd_id]["l0_result"] = ""
                processed_job_ids.add(jd_id)

        jd_data = TblJobDescription.objects.exclude(
            status="Duplicate"
        ).exclude(
            jd_id__in=processed_job_ids
        )

        skill_matrix_data = {}
        json_path = os.path.join(settings.BASE_DIR, "masters", "skill_matrix.json")
        with open(json_path, "r", encoding="utf-8") as f:
            skill_matrix_data = json.load(f)

        eligible_jd_ids = []

        for jd in jd_data:
            jd_id = str(jd.jd_id)

            experience_match = self.is_years_match(
                jd.years_of_experience,
                resume_data.totalexp
            )

            if experience_match:
                eligible_jd_ids.append(jd_id)

                results[jd_id] = {
                    "experience": True,
                }

        if eligible_jd_ids:
            if not resume_primary_skills or resume_primary_skills == "Not Specified":
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}
                    results[jd_id]["primary_skills"] = None
            else:
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}

                    jd_obj = TblJobDescription.objects.get(jd_id=jd)

                    primary_skill_score = self.calculate_skill_score(
                        resume_primary_skills,
                        jd_obj,
                        skill_matrix_data
                    )

                    results[jd_id]["primary_skills"] = primary_skill_score["score"]
                    
        if eligible_jd_ids:
            if not resume_technical_skills or resume_primary_skills == "Not Specified":
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}
                    results[jd_id]["technical_skills"] = None
            else:
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}

                    jd_obj = TblJobDescription.objects.get(jd_id=jd)

                    technical_skills_score = self.calculate_skill_score(
                        resume_technical_skills,
                        jd_obj,
                        skill_matrix_data
                    )

                    results[jd_id]["technical_skills"] = technical_skills_score["score"]

        for jd in eligible_jd_ids:
            jd_id = str(jd)
            if jd_id not in results:
                results[jd_id] = {}
            results[jd_id]["responsibilities"] = None

        similarity_jds = (
            TblJobDescription.objects
            .filter(jd_id__in=eligible_jd_ids)
            .exclude(jd_responsibilities_embedding=None)
            .annotate(
                distance=CosineDistance(
                    F("jd_responsibilities_embedding"),
                    resume_responibilities_embeddings
                )
            )
            .annotate(
                responsibilities_similarity=1 - F("distance")
            )
            .filter(responsibilities_similarity__isnull=False)
        )

        for jd in similarity_jds:
            jd_id = str(jd.jd_id)
            similarity = jd.responsibilities_similarity or 0.0

            results[jd_id]["responsibilities"] = round(
                float(similarity), 4
            )

        if eligible_jd_ids:
            if not resume_tools_and_frameworks or resume_tools_and_frameworks == "Not Specified":
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}
                    results[jd_id]["tools_and_frameworks"] = None
            else:
                for jd in eligible_jd_ids:
                    jd_id = str(jd)

                    if jd_id not in results:
                        results[jd_id] = {}

                    jd_obj = TblJobDescription.objects.get(jd_id=jd)

                    matched_tools_skill = 0
                    for skill in resume_tools_and_frameworks:
                        if self.skill_match(skill, jd_obj.search_pattern["tools_and_frameworks"], skill_matrix_data):
                            matched_tools_skill += 1

                    tools_total = len(resume_tools_and_frameworks)
                    tools_percentage = (matched_tools_skill / tools_total) * 100
                    tools_score = matched_tools_skill / tools_total

                    tools_skills_result = {
                        "matched": matched_tools_skill,
                        "total": tools_total,
                        "percentage": round(tools_percentage, 2),
                        "score": round(tools_score, 4)
                    }

                    results[jd_id]["tools_and_frameworks"] = tools_skills_result["score"]

        if eligible_jd_ids:
            if not resume_domain_skills or resume_domain_skills == "Not Specified":
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}
                    results[jd_id]["domain_skills"] = None
            else:
                for jd in eligible_jd_ids:
                    jd_id = str(jd)

                    if jd_id not in results:
                        results[jd] = {}

                    jd_obj = TblJobDescription.objects.get(jd_id=jd)

                    matched_domain_skill = 0
                    for skill in resume_domain_skills:
                        if self.skill_match(skill, jd_obj.search_pattern["domain_requirements"], skill_matrix_data):
                            matched_domain_skill += 1

                    domain_total = len(resume_domain_skills)
                    domain_percentage = (matched_domain_skill / domain_total) * 100
                    domain_score = matched_domain_skill / domain_total

                    domain_requirements_score = {
                        "matched": matched_domain_skill,
                        "total": domain_total,
                        "percentage": round(domain_percentage, 2),
                        "score": round(domain_score, 4)
                    }

                    results[jd_id]["domain_skills"] = domain_requirements_score["score"]

        if eligible_jd_ids:
            if not resume_education or resume_education == "Not Specified":
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}
                    results[jd_id]["education"] = None
            else:
                candidate_education = [
                    item["degree"]
                    for item in (resume_education or [])
                    if "degree" in item and item["degree"]
                ]

                for jd in eligible_jd_ids:
                    jd_id = str(jd)

                    if jd_id not in results:
                        results[jd_id] = {}

                    jd_obj = TblJobDescription.objects.get(jd_id=jd)

                    matched_education_skill = 0
                    for education in candidate_education:
                        if self.skill_match(education, jd_obj.search_pattern["education"], skill_matrix_data):
                            matched_education_skill += 1

                    education_total = len(candidate_education)
                    education_percentage = (matched_education_skill / education_total) * 100
                    education_score = matched_education_skill / education_total

                    education_requirements_score = {
                        "matched": matched_education_skill,
                        "total": education_total,
                        "percentage": round(education_percentage, 2),
                        "score": round(education_score, 4)
                    }

                    results[jd_id]["education"] = education_requirements_score["score"]

        if eligible_jd_ids:
            if not resume_location or resume_location == "Not Specified":
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}
                    results[jd_id]["location"] = None
            else:
                for jd in eligible_jd_ids:
                    jd_id = str(jd)

                    if jd_id not in results:
                        results[jd_id] = {}

                    jd_obj = TblJobDescription.objects.get(jd_id=jd)

                    location_match = False
                    if self.skill_match(resume_location, [jd_obj.search_pattern["location"]], skill_matrix_data):
                        location_match = True

                    results[jd_id]["location"] = location_match

        if eligible_jd_ids:
            if not resume_salary or resume_salary == "Not Specified":
                for jd in eligible_jd_ids:
                    jd_id = str(jd)
                    if jd_id not in results:
                        results[jd_id] = {}
                    results[jd_id]["salary_range"] = None
            else:
                for jd in eligible_jd_ids:
                    jd_id = str(jd)

                    if jd_id not in results:
                        results[jd_id] = {}

                    jd_obj = TblJobDescription.objects.get(jd_id=jd)

                    jd_salary = jd_obj.search_pattern["salary_range"]
                    eligible = self.salary_match(jd_salary, resume_salary)

                    results[resume_id]["salary_range"] = eligible

        BASE_WEIGHTS = {
            "primary_skills": 0.6,
            "responsibilities": 0.2,
            "technical_skills": 0.05,
            "tools_and_frameworks": 0.05,
            "domain_skills": 0.025,
            "education": 0.025,
            "location": 0.025,
            "salary_range": 0.025
        }

        for _, match_data in results.items():
            match_data["overall_score"] = self.calculate_overall_score(match_data, BASE_WEIGHTS)
            if not match_data.get("status"):
                match_data["status"] = "matched"
                match_data["l0_result"] = ""

        results = dict(
            sorted(
                results.items(),
                key=lambda item: item[1].get("overall_score", 0),
                reverse=True
            )
        )

        return Response({
            "resume_id": resume_id,
            "total_jd": len(results),
            "matches": results
        })

def run_matching_engine_candidate(resume_id):

    view = IdentifyMatchingProfilesCandidate()
    payload = {
        "resume_id": resume_id,
    }
    fake_request = type("obj", (object,), {
        "data": payload
    })

    response = view.post(fake_request)
    return response.data
    
@api_view(['POST'])
def get_matched_profiles_candidate(request):
    resume_id = request.data.get("resume_id")
    regen_filter = request.data.get("regen_filter", False)

    if not regen_filter:
        existing = TblMatchedProfilesCandidate.objects.filter(resume_id=resume_id)

        for m in existing:
            if not TblJobDescription.objects.filter(jd_id=m.jd_id).exists():
                m.delete()

        l0_result_map = {}
        for data in existing:
            if data.status == "Generated L0 report":
                matched_profile_data = TblMatchedProfiles.objects.filter(resume_id=resume_id, job_id=data.jd_id).first()
                l0_result_map[data.jd_id] = matched_profile_data.l0_interview_summary.get("Final_Hiring_Recommendation", "")
            else:
                l0_result_map[data.jd_id] = None

        if existing.exists():
            serializer = TblMatchedProfilesCandidateSerializer(existing, many=True)
            response_data = serializer.data

            for item in response_data:
                item["l0_result"] = l0_result_map.get(item["jd_id"])

            return Response({
                "source": "db",
                "data": response_data
            })
    else:
        TblMatchedProfilesCandidate.objects.filter(
            resume_id=resume_id        
        ).delete()

    match_response = run_matching_engine_candidate(resume_id)
    matches = match_response.get("matches", {})

    if not matches:
        existing = TblMatchedProfilesCandidate.objects.filter(resume_id=resume_id)

        l0_result_map = {}
        for data in existing:
            if data.status == "Generated L0 report":
                matched_profile_data = TblMatchedProfiles.objects.filter(resume_id=resume_id, job_id=data.jd_id).first()
                l0_result_map[data.jd_id] = matched_profile_data.l0_interview_summary.get("Final_Hiring_Recommendation", "")
            else:
                l0_result_map[data.jd_id] = None

        serializer = TblMatchedProfilesCandidateSerializer(existing, many=True)
        response_data = serializer.data

        for item in response_data:
            item["l0_result"] = l0_result_map.get(item["jd_id"])
                
        return Response({
            "source": "engine",
            "data": response_data
        })

    sorted_jd = sorted(
        matches.items(),
        key=lambda x: x[1]["overall_score"],
        reverse=True
    )

    l0_result_map = {}

    for jd_id, d in sorted_jd:
        l0_result_map[jd_id] = d.get("l0_result")

        if TblMatchedProfilesCandidate.objects.filter(
            resume_id=resume_id,
            jd_id=jd_id
        ).exists():
            continue

        TblMatchedProfilesCandidate.objects.create(
            resume_id=resume_id,
            jd_id=jd_id,
            overall_score=d.get("overall_score", 0),
            primaryskill_score=d.get("primary_skills", 0),
            technicalskill_score=d.get("technical_skills", 0),
            responsibilities_score=d.get("responsibilities", 0),
            tech_framework_score=d.get("tools_and_frameworks", 0),
            experience_score=d.get("experience", True),
            status=d.get("status", ""),
        )

    final_queryset = TblMatchedProfilesCandidate.objects.filter(resume_id=resume_id)

    serializer = TblMatchedProfilesCandidateSerializer(final_queryset, many=True)
    response_data = serializer.data

    for item in response_data:
        jd_id = str(item["jd_id"])
        item["l0_result"] = l0_result_map.get(jd_id)

    return Response({
        "source": "engine",
        "data": response_data
    })

def public_job_share(request, token):
    job = get_object_or_404(TblJobDescription, public_token=token)

    user_agent = request.META.get("HTTP_USER_AGENT", "").lower()

    if any(bot in user_agent for bot in ["facebookexternalhit", "linkedinbot", "twitterbot", "whatsapp"]):
        return render(request, "masters/public_job_share.html", {"job": job})

    return redirect(f"https://career.itconnectus.com/career/apply/{job.jd_id}")

@api_view(["POST"])
@permission_classes([AllowAny])
def email_share_job(request):
    job_id = request.data.get("job_id")
    friend_emails = request.data.get("friend_emails")
    your_name = request.data.get("your_name")
    your_email = request.data.get("your_email")

    job = TblJobDescription.objects.get(jd_id=job_id)

    email_list = [email.strip() for email in friend_emails.split(",")]

    subject, message = get_email_template("job_share_email", is_html=False)

    subject = subject.format(
        your_name=your_name
    )

    message = message.format(
        your_name=your_name,
        job_title=job.job_title,
        job_location=job.job_location,
        apply_url=f"https://career.itconnectus.com/career/apply/{job.jd_id}"
    )

    send_mail(
        subject,
        message,
        settings.EMAIL_HOST_USER,
        email_list,
        fail_silently=False,
    )

    JobEmailShare.objects.create(
        job=job,
        your_name=your_name,
        your_email=your_email,
        friend_emails=friend_emails
    )

    return Response({"message": "Email sent"})

@api_view(["POST"])
@permission_classes([AllowAny])
def candidate_consent_approval(request):
    try:
        jd_id = request.data.get("jd_id")
        resume_id = request.data.get("resume_id")

        matched_obj = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()
        
        if matched_obj.status == "Generated L0 report":
            return Response({
                "error": "L0 Report Generated"}, 
                status=500
            )
        
        l0_exist_obj = L0InterviewList.objects.filter(
            status="Completed",
            matched_profile__resume_id=resume_id,
            interview_score__isnull=False
        ).order_by("-created_at").first()

        final_source_path = l0_exist_obj.interview_video.path

        new_folder = os.path.join(
            settings.MEDIA_ROOT,
            "l0_interview_videos",
            str(matched_obj.id)
        )
        os.makedirs(new_folder, exist_ok=True)

        final_file_extension = os.path.splitext(final_source_path)[1]

        final_filename = f"L0_jd_{jd_id}_resume_{resume_id}_final_video{final_file_extension}"
        final_video_path = os.path.join(new_folder, final_filename)

        if os.path.exists(final_source_path):
            shutil.copy2(final_source_path, final_video_path)

        relative_final_video_path = os.path.join(
            "l0_interview_videos",
            str(matched_obj.id),
            final_filename
        )

        l0_created_obj = L0InterviewList.objects.create(
            status=l0_exist_obj.status,
            interview_score=l0_exist_obj.interview_score,
            interview_completed_time=l0_exist_obj.interview_completed_time,
            matched_profile=matched_obj,
            interview_video=relative_final_video_path
        )

        l0_exist_session_obj = L0InterviewSessions.objects.filter(l0_interview=l0_exist_obj).order_by("-created_at")
        for l0_session in l0_exist_session_obj:
            session_source_path = l0_session.recorded_video.path

            session_video_filename = os.path.basename(session_source_path)
            session_file_extension = os.path.splitext(session_video_filename)[1]
            if "_Q_" in session_video_filename:
                question_part = session_video_filename.split("_Q_", 1)[1]
                question_part = "_Q_" + question_part.replace(session_file_extension, "")
            else:
                question_part = ""

            session_filename = f"L0_jd_{jd_id}_resume_{resume_id}{question_part}{session_file_extension}"

            session_video_path = os.path.join(new_folder, session_filename)

            if os.path.exists(session_source_path):
                shutil.copy2(session_source_path, session_video_path)

            relative_session_video_path = os.path.join(
                "l0_interview_videos",
                str(matched_obj.id),
                session_filename
            )
            
            L0InterviewSessions.objects.create(
                question=l0_session.question,
                answer=l0_session.answer,
                recorded_video=relative_session_video_path,
                matched_profile=matched_obj,
                l0_interview=l0_created_obj,
            )

        matched_obj.status = "Generated L0 report"
        matched_obj.l0_interview_summary = l0_exist_obj.interview_score
        matched_obj.save(update_fields=["l0_interview_summary", "status"])

        candidate_resume = TblCandidateResume.objects.get(pk=resume_id)
        candidate_resume.l0_interview_data = l0_created_obj
        candidate_resume.l0_interview_summary = l0_exist_obj.interview_score
        candidate_resume.save(update_fields=["l0_interview_data", "l0_interview_summary"])

        return Response({"status": "success"}, status=200)
    except Exception as e:
        import traceback
        traceback.print_exc()
        return Response({"error": "Failed " + str(e)}, status=404)

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def dashboard_filter_data(request):

    user = request.user

    jd_ids = DBAuthFilters.get_jd_by_auth_user(user)

    jds = TblJobDescription.objects.filter(jd_id__in=jd_ids).values(
        "jd_id", "jd_display_id", "job_title"
    )

    jd_list = [
        {
            "id": jd["jd_id"],
            "jd_display_id": jd["jd_display_id"],
            "job_title": jd["job_title"]
        }
        for jd in jds
    ]

    client_ids = DBAuthFilters.get_client_by_auth_user(user)

    clients = TblClient.objects.filter(
        client_id__in=client_ids,
        status="Active"
    ).values("client_id", "client_name")

    client_list = [
        {
            "id": c["client_id"],
            "name": c["client_name"]
        }
        for c in clients
    ]

    recruiters = User.objects.filter(
        groups__name="Recruiter",
        is_active=True
    ).values("id", "first_name", "last_name", "username")

    recruiter_list = [
        {
            "id": r["id"],
            "name": f'{r["first_name"]} {r["last_name"]}'.strip() or r["username"]
        }
        for r in recruiters
    ]

    managers = User.objects.filter(
        groups__name="Delivery Manager (DM)",
        is_active=True
    ).values("id", "first_name", "last_name", "username")

    manager_list = [
        {
            "id": m["id"],
            "name": f'{m["first_name"]} {m["last_name"]}'.strip() or m["username"]
        }
        for m in managers
    ]

    return Response({
        "clients": client_list,
        "recruiters": recruiter_list,
        "managers": manager_list,
        "jds": jd_list
    })
    
class DashboardOverview(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):

        user = request.user

        jd_ids = request.GET.getlist("jd_ids")
        client_ids = request.GET.getlist("client_ids")
        recruiter_ids = request.GET.getlist("recruiter_ids")
        start_date = request.GET.get("start_date")
        end_date = request.GET.get("end_date")

        authorized_jd_ids = DBAuthFilters.get_jd_by_auth_user(user)

        jd_query = TblJobDescription.objects.filter(
            jd_id__in=authorized_jd_ids
        )

        if jd_ids:
            jd_query = jd_query.filter(jd_id__in=jd_ids)

        if client_ids:
            jd_query = jd_query.filter(client_id__in=client_ids)

        if recruiter_ids:
            jd_query = jd_query.filter(
                assignjd__user__id__in=recruiter_ids
            )

        if start_date and end_date:
            jd_query = jd_query.filter(
                jd_date__range=[start_date, end_date]
            )

        jd_query = jd_query.distinct()

        jd_ids_list = jd_query.values_list("jd_id", flat=True)

        active_jds = jd_query.filter(status="Open").count()

        clients = jd_query.values("client_id").distinct().count()

        recruiters = User.objects.filter(
            groups__name="Recruiter",
            is_active=True
        ).count()

        delivery_managers = User.objects.filter(
            groups__name="Delivery Manager (DM)",
            is_active=True
        ).count()

        candidates = TblMatchedProfiles.objects.filter(
            job_id__in=jd_ids_list
        ).count()

        jd_stage_data = (
            jd_query.values("jd_stage")
            .annotate(count=Count("jd_id"))
        )

        client_stage = (
            jd_query.values("client_id", "client_id__client_name", "jd_stage")
            .annotate(count=Count("jd_id"))
            .order_by("client_id__client_name")
        )

        funnel = (
            TblMatchedProfiles.objects
            .filter(job_id__in=jd_ids_list)
            .values("status")
            .annotate(count=Count("id"))
        )

        return Response({
            "stats": {
                "active_jds": active_jds,
                "clients": clients,
                "recruiters": recruiters,
                "delivery_managers": delivery_managers,
                "candidates": candidates
            },
            "jd_stage_data": list(jd_stage_data),
            "client_stage": list(client_stage),
            "candidate_funnel": list(funnel)
        })

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def delivery_manager_dashboard(request):

    user = request.user
    dm_id = request.query_params.get("dm_id")

    dms = User.objects.filter(
        groups__name="Delivery Manager (DM)",
        is_active=True
    )

    response_data = []

    for dm in dms:

        jd_ids = DBAuthFilters.get_jd_by_auth_user(dm)

        jds = TblJobDescription.objects.filter(jd_id__in=jd_ids)

        total_jds = jds.count()

        stage_data = (
            jds.values("jd_stage")
               .annotate(count=Count("jd_stage"))
        )

        client_ids = list(
            set(
                jds.values_list("client_id", flat=True)
            )
        )

        response_data.append({
            "dm_id": dm.id,
            "dm_name": f"{dm.first_name} {dm.last_name}".strip() or dm.username,
            "total_jds": total_jds,
            "client_ids": client_ids,
            "stages": list(stage_data)
        })

    if dm_id:
        response_data = [
            r for r in response_data
            if str(r["dm_id"]) == dm_id
        ]

    return Response({
        "delivery_managers": response_data
    })

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def client_dashboard_stats(request):
    """
    Returns JD distribution stats for all Active Clients or a selected client.
    """
    user = request.user
    client_id = request.query_params.get("client_id")

    client_ids_auth = DBAuthFilters.get_client_by_auth_user(user)
    clients = TblClient.objects.filter(status="Active", client_id__in=client_ids_auth)

    if client_id:
        clients = clients.filter(pk=client_id)

    response_data = []

    for client in clients:
        jds = TblJobDescription.objects.filter(client_id=client)
        
        total_jds = jds.count()
        
        stage_data = (
            jds.values("jd_stage")
               .annotate(count=Count("jd_stage"))
               .order_by("jd_stage")
        )

        jd_details = jds.values("job_title", "jd_stage")

        response_data.append({
            "id": client.client_id,
            "name": client.client_name,
            "total_jds": total_jds,
            "stages": list(stage_data),
            "jd_details": [{"title": jd["job_title"], "stage": jd["jd_stage"]} for jd in jd_details]
        })

    return Response({"clients": response_data})

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def recruiter_dashboard_stats(request):
    """
    Returns JD distribution stats for all Recruiters.
    """
    recruiter_id = request.query_params.get("recruiter_id")

    recruiters = User.objects.filter(groups__name="Recruiter", is_active=True)

    if recruiter_id:
        recruiters = recruiters.filter(pk=recruiter_id)

    response_data = []

    for recruiter in recruiters:
        jd_ids = DBAuthFilters.get_jd_by_auth_user(recruiter)
        jds = TblJobDescription.objects.filter(jd_id__in=jd_ids)

        total_jds = jds.count()

        stage_data = (
            jds.values("jd_stage")
               .annotate(count=Count("jd_stage"))
               .order_by("jd_stage")
        )

        jd_details = jds.values("job_title", "jd_stage")

        response_data.append({
            "id": recruiter.id,
            "name": f"{recruiter.first_name} {recruiter.last_name}".strip() or recruiter.username,
            "total_jds": total_jds,
            "stages": list(stage_data),
            "jd_details": [{"title": jd["job_title"], "stage": jd["jd_stage"]} for jd in jd_details]
        })

    return Response({"recruiters": response_data})

class AuditLogView(APIView):

    def get(self, request):
        model_name = request.GET.get("model")
        actor = request.GET.get("actor")

        page = int(request.GET.get("page", 1))
        page_size = int(request.GET.get("page_size", 24))

        logs = LogEntry.objects.all().order_by("-timestamp")

        if model_name:
            try:
                content_type = ContentType.objects.get(model=model_name)
                logs = logs.filter(content_type=content_type)
            except ContentType.DoesNotExist:
                return Response({"error": "Invalid model name"}, status=400)

        if actor:
            logs = logs.filter(actor__username=actor)

        total_count = logs.count()

        start = (page - 1) * page_size
        end = start + page_size

        logs = logs[start:end]

        serializer = AuditLogSerializer(logs, many=True)
        users = (
            LogEntry.objects
            .exclude(actor=None)
            .order_by()
            .values_list("actor__username", flat=True)
            .distinct()
        )

        return Response({
            "count": total_count,
            "page": page,
            "page_size": page_size,
            "users": list(users),
            "results": serializer.data
        })

class ScreenScrapeView(APIView):
    def post(self, request):

        username = request.data.get("username")
        password = request.data.get("password")

        if not username or not password:
            return Response(
                {"error": "username and password required"},
                status=status.HTTP_400_BAD_REQUEST
            )

        try:

            data = run_scraper(username, password)

            return Response({
                "message": "Scraping completed",
                "data": data
            })

        except Exception as e:

            return Response(
                {"error": str(e)},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )
