import string, random
from urllib.parse import quote
import requests
import re
import difflib
import html
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from django.contrib.auth.models import User
from django.core.mail import EmailMessage
from django.conf import settings
from .models import UserClient
import os
from .models import UserClient, TblCandidateResume, TblJobDescription, EmailQueue, WhatsAppQueue, SMSQueue, AssignJd, TblMatchedProfiles, PublicLink, TblClient
import logging
import subprocess
import tempfile
import zipfile
from lxml import etree
import pdfplumber
import pandas as pd
from django.utils import timezone
from datetime import timedelta
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from smtplib import SMTPRecipientsRefused
import traceback
import uuid
from collections import defaultdict
import json

BASE_DIR = os.path.dirname(os.path.dirname(__file__))

TEMPLATE_FILE = os.path.join(BASE_DIR, "masters", "mail_configs", "email_templates.json")

with open(TEMPLATE_FILE) as f:
    EMAIL_CONFIG = json.load(f)

def get_email_template(key, is_html=False):

    template = EMAIL_CONFIG["emails"][key]

    subject = template["title"]
    body = template["body"]

    if is_html:
        footer = EMAIL_CONFIG["footer"]["html"]
        body = body.replace("\n", "<br>")
        body = f"<html><body style='font-family:Arial'>{body}{footer}</body></html>"
    else:
        footer = EMAIL_CONFIG["footer"]["text"]
        body = body + "\n\n" + footer

    return subject, body

logger = logging.getLogger(__name__)

def generate_public_link(resume_id=None, jd_id=None, link_type="career"):
    token = uuid.uuid4().hex[:18]

    print("========== GENERATING PUBLIC LINK ==========")
    print("Generated Token:", token)
    print("Resume ID:", resume_id)
    print("JD ID:", jd_id)
    print("Link Type:", link_type)

    obj = PublicLink.objects.create(
        token=token,
        resume_id=resume_id,
        jd_id=jd_id,
        link_type=link_type
    )

    print("Saved Object ID:", obj.id)
    print("===========================================")

    return token

def send_email_safe(subject, body, to_email, from_email, cc_emails=None, is_html=False):

    if isinstance(to_email, str):
        to_email = [to_email]

    valid_to = []
    for email in to_email:
        try:
            validate_email(email)
            valid_to.append(email)
        except ValidationError:
            logger.warning(f"[EMAIL-SEND] INVALID_TO_SKIPPED email={email}")

    valid_cc = []
    if cc_emails:
        if isinstance(cc_emails, str):
            cc_emails = [cc_emails]

        for cc in cc_emails:
            try:
                validate_email(cc)
                valid_cc.append(cc)
            except ValidationError:
                logger.warning(f"[EMAIL-SEND] INVALID_CC_SKIPPED cc={cc}")

    if not valid_to:
        logger.error("[EMAIL-SEND] No valid recipient emails")
        return False

    try:
        email = EmailMessage(
            subject=subject,
            body=body,
            from_email=from_email,
            to=valid_to,
            cc=valid_cc
        )

        if is_html:
            email.content_subtype = "html"

        email.send(fail_silently=False)

        logger.info(
            f"[EMAIL-SEND] status=SENT to={valid_to} cc={valid_cc}"
        )

        return True

    except SMTPRecipientsRefused as e:
        logger.error(
            f"[EMAIL-SEND] status=FAILED reason=INVALID_RECIPIENT error={e}"
        )

        EmailQueue.objects.create(
            to_email=",".join(valid_to),
            subject=subject,
            body=body,
            from_email=from_email,
            status="FAILED",
            retry_count=0,
            last_error=str(e),
        )

        return False

    except Exception as e:

        next_retry = timezone.now() + timedelta(minutes=5)

        logger.warning(
            f"[EMAIL-SEND] status=QUEUED to={valid_to} next_retry_at={next_retry} error={e}"
        )

        EmailQueue.objects.create(
            to_email=",".join(valid_to),
            subject=subject,
            body=body,
            from_email=from_email,
            status="PENDING",
            retry_count=1,
            next_retry_at=next_retry,
            last_error=str(e),
        )

        return False

    except SMTPRecipientsRefused as e:
        print(f"[EMAIL-SEND] status=FAILED reason=INVALID_RECIPIENT to={to_email} error={e}")
        logger.error(
            f"[EMAIL-SEND] status=FAILED reason=INVALID_RECIPIENT to={to_email} error={e}"
        )

        EmailQueue.objects.create(
            to_email=to_email,
            subject=subject,
            body=body,
            from_email=from_email,
            status="FAILED",
            retry_count=0,
            last_error=str(e),
        )
        return False

    except Exception as e:
        next_retry = timezone.now() + timedelta(minutes=5)

        print(
            f"[EMAIL-SEND] status=QUEUED to={to_email} "
            f"next_retry_at={next_retry} error={e}"
        )
        logger.warning(
            f"[EMAIL-SEND] status=QUEUED to={to_email} "
            f"next_retry_at={next_retry} error={e}"
        )

        EmailQueue.objects.create(
            to_email=to_email,
            subject=subject,
            body=body,
            from_email=from_email,
            status="PENDING",
            retry_count=1,
            next_retry_at=next_retry,
            last_error=str(e),
        )
        return False
    
logger = logging.getLogger(__name__)

