from django.db import transaction
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from typing import Any, Dict, List, Optional
from django.db.models import QuerySet

from .models import Candidate, Skill, Language, CandidateProject, CandidateDocument, CandidateReference, CandidateExperience, CandidateEducation

User = get_user_model()


class CandidateService:
    """Service layer class containing all candidate business logic."""

    @staticmethod
    @transaction.atomic
    def create_candidate(
        user: Any,
        candidate_data: Dict[str, Any],
        skills: Optional[List[str]] = None,
        languages: Optional[List[str]] = None,
        projects: Optional[List[Dict[str, Any]]] = None,
        references: Optional[List[Dict[str, Any]]] = None,
        experiences: Optional[List[Dict[str, Any]]] = None,
        educations: Optional[List[Dict[str, Any]]] = None,
        created_by: Optional[Any] = None,
    ) -> Candidate:
        """
        Atomically create a Candidate profile and its nested dependencies.
        If a soft-deleted profile exists, it will be restored and updated.
        """
        # Candidates are no longer tied to a User account. Only when an explicit
        # user is passed do we reuse/restore that user's existing profile.
        existing_candidate = (
            Candidate.objects.all_with_deleted().filter(user=user).first() if user else None
        )

        if existing_candidate:
            if not existing_candidate.is_deleted:
                raise ValidationError("A profile already exists for this user.")
            else:
                # Restore soft-deleted profile
                existing_candidate.is_deleted = False
                existing_candidate.is_active = True
                candidate = existing_candidate
        else:
            candidate = Candidate(user=user)   # user may be None

        # Assign basic fields (excluding nested and M2M relations)
        exclude_fields = {"skills", "languages", "projects", "references", "experiences", "educations"}
        for field, value in candidate_data.items():
            if field not in exclude_fields:
                setattr(candidate, field, value)

        # Enforce Fresher business rules: only WORK-HISTORY fields are cleared for
        # a fresher. Desired pay, notice period and locations are valid for
        # freshers too, so they are preserved.
        if candidate.fresher:
            candidate.total_experience = None
            candidate.current_company = None
            candidate.previous_company = None
            candidate.current_role = None
            candidate.current_ctc = None

        candidate.created_by = created_by
        candidate.updated_by = created_by
        candidate.save()

        # Handle Skills
        if skills is not None:
            skill_objs = []
            for skill_name in skills:
                name_clean = skill_name.strip()
                if name_clean:
                    skill, _ = Skill.objects.get_or_create(name=name_clean)
                    skill_objs.append(skill)
            candidate.skills.set(skill_objs)

        # Handle Languages
        if languages is not None:
            lang_objs = []
            for lang_name in languages:
                name_clean = lang_name.strip()
                if name_clean:
                    lang, _ = Language.objects.get_or_create(name=name_clean)
                    lang_objs.append(lang)
            candidate.languages.set(lang_objs)

        # Handle Projects
        # Delete old projects if restoring
        candidate.projects.all().delete()
        if projects:
            for proj in projects:
                CandidateProject.objects.create(
                    candidate=candidate,
                    project_name=proj.get("project_name"),
                    description=proj.get("description"),
                    technologies_used=proj.get("technologies_used"),
                    duration=proj.get("duration"),
                    role=proj.get("role"),
                )

        # Handle Experiences
        candidate.experiences.all().delete()
        if not candidate.fresher and experiences:
            for exp in experiences:
                CandidateExperience.objects.create(
                    candidate=candidate,
                    company_name=exp.get("company_name"),
                    role=exp.get("role"),
                    joining_date=exp.get("joining_date"),
                    last_working_date=exp.get("last_working_date"),
                    is_current_company=exp.get("is_current_company", False),
                    current_ctc=exp.get("current_ctc"),
                    expected_ctc=exp.get("expected_ctc"),
                    notice_period=exp.get("notice_period"),
                    achievements=exp.get("achievements"),
                    responsibilities=exp.get("responsibilities"),
                    reason_for_leaving=exp.get("reason_for_leaving"),
                )

        # Handle Educations
        candidate.educations.all().delete()
        if educations:
            for edu in educations:
                CandidateEducation.objects.create(
                    candidate=candidate,
                    degree_name=edu.get("degree_name"),
                    field_of_study=edu.get("field_of_study"),
                    institution_name=edu.get("institution_name"),
                    passing_year=edu.get("passing_year"),
                    percentage_cgpa=edu.get("percentage_cgpa"),
                )

        # Handle References
        # Delete old references if restoring
        candidate.references.all().delete()
        if references:
            for ref in references:
                CandidateReference.objects.create(
                    candidate=candidate,
                    name=ref.get("name"),
                    company=ref.get("company"),
                    designation=ref.get("designation"),
                    email=ref.get("email"),
                    phone=ref.get("phone"),
                    relationship=ref.get("relationship"),
                )

        return candidate

    @staticmethod
    @transaction.atomic
    def update_candidate(
        candidate: Candidate,
        candidate_data: Dict[str, Any],
        skills: Optional[List[str]] = None,
        languages: Optional[List[str]] = None,
        projects: Optional[List[Dict[str, Any]]] = None,
        references: Optional[List[Dict[str, Any]]] = None,
        experiences: Optional[List[Dict[str, Any]]] = None,
        educations: Optional[List[Dict[str, Any]]] = None,
        updated_by: Optional[Any] = None,
    ) -> Candidate:
        """Atomically update a Candidate profile and its nested dependencies."""
        # Assign basic fields (excluding nested and M2M relations)
        exclude_fields = {"skills", "languages", "projects", "references", "experiences", "educations"}
        for field, value in candidate_data.items():
            if field not in exclude_fields:
                setattr(candidate, field, value)

        # Enforce Fresher business rules: only WORK-HISTORY fields are cleared for
        # a fresher. Desired pay, notice period and locations are valid for
        # freshers too, so they are preserved.
        if candidate.fresher:
            candidate.total_experience = None
            candidate.current_company = None
            candidate.previous_company = None
            candidate.current_role = None
            candidate.current_ctc = None

        candidate.updated_by = updated_by
        candidate.save()

        # Handle Skills
        if skills is not None:
            skill_objs = []
            for skill_name in skills:
                name_clean = skill_name.strip()
                if name_clean:
                    skill, _ = Skill.objects.get_or_create(name=name_clean)
                    skill_objs.append(skill)
            candidate.skills.set(skill_objs)

        # Handle Languages
        if languages is not None:
            lang_objs = []
            for lang_name in languages:
                name_clean = lang_name.strip()
                if name_clean:
                    lang, _ = Language.objects.get_or_create(name=name_clean)
                    lang_objs.append(lang)
            candidate.languages.set(lang_objs)

        # Handle Projects (delete and recreate for simple update logic)
        if projects is not None:
            candidate.projects.all().delete()
            for proj in projects:
                CandidateProject.objects.create(
                    candidate=candidate,
                    project_name=proj.get("project_name"),
                    description=proj.get("description"),
                    technologies_used=proj.get("technologies_used"),
                    duration=proj.get("duration"),
                    role=proj.get("role"),
                )

        # Handle Experiences (delete and recreate)
        if experiences is not None:
            candidate.experiences.all().delete()
            if not candidate.fresher:
                for exp in experiences:
                    CandidateExperience.objects.create(
                        candidate=candidate,
                        company_name=exp.get("company_name"),
                        role=exp.get("role"),
                        joining_date=exp.get("joining_date"),
                        last_working_date=exp.get("last_working_date"),
                        is_current_company=exp.get("is_current_company", False),
                        current_ctc=exp.get("current_ctc"),
                        expected_ctc=exp.get("expected_ctc"),
                        notice_period=exp.get("notice_period"),
                        achievements=exp.get("achievements"),
                        responsibilities=exp.get("responsibilities"),
                        reason_for_leaving=exp.get("reason_for_leaving"),
                    )

        # Handle Educations (delete and recreate)
        if educations is not None:
            candidate.educations.all().delete()
            for edu in educations:
                CandidateEducation.objects.create(
                    candidate=candidate,
                    degree_name=edu.get("degree_name"),
                    field_of_study=edu.get("field_of_study"),
                    institution_name=edu.get("institution_name"),
                    passing_year=edu.get("passing_year"),
                    percentage_cgpa=edu.get("percentage_cgpa"),
                )

        # Handle References (delete and recreate)
        if references is not None:
            candidate.references.all().delete()
            for ref in references:
                CandidateReference.objects.create(
                    candidate=candidate,
                    name=ref.get("name"),
                    company=ref.get("company"),
                    designation=ref.get("designation"),
                    email=ref.get("email"),
                    phone=ref.get("phone"),
                    relationship=ref.get("relationship"),
                )

        return candidate

    @staticmethod
    def soft_delete_candidate(candidate: Candidate, deleted_by: Any) -> None:
        """Perform soft delete by setting flag and updating updated_by."""
        candidate.updated_by = deleted_by
        candidate.delete()

    @staticmethod
    def create_candidate_document(
        candidate: Candidate,
        document_type: str,
        file_obj: Any,
    ) -> CandidateDocument:
        """Upload and associate a verification document."""
        return CandidateDocument.objects.create(
            candidate=candidate,
            document_type=document_type,
            file=file_obj,
            verification_status=CandidateDocument.VerificationStatus.PENDING,
        )

    @staticmethod
    def update_document_status(
        document: CandidateDocument,
        status: str,
        updated_by: Any,
    ) -> CandidateDocument:
        """Update verification status of a candidate document."""
        if status not in CandidateDocument.VerificationStatus.values:
            raise ValidationError(f"Invalid verification status: {status}")
        
        document.verification_status = status
        document.save()
        
        # Touch candidate updated_by audit field
        document.candidate.updated_by = updated_by
        document.candidate.save(update_fields=["updated_by", "updated_at"])
        
        return document