def send_whatsapp_safe(to_phone, template_name, template_params):

    url = f"https://graph.facebook.com/v19.0/{settings.WHATSAPP_PHONE_NUMBER_ID}/messages"

    payload = {
        "messaging_product": "whatsapp",
        "to": to_phone,
        "type": "template",
        "template": {
            "name": template_name,
            "language": {"code": "en"},
            "components": [
                {
                    "type": "body",
                    "parameters": [
                        {"type": "text", "text": str(p)}
                        for p in template_params
                    ],
                }
            ],
        },
    }

    headers = {
        "Authorization": f"Bearer {settings.WHATSAPP_ACCESS_TOKEN}",
        "Content-Type": "application/json",
    }

    try:
        res = requests.post(url, json=payload, headers=headers, timeout=10)

        if res.status_code == 200:
            logger.info(f"[WHATSAPP] SENT to={to_phone}")
            return True

        raise Exception(res.text)

    except Exception as e:
        next_retry = timezone.now() + timedelta(minutes=5)

        WhatsAppQueue.objects.create(
            to_phone=to_phone,
            template_name=template_name,
            template_params=template_params,
            status="PENDING",
            retry_count=1,
            next_retry_at=next_retry,
            last_error=str(e),
        )

        logger.warning(f"[WHATSAPP] QUEUED to={to_phone} error={e}")
        return False
    
def send_sms_safe(to_phone, message):

    # Clean number
    to_phone = (
        str(to_phone)
        .replace("+", "")
        .replace(" ", "")
        .replace("-", "")
    )

    if not to_phone.startswith("91"):
        to_phone = "91" + to_phone

    payload = {
        "route": "q",
        "message": message,
        "language": "english",
        "numbers": to_phone,
    }

    headers = {
        "authorization": settings.FAST2SMS_API_KEY,
        "Content-Type": "application/json",
    }

    try:
        res = requests.post(
            settings.FAST2SMS_URL,
            json=payload,
            headers=headers,
            timeout=10
        )

        if res.status_code == 200:
            response_data = res.json()

            if response_data.get("return") is True:
                logger.info(f"[SMS] SENT to={to_phone}")
                return True

        raise Exception(res.text)

    except Exception as e:
        next_retry = timezone.now() + timedelta(minutes=5)

        SMSQueue.objects.create(
            to_phone=to_phone,
            message=message,
            status="PENDING",
            retry_count=1,
            next_retry_at=next_retry,
            last_error=str(e),
        )

        logger.warning(f"[SMS] QUEUED to={to_phone} error={e}")
        return False

def generate_password(length=12):
        # Define characters to use for password generation
        characters = string.ascii_letters + string.digits #+ string.punctuation

        # Generate password using random choice
        password = ''.join(random.choice(characters) for _ in range(length))
        
        return password

def generate_apply_link(jd):
    return f"{settings.CAREER}/apply/{jd.apply_token_url}"

def delete_linkedin_post(access_token, post_urn):
    encoded_urn = quote(post_urn, safe="")
    url = f"https://api.linkedin.com/v2/ugcPosts/{encoded_urn}"

    headers = {
        "Authorization": f"Bearer {access_token}",
        "X-Restli-Protocol-Version": "2.0.0"
    }

    res = requests.delete(url, headers=headers)
    return res.status_code == 204

def post_to_foundit(config, jd, text):

    url = "https://api.foundit.in/v1/job/post" 
    
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Basic {config.portal_password}" 
    }
    
    payload = {
        "username": config.portal_username,
        "job_title": jd.job_title,
        "job_description": text,
        "location": jd.job_location,
        "experience_min": jd.years_of_experience,
    }
    

    return {"status": "success", "foundit_job_id": "FI_" + str(jd.pk)}

class LlmPrompts:
    def resume_extraction_prompt(resume_text):
        return f"""
            Role:
            Act as an Expert HR Data Parser and Resume Analyst.
            Input Data:
            I will provide you with the raw text from an EMAIL BODY containing a candidate's resume or profile. The text may include email headers, signatures, forwarding history, and conversational text (e.g., "Please find attached...").
            Your Task:
            Noise Filtering: actively ignore email metad ata, "Sent from my iPhone" footers, legal disclaimers, and conversational introductions. Isolate the actual candidate profile data.
            Semantic Analysis:
            Standardization: transfer the specific to given JSON format.
            Format: Output the data strictly into the JSON format defined below.
            Rules for Extraction:
            Name: Identify the candidate's name, not the recruiter's or sender's name.
            Age : Calculate the age from the date of birth, blank if date for birth is not specified
            Gender : Gender of the candidate either Male or FeMale, blank if not gende is not available 	
            Employment type : Current Job type like Contract or Employee or Part Time or Freelancer, leave blank if not available 
            Dates: Ensure dates are formatted consistently (e.g., "Month Year" or "Year").
            Title: Identify from the resume. if resume doesn't have the title, create a suitable title for the resume.
            Primary Skills: more experience skills
            Responsibility Summary: summary of all the skills, work experience responsibilities and project experience.
            Domain Skills: Industry or Buisiness Related skills
            Missing Data: If a specific field is not found, return "Not Specified" (do not use null or empty strings).
            Output Format:
            Return ONLY a single valid JSON object. Do not include any conversational text.
            ```json
            {{
                "personal_information": {{
                    "first_name": "String",
                    "last_name" : "String",
                    "full_name": "String",
                    "email": "String",
                    "mobile": "String",
                    "linkedin_url": "String",
                    "portfolio_url": "String",
                    "location": "String",
					"age": "String",
					"gender": "String",
					"notice_period": "String",
					"empoyment_type"
                }},
                "title": "String",
                "total_exp": "String",
                "salary" : "String",
                "professional_summary": "String",
                "responsibility_summary": "String',
                "primary_skills": ["String"],
                "skills": {{
                    "technical_skills": ["String"],
                    "soft_skills": ["String"],
                    "tools_and_frameworks": ["String"]
                }},
                "domain_skills": ["String"],
                "work_experience": [
                    {{
                        "job_title": "String",
                        "company": "String",
                        "location": "String",
                        "start_date": "String",
                        "end_date": "String",
                        "is_current": Boolean,
                        "responsibilities": "String"
                    }}
                ],
                "education": [
                    {{
                        "degree": "String",
                        "institution": "String",
                        "location": "String",
                        "graduation_year": "String"
                    }}
                ],
                "projects": [
                    {{
                        "project_name": "String",
                        "description": "String",
                        "technologies_used": ["String"]
                    }}
                ],
                "certifications": [
                    {{
                        "name": "String",
                        "issuer": "String",
                        "year": "String"
                    }}
                ],
                "languages": ["String"]
            }}
            ```
            Input Email Body:	
            {resume_text}
        """

    def jd_extraction_prompt(final_body_text):
        jd_generate_prompt = f"""
            Role:
            Act as an Expert Technical Recruiter and Data Parsing AI.
            Input Data:
            I will provide you with the raw text from an EMAIL BODY or PDF or Word, which containing an  unstructured Job Description (JD).
            Your Task:
            Analyze: Read the text and identify key job details. Ignore email metadata (signatures, "Sent from my iPhone", greetings, and thread headers).
            Semantic Extraction & Tuning:
            Interpret Meaning: If the email says, "looking for a rockstar in Python," interpret this as "Expert proficiency in Python" under skills.
            Standardize: Expand only universal recruitment abbreviations (e.g., "wfh" -> "Remote"). Strictly preserve Client Acronyms, Project Codes, and Proper Nouns exactly as written (e.g., keep "LTA", "JPMC", "DBS" as is; do not expand them).
            Categorize: Semantically distinguish between what is "Required" (Must have) vs. "Preferred" (Nice to have/Bonus).
            Refine: Clean up the grammar and tone of the extracted text to be professional and suitable for a formal JD.
            Audit Transformation: Track every specific change made, including which input headers were mapped to which output keys.
            Format: Output the data strictly into the specific JSON format defined below.
            Rules for Extraction:
            If a field is not mentioned in the email, return "Not Specified".
            jd_date: Extract the date from the email text if present, otherwise use "Current".
            job_title : Retain the given title in the JD
            ai_title : Tuned and Generated Title from the JD
            job_type: type of Job in the JD example Contract or Employee or etc..
            job_id: reference for the JD or JD identifier
            duration : Duration of the Job requirement in months, if not specified use 24+ months
            about_company: If not explicitly stated, try to infer the company name/domain from the context, or leave as "Not Specified".
            no_of_open_positions: If not explicitly stated, give default value as 1
            primary_skills: highly important skills required for the JD
            secondary_skills: technical skills exclude the primary skills
            domain_requirements: must specified as domain skills in the JD
            Specific Instruction for "changes" field:
            You must generate a semicolon-separated string listing THREE types of changes:
            MAPPING: Which input section was mapped to which JSON key (e.g., "Mapped 'Key Responsibilities' to 'responsibilities'").
            CORRECTION: Content fixes (e.g., "Corrected '58 years' to '5-8 Years'").
            INFERENCE: Data derived from context (e.g., "Inferred 'LTA' is the Domain/Client").
            Output Format:
            Return ONLY a single valid JSON object. Do not include any conversational text.
            ```json
            {{
            "ai_title" : "String",
            "job_title" : "String",
            "job_type" : "String",
            "job_id" : "String",
            "about_company"  : "String",
            "no_of_open_positions" : "String",
            "job_summary"  : "String",
            "responsibilities" : "String",
            "domain_requirements" : "String",
            "certification_requirements" : "String",
            "security_clearance_requirements" : "String",
            "years_of_experience" : "String",
            "duration" : "String",
            "onsite_job" : "String",
            "job_location" : "String",
            "salary_range" : "String",
            "required_qualifications" : "String",
            "preferred_qualifications" : "String",
            "working_hours" : "String",
            "benefits" : "String",
            "requirement_priority" : "String",
            "search_pattern" : {{
                "job_title": "String",
                "location": "String",
                "education": ["String"],
                "primary_skills": ["String"],
                "secondary_skills": ["String"],
                "soft_skills": ["String"],
                "tools_and_frameworks": ["String"],
                "salary_range": "String",
                "keywords": ["String"],
                "domain_requirements": ["String"]
            }},
            "changes": "String",
            }}
            ```

            Input Email Body:
            {final_body_text}
        """
        return jd_generate_prompt

    def jd_search_pattern_prompt(final_body_text):
        return f"""
            Role:
            Act as an Expert Technical Recruiter and Data Parsing AI.
            Input Data:
            I will provide you with the JSON text, which containing list of structured Job Description (JD).
            Your Task:
            Analyze: Read each JD in the JSON and generate the search patterns in specific JSON structure
            Format: Output the data strictly into the specific JSON format defined below.
            Rules for Extraction:
            jd_id : Retain the given jd_id in the JD
            job_title : Retain the given title in the JD
            primary_skills: highly important skills required for the JD
            secondary_skills: technical skills exclude the primary skills
            domain_requirements: must specified as domain skills in the JD
            Output Format:
            Return list of valid JSON object. Do not include any conversational text.
            ```json
            [
                {{
                    "jd_id" : "String",
                    "search_pattern" : {{
                        "job_title": "String",
                        "location": "String",
                        "education": ["String"],
                        "primary_skills": ["String"],
                        "secondary_skills": ["String"],
                        "soft_skills": ["String"],
                        "tools_and_frameworks": ["String"],
                        "salary_range": "String",
                        "keywords": ["String"],
                        "domain_requirements": ["String"]
                    }}
                }}
            ]
            ```
            
            Input JSON:
            {final_body_text}
        """
    
    def l0_interview_summary_prompt(interview_data):
        return f"""
            Generate a structured JSON report  for this interview.
            Interview Logs: {interview_data}

            Output Format:
            ``` json 
            {{
                "Candidate_Strengths" : "String[]",
                "Candidate_Weaknesses": "String[]",
                "Score": {{
                    "communication": 'String", 
                    "attitude": "String", 
                    "Technical": "String"
                }},
                "Final_Hiring_Recommendation": "String (Excellent/Very Good/Good/Fair/Poor)"
            }}
            ```
        """

def normalize_resume_title(title: str) -> str:
    if not title:
        return ""

    title = title.strip().lower()

    # title = re.sub(r"\(.*?\)", "", title)
    # title = re.sub(r"\[.*?\]", "", title)
    # title = re.sub(r"\{.*?\}", "", title)

    title = re.sub(r"[_\-\/]+", " ", title)

    title = re.sub(r"[^a-z0-9 ]+", "", title)

    title = re.sub(r"\s+", " ", title).strip()

    return title

def highlight_inline_diff(old_line, new_line):
    """
    Returns (old_html, new_html)
    old_html: highlights removed/changed words (blue)
    new_html: highlights added/changed words (green)
    """

    # split by words but keep spaces/punctuation
    old_tokens = re.findall(r'\w+|\s+|[^\w\s]', old_line)
    new_tokens = re.findall(r'\w+|\s+|[^\w\s]', new_line)

    sm = difflib.SequenceMatcher(None, old_tokens, new_tokens)

    old_result = []
    new_result = []

    for tag, i1, i2, j1, j2 in sm.get_opcodes():
        old_part = "".join(old_tokens[i1:i2])
        new_part = "".join(new_tokens[j1:j2])

        if tag == "equal":
            old_result.append(html.escape(old_part))
            new_result.append(html.escape(new_part))

        elif tag == "replace":
            old_result.append(f"<span style='background:#dbeafe;border-radius:4px;padding:1px 3px;'>{html.escape(old_part)}</span>")
            new_result.append(f"<span style='background:#dcfce7;border-radius:4px;padding:1px 3px;'>{html.escape(new_part)}</span>")

        elif tag == "delete":
            old_result.append(f"<span style='background:#dbeafe;border-radius:4px;padding:1px 3px;'>{html.escape(old_part)}</span>")

        elif tag == "insert":
            new_result.append(f"<span style='background:#dcfce7;border-radius:4px;padding:1px 3px;'>{html.escape(new_part)}</span>")

    return "".join(old_result), "".join(new_result)

def build_facebook_post_text(jd):
    lines = [
        "🚀 We are Hiring!",
        f"🔹 Role: {jd.job_title}",
        f"📍 Location: {jd.job_location}",
        f"🧠 Experience: {jd.years_of_experience}",
    ]

    if jd.salary_range:
        lines.append(f"💰 Salary: {jd.salary_range}")

    apply_url = generate_apply_link(jd.jd_display_id)

    lines.extend([
        f"👉 Apply here: {apply_url}",
        f"🆔 Ref ID: {jd.jd_display_id}"
    ])

    return "\n\n".join(lines), apply_url

def calculate_title_similarity(title1: str, title2: str) -> float:
    if not title1 or not title2:
        return 0.0

    vectorizer = TfidfVectorizer(stop_words="english")
    tfidf_matrix = vectorizer.fit_transform([title1, title2])

    similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
    return round(similarity * 100, 2)

def safe_similarity(ai_title, job_title):
    if not ai_title or not job_title:
        return None

    ai_title = ai_title.strip()
    job_title = job_title.strip()

    if len(ai_title) < 3 or len(job_title) < 3:
        return None

    return calculate_title_similarity(ai_title, job_title)

def get_client_delivery_managers(client):
    """
    Returns Delivery Managers assigned to the given client
    """
    return User.objects.filter(
        groups__name="Delivery Manager (DM)",
        id__in=UserClient.objects.filter(
            client=client
        ).values_list("user_id", flat=True)
    ).distinct()

def notify_dm_low_match(dm_users, job_title, ai_title, similarity, jd_display_id):
    try:
        for dm in dm_users:
            if not dm.email:
                continue

            subject, body = get_email_template("ai_title_mismatch_notification",
        is_html=False)

            subject = subject.format(
                similarity=similarity
            )

            body = body.format(
                dm_name=f"{dm.first_name or ''} {dm.last_name or ''}".strip(),
                jd_display_id=jd_display_id,
                job_title=job_title,
                ai_title=ai_title
            )

            send_email_safe(
                subject=subject,
                body=body.strip(),
                to_email=dm.email,
                from_email=settings.EMAIL_HOST_USER,
                is_html=False
            )
            return True
    except Exception as e:
        print("Email unexpected error:", e)
        return False

def check_ai_title_similarity_and_notify(jd_obj):
    """
    jd_obj must already be saved (jd_display_id exists)
    """

    if not jd_obj.ai_title or not jd_obj.job_title:
        return

    similarity = safe_similarity(
        jd_obj.ai_title,
        jd_obj.job_title
    )

    if similarity is None:
        return

    jd_obj.ai_title_similarity = similarity
    jd_obj.save(update_fields=["ai_title_similarity"])

    if similarity >= 80:
        return

    if not jd_obj.client_id:
        return

    dm_users = get_client_delivery_managers(jd_obj.client_id)

    if not dm_users.exists():
        print("No Delivery Managers found for client:", jd_obj.client_id)
        return

    notify_dm_low_match(
        dm_users,
        jd_obj.job_title,
        jd_obj.ai_title,
        similarity,
        jd_obj.jd_display_id
    )

logger = logging.getLogger(__name__)


def send_user_created_mail(email, username, password, full_name):

    subject, body = get_email_template("user_created_account", is_html=False)

    body = body.format(
        full_name=full_name,
        username=username,
        password=password,
        login_url="https://auto-recruiter.itconnectus.com/login"
    )

    try:
        send_email_safe(
            subject=subject,
            body=body.strip(),
            to_email=[email],
            from_email=settings.EMAIL_HOST_USER,
            is_html=False
        )
        return True

    except Exception as e:
        logger.error(f"Email sending failed: {e}")
        return False
    
def send_resume_ack_email(to_email, candidate_name, job_title):

    try:
        subject, body = get_email_template("resume_acknowledgement", is_html=False)

        subject = subject.format(job_title=job_title)

        body = body.format(
            candidate_name=candidate_name,
            job_title=job_title
        )

        send_email_safe(
            subject=subject,
            body=body.strip(),
            to_email=[to_email],
            from_email=settings.EMAIL_HOST_USER,
            is_html=False
        )

        return True

    except Exception as e:
        print("Email unexpected error:", e)
        return False

class DocumentTextExtraction:
    def extract_headers_xml(file_path):
        headers = []
        namespaces = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}

        with zipfile.ZipFile(file_path) as z:
            for name in z.namelist():
                if name.startswith("word/header"):
                    root = etree.fromstring(z.read(name))
                    for t in root.xpath(".//w:t", namespaces=namespaces):
                        if t.text and t.text.strip():
                            headers.append(t.text.strip())

        return "\n".join(headers)
    
    def extract_docx_xml(file_path):
        try:
            with zipfile.ZipFile(file_path) as z:
                xml_content = z.read("word/document.xml")

            root = etree.fromstring(xml_content)
            namespaces = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}

            lines = []
            current_line = []

            for element in root.xpath(".//w:body/*", namespaces=namespaces):
                if element.tag.endswith("}p"):
                    texts = element.xpath(".//w:t", namespaces=namespaces)

                    for t in texts:
                        if t.text:
                            current_line.append(t.text)

                    if current_line:
                        line = "".join(current_line).strip()
                        if line:
                            lines.append(line)
                        current_line = []

                elif element.tag.endswith("}tbl"):
                    for row in element.xpath(".//w:tr", namespaces=namespaces):
                        row_cells = []

                        for cell in row.xpath(".//w:tc", namespaces=namespaces):
                            cell_texts = cell.xpath(".//w:t", namespaces=namespaces)
                            cell_value = "".join(t.text for t in cell_texts if t.text).strip()

                            if cell_value:
                                row_cells.append(cell_value)

                        if row_cells:
                            lines.append(" | ".join(row_cells))

            doc_text = "\n".join(lines)

            if not doc_text.strip():
                raise Exception("No readable text found in Word document.")

            return doc_text.strip()

        except Exception as e:
            print(f"Failed to extract Word Document text: {e}")
            raise Exception(f"Failed to extract text from Word document: {str(e)}")
        
    def extract_with_libreoffice(file_path):
        try:
            with tempfile.TemporaryDirectory() as tmpdir:
                subprocess.run(
                    [
                        "libreoffice",
                        "--headless",
                        "--convert-to", "txt:Text",
                        file_path,
                        "--outdir", tmpdir
                    ],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                    check=True
                )

                base = os.path.splitext(os.path.basename(file_path))[0]
                txt_file = os.path.join(tmpdir, base + ".txt")

                if not os.path.exists(txt_file):
                    raise Exception("LibreOffice conversion failed")

                with open(txt_file, "r", encoding="utf-8", errors="ignore") as f:
                    text = f.read().strip()

                if not text:
                    raise Exception("Empty LibreOffice output")

                return text

        except Exception as e:
            raise Exception(f"LibreOffice extraction failed: {e}")
        
    def is_docx(file_path):
        try:
            with zipfile.ZipFile(file_path):
                return True
        except zipfile.BadZipFile:
            return False
    
    @staticmethod
    def process_word(file_path):
        if DocumentTextExtraction.is_docx(file_path):
            try:
                body_text = DocumentTextExtraction.extract_docx_xml(file_path)
                header_text = DocumentTextExtraction.extract_headers_xml(file_path)

                final_text = "\n".join(
                    t for t in [header_text, body_text] if t
                )
                return final_text
            except Exception:
                pass

        try:
            return DocumentTextExtraction.extract_with_libreoffice(file_path)
        except Exception:
            pass

        raise Exception("Unable to extract text from Word document")
    
    @staticmethod
    def process_pdf(file_path):
        try:
            with pdfplumber.open(file_path) as pdf:
                pdf_text = []
                for page in pdf.pages:
                    text = page.extract_text()

                    if text:
                        pdf_text.append(text)
            if not pdf_text:
                raise Exception("No readable text found in PDF.")

            return "\n".join(pdf_text)

        except Exception as e:
            print(f"Failed to extract PDF text: {e}")
            raise Exception(f"Failed to extract text from PDF: {str(e)}")
        
    def normalize_excel_header(header: str) -> str:
        return re.sub(
            r"\s+",
            " ",
            header.replace("_", " ").strip().lower()
        )
    
    @staticmethod
    def process_excel_or_csv(file_path, extension):
        if extension in [".xlsx", ".xls"]:
            df = pd.read_excel(file_path)
        elif extension in [".csv"]:
            try:
                df = pd.read_csv(file_path)
            except UnicodeDecodeError:
                df = pd.read_csv(file_path, encoding="latin1")

        header_name_map = {
            "job_title": ["job title", "jobtitle", "designation", "title"],
            "job_type": ["job type", "jobtype", "type"],
            "years_of_experience": ["years of experience", "experience", "required experience"],
            "about_company": ["about company", "comapany about"],
            "job_summary": ["job summary", "summary"],
            "responsibilities": ["responsibilities", "roles & responsibilities"],
            "domain_requirements": ["domain requirements", "domainrequirements"],
            "certification_requirements": ["certification requirements", "certifications requirements", "certifications", "certification", "required certification", "required certifications"],
            "security_clearance_requirements": ["security clearance requirements", "clearance requirements"],
            "onsite_job": ["onsite job", "onsite"],
            "job_location": ["job location", "location"],
            "required_qualifications": ["required qualifications", "qualifications", "qualification required"],
            "preferred_qualifications": ["preferred qualifications", "qualification preferred"],
            "working_hours": ["working hours"],
            "benefits": ["benefits", "benefit"],
            "search_pattern": ["search pattern", "pattern"],
            "requirement_priority": ["requirement priority", "priority"],
            "salary_range": ["salary range", "salary"],
            "no_of_open_positions": ["open positions", "required positions", "number of positions"]
        }

        excel_headers = {DocumentTextExtraction.normalize_excel_header(col): col for col in df.columns}
        header_lookup = {}

        for key_value, variations in header_name_map.items():
            for variant in variations:
                normalized_variant = DocumentTextExtraction.normalize_excel_header(variant)
                if normalized_variant in excel_headers:
                    header_lookup[key_value] = excel_headers[normalized_variant]
                    break

        normalized_rows = []
        for _, row in df.iterrows():
            normalized_row = {}

            for key_value in header_name_map.keys():
                excel_column = header_lookup.get(key_value)

                normalized_row[key_value] = (
                    str(row[excel_column]).strip()
                    if excel_column and pd.notna(row[excel_column])
                    else ""
                )

            normalized_rows.append(normalized_row)

        return normalized_rows
    
def send_shortlist_email(resume_id, jd_id, consent_required):
    try:
        resume = TblCandidateResume.objects.select_related("candidate_id").get(
            resume_id=resume_id
        )
        jd = TblJobDescription.objects.get(jd_id=jd_id)

        to_email = resume.email
        if not to_email:
            print("Shortlist email skipped: Candidate email not found")
            return False
        
        to_mobile = resume.mobile_number
        if not to_mobile:
            print("Shortlist Mobile number skipped: Candidate Mobile number email not found")
            return False

        candidate_name = f"{resume.candidate_id.first_name} {resume.candidate_id.last_name}".strip()
        job_title = jd.job_title

        tracking_token = f"{resume.resume_id}_{jd.jd_id}"

        subject, body = get_email_template("shortlisted_candidate_html", is_html=True)

        subject = subject.format(
            job_title=job_title,
            tracking_token=tracking_token
        )

        token_career = generate_public_link(resume.resume_id, jd.jd_id, "job")
        jd_url = f"{settings.CAREER}/job/l0/{token_career}/"

        print("Generated Job URL:", jd_url)

        token = generate_public_link(resume.resume_id, jd.jd_id, "shortlist")
        form_url = f"{settings.CAREER}/shortlist-details/{token}/"

        if consent_required:
            token = generate_public_link(resume.resume_id, jd.jd_id, "client_submission_consent")
            form_url = f"{settings.INTERVIEW}/client_submission_consent/?token={token}"
        else:
            token = generate_public_link(resume.resume_id, jd.jd_id, "shortlist")
            form_url = f"{settings.CAREER}/shortlist-details/{token}/"

        print("Generated Shortlist URL:", form_url)

        assigned_users = AssignJd.objects.filter(
            jd=jd
        ).select_related("user")

        recruiter_emails = [
            a.user.email for a in assigned_users
            if a.user and a.user.email
        ]

        dm_emails = []
        if jd.client_id:
            delivery_managers = get_client_delivery_managers(jd.client_id)
            dm_emails = [
                dm.email for dm in delivery_managers
                if dm.email
            ]

        cc_emails = list(set(recruiter_emails + dm_emails))

        if to_email in cc_emails:
            cc_emails.remove(to_email)

        body = body.format(
            candidate_name=candidate_name,
            resume_source=resume.resume_source,
            job_title=job_title,
            tracking_token=tracking_token,
            form_url=form_url,
            jd_url=jd_url
        )

        print("-----mail sending")

        send_email_safe(
            subject=subject,
            body=body.strip(),
            to_email=to_email,
            cc_emails=cc_emails,
            from_email=settings.EMAIL_HOST_USER,
            is_html=True,
        )

        send_whatsapp_safe(
            to_phone=to_mobile,
            template_name="resume_acknowledgement",
            template_params=[
                candidate_name,
                job_title,
            ],
        )

        # send_sms_safe(
        #     to_phone=to_mobile,
        #     message=f"""
        # Congratulations {candidate_name}!

        # You are shortlisted for {job_title}.

        # HR team will contact you soon.

        # ITConnectUS
        # """
        # )

        print("-----mail sent successfully")
        return True

    except TblCandidateResume.DoesNotExist:
        print("Shortlist email error: Resume not found")
        return False

    except TblJobDescription.DoesNotExist:
        print("Shortlist email error: JD not found")
        return False

    except Exception as e:
        print("Shortlist email unexpected error:", e)
        return False
    
def retry_pending_whatsapp():

    msgs = WhatsAppQueue.objects.filter(
        status="PENDING",
        next_retry_at__lte=timezone.now(),
        retry_count__lt=3,
    )

    for msg in msgs:
        success = send_whatsapp_safe(
            msg.to_phone,
            msg.template_name,
            msg.template_params,
        )

        if success:
            msg.status = "SENT"
        else:
            msg.retry_count += 1
            msg.next_retry_at = timezone.now() + timedelta(minutes=5)

        msg.save()

# def retry_pending_sms():

#     msgs = SMSQueue.objects.filter(
#         status="PENDING",
#         next_retry_at__lte=timezone.now(),
#         retry_count__lt=3,
#     )

#     for msg in msgs:
#         success = send_sms_safe(
#             msg.to_phone,
#             msg.message,
#         )

#         if success:
#             msg.status = "SENT"
#         else:
#             msg.retry_count += 1
#             msg.next_retry_at = timezone.now() + timedelta(minutes=5)

#         msg.save()

def send_thanks_notifications(matched_profile):

    jd = TblJobDescription.objects.filter(
        jd_id=matched_profile.job_id
    ).first()

    resume = TblCandidateResume.objects.filter(
        resume_id=matched_profile.resume_id
    ).first()

    if not jd or not resume:
        return False

    candidate_profile = resume.candidate_id

    assigned = AssignJd.objects.filter(jd=jd).first()
    if not assigned:
        return False

    recruiter = assigned.user

    interview_dt = matched_profile.interview_datetime.strftime("%d %b %Y %I:%M %p")

    candidate_name = f"{candidate_profile.first_name} {candidate_profile.last_name}"

    tracking_token = f"{resume.resume_id}_{jd.jd_id}"

    form_url = f"{settings.CAREER}/shortlist-details/{tracking_token}"

    subject, body = get_email_template(
        "candidate_interest_confirmation_html",
        is_html=True
    )

    subject = subject.format(
        job_title=jd.job_title
    )

    body = body.format(
        candidate_name=candidate_name,
        form_url=form_url
    )

    send_email_safe(
        subject=subject,
        body=body.strip(),
        to_email=candidate_profile.email,
        from_email=settings.EMAIL_HOST_USER,
        is_html=True
    )

    send_whatsapp_safe(
            to_phone=candidate_profile.mobile_number,
            template_name="interview_schedule_candidate",
            template_params=[
                candidate_name,
                jd.job_title,
                interview_dt,
                matched_profile.interview_link
            ],
        )

    return True

def process_shortlist_reply(tracking_token, reply_text=None, interested = False):

    try:
        print("Processing token:", tracking_token)

        resume_id, jd_id = tracking_token.split("_")

        print("Resume ID:", resume_id)
        print("JD ID:", jd_id)

        matched = TblMatchedProfiles.objects.filter(
            resume_id=resume_id,
            job_id=jd_id
        ).first()

        if not matched:
            print("❌ No matched profile found")
            return False

        print("Matched found:", matched.id)

        matched.accepted_message = reply_text

        matched.status = "Interested"
        matched.accepted_at = timezone.now()

        matched.save()

        print("✅ Status updated")

        send_thanks_notifications(matched)

        return True

    except Exception as e:
        print("❌ ERROR OCCURRED")
        traceback.print_exc()
        return False

def generate_ics_file(matched_profile, candidate_email, interview_dt, interview_link, jd_title):

    uid = str(uuid.uuid4())

    dtstamp = interview_dt.strftime("%Y%m%dT%H%M%SZ")
    dtstart = interview_dt.strftime("%Y%m%dT%H%M%SZ")
    dtend = (interview_dt + timedelta(minutes=30)).strftime("%Y%m%dT%H%M%SZ")

    token = generate_public_link(
        matched_profile.resume_id,
        matched_profile.job_id,
        "l0_interview"
    )

    interview_link = f"{settings.INTERVIEW}/interview_welcome/?token={token}"

    ics_content = (
        "BEGIN:VCALENDAR\n"
        "VERSION:2.0\n"
        "PRODID:-//AutoRecruiter//EN\n"
        "METHOD:REQUEST\n"
        "BEGIN:VEVENT\n"
        f"UID:{uid}\n"
        f"DTSTAMP:{dtstamp}\n"
        f"DTSTART:{dtstart}\n"
        f"DTEND:{dtend}\n"
        f"SUMMARY:Interview - {jd_title}\n"
        f"DESCRIPTION:Online Interview\\nJoin here: {interview_link}\n"
        f"LOCATION:{interview_link}\n"
        f"ORGANIZER;CN=ITConnectUS:MAILTO:{settings.EMAIL_HOST_USER}\n"
        f"ATTENDEE;CN=Candidate;RSVP=TRUE:MAILTO:{candidate_email}\n"
        "STATUS:CONFIRMED\n"
        "SEQUENCE:0\n"
        "END:VEVENT\n"
        "END:VCALENDAR"
    )

    return ics_content

def send_interview_calendar_invite(matched_profile):
    try:
        jd = TblJobDescription.objects.get(jd_id=matched_profile.job_id)
        resume = TblCandidateResume.objects.get(resume_id=matched_profile.resume_id)

        candidate = resume.candidate_id
        interview_dt = matched_profile.interview_datetime

        token = generate_public_link(matched_profile.resume_id, matched_profile.job_id, "l0_interview")

        interview_link = (
            f"{settings.INTERVIEW}/interview_welcome/?token={token}"
        )

        assigned = AssignJd.objects.filter(jd=jd).select_related("user").first()
        recruiter_email = assigned.user.email if assigned and assigned.user and assigned.user.email else None

        candidate_name = f"{candidate.first_name} {candidate.last_name}"
        formatted_dt = interview_dt.strftime("%d %b %Y %I:%M %p")

        subject, body = get_email_template("interview_scheduled_calendar", is_html=True)

        subject = subject.format(
            job_title=jd.job_title
        )

        body = body.format(
            candidate_name=candidate_name,
            job_title=jd.job_title,
            interview_datetime=formatted_dt,
            interview_link=interview_link
        )

        ics_content = generate_ics_file(
            matched_profile,
            candidate.email,
            interview_dt,
            interview_link,
            jd.job_title
        )

        email = EmailMessage(
            subject=subject,
            body=body.strip(),
            from_email=settings.EMAIL_HOST_USER,
            to=[candidate.email],
            cc=[recruiter_email] if recruiter_email else []
        )

        email.content_subtype = "html"

        email.attach(
            filename="interview_invite.ics",
            content=ics_content,
            mimetype="text/calendar; method=REQUEST"
        )

        email.send(fail_silently=False)

        return True

    except Exception as e:
        print("ICS email error:", e)
        return False
    
def notify_recruiter_multiple_jds(assignments):

    grouped = defaultdict(list)

    for assign in assignments:
        grouped[assign.user].append(assign)

    for recruiter, recruiter_assignments in grouped.items():

        if not recruiter or not recruiter.email:
            continue

        jd_lines = []
        cc_emails = set()

        for assign in recruiter_assignments:
            jd = assign.jd
            client_obj = jd.client_id

            jd_lines.append(
                f"""
                JD Reference ID : {jd.jd_display_id}
                Job Title       : {jd.job_title}
                Client          : {client_obj.client_name if client_obj else 'N/A'}
                Assigned Time   : {timezone.localtime(assign.created_at).strftime('%d-%m-%Y %I:%M %p')}
                """
            )

            if client_obj:
                delivery_managers = get_client_delivery_managers(client_obj)
                for dm in delivery_managers:
                    if dm.email:
                        cc_emails.add(dm.email)

        jd_count = len(recruiter_assignments)

        subject, body = get_email_template("jd_assignment_notification", is_html=False)

        subject = subject.format(
            jd_count=jd_count
        )

        body = body.format(
            recruiter_name=recruiter.first_name or recruiter.username,
            jd_list="".join(jd_lines),
            login_url="https://auto-recruiter.itconnectus.com/job_description"
        )

        send_email_safe(
            subject=subject,
            body=body.strip(),
            to_email=recruiter.email,
            cc_emails=list(cc_emails),
            from_email=settings.EMAIL_HOST_USER,
            is_html=False
        )

class DBAuthFilters:  
    def get_client_by_auth_user(user):
        if user:
            if user.groups.filter(name__in=["Recruiter", "Implementer Super Admin"]).exists() or user.is_superuser:
                client_ids = list(
                    TblClient.objects.values_list("client_id", flat=True)
                )
            else:
                client_ids = list(
                    user.client_mappings.values_list("client_id", flat=True)
                )

            return client_ids

        else:
            return []
        
    def get_jd_by_auth_user(user):
        if user:
            if user.groups.filter(name__in=["Recruiter"]).exists():
                jd_ids = list(
                    AssignJd.objects.filter(user=user).values_list("jd", flat=True)
                )
            elif user.groups.filter(name__in=["Implementer Super Admin"]).exists() or user.is_superuser:
                jd_ids = list(
                    TblJobDescription.objects.values_list("jd_id", flat=True)
                )
            else:
                client_ids = list(
                    user.client_mappings.values_list("client_id", flat=True)
                )

                jd_ids = list(
                    TblJobDescription.objects
                    .filter(client_id__in=client_ids)
                    .values_list("jd_id", flat=True)
                )

            return jd_ids
        else:
            return []
