from django.conf import settings
from django.shortcuts import render
from .models import Profiles,Otp,Product,Setting,Holiday, Announcement, LeaveRequest,Payslip,  Employee, Attendance,Department, Designation
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.exceptions import NotFound
from django.contrib.auth import authenticate, login
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User
from django.utils import timezone as djtz
from django.utils import timezone
from datetime import date, datetime, timedelta
import cv2
import face_recognition
import os
import numpy as np
import sys
import requests


import io
import requests
from django.utils import timezone
from django.http import HttpResponse
import base64
from PIL import Image, ImageEnhance, ImageFilter
from django.db.models import Q
from rest_framework.generics import RetrieveAPIView, ListAPIView
from .serializers import UserSerializer
from rest_framework.permissions import IsAuthenticated
from datetime import datetime, time
import json
from django.core.mail import EmailMessage
from school.settings import BASE_DIR
import pdfkit
from django.core.exceptions import ObjectDoesNotExist
# import openai
import re
from django.conf import settings
import threading
# import pywhatkit
from django.core.mail import send_mail
from django.utils.crypto import get_random_string
from django.db.models import Sum




from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from django.core.exceptions import ObjectDoesNotExist
from rest_framework_simplejwt.tokens import RefreshToken
import re  # For basic email validation
# from twilio.rest import Client as TwilioClient # ðŸ’¡ Maru Peyaridhal!
# class LoginAPIView(APIView):

#     def post(self, request):
#         data = request.data
#         username_or_email = data.get("username")  # Allow either username or email field
#         password = data["password"]



#         # Basic email validation
#         if re.match(r"[^@]+@[^@]+\.[^@]+", username_or_email):
#             # It's an email address, fetch the user by email
#             try:
#                 user = User.objects.get(email=username_or_email)
#             except User.DoesNotExist:
#                 user = None
#         else:
#             # It's a username, fetch the user by username
#             user = authenticate(request, username=username_or_email, password=password)

#         # Authenticate the user
#         if user is not None and user.check_password(password):
#             login(request, user)
#             token = RefreshToken.for_user(user)

#             try:
#                 profile = Profiles.objects.get(user_id=user.id)

#                 if profile.status == 2:  # Check if the profile status s 2 (inactive user)
#                     return Response({
#                         "status": {"code": status.HTTP_403_FORBIDDEN, "response": "error"},
#                         "error": "Inactive user",
#                         "message": "You are inactive. Contact Admin",
#                         "data": None
#                     }, status=status.HTTP_403_FORBIDDEN)
#                 pro = profile.profiles.url
#                 companyname = profile.name
#                 # Set default company logo
#                 com = "../static/dummy.png"

#                 if profile.role == 2:
#                     try:
#                         created_company = Profiles.objects.get(user_id=profile.user_id)
#                         company_creator_profile = Profiles.objects.get(user_id=created_company.created_by_id)
#                         if company_creator_profile.companylogo:
#                             com = company_creator_profile.companylogo.url
#                             companyname = company_creator_profile.servicename
#                     except ObjectDoesNotExist:
#                         pass
#                 else:
#                     if profile.companylogo:
#                         com = profile.companylogo.url
#                         companyname = profile.servicename

#             except ObjectDoesNotExist:
#                 pro = "../static/dummy.png"
#                 com = "../static/dummy.png"
#                 profile = None

#             companyname = "loop"
#             phone = profile.phone
#             return Response({
#                 "status": {"code": status.HTTP_200_OK, "response": "success"},
#                 "error": None,
#                 "message": "Login success",
#                 "data": {
#                     "name": user.username,
                
#                     "id": user.id,
#                     "username": user.first_name,
#                     "email": user.email,
#                     "isSuper": user.is_superuser,
#                     "refresh": str(token),
#                     "token": str(token.access_token),
#                     "profile": pro,
#                     "companylogo" : com,
#                     "companyname" : companyname,
#                     "phone" : phone,
#                     "role": profile.role,
#                 }
#             })
#         else:
#             return Response({
#                 "status": {"code": status.HTTP_401_UNAUTHORIZED, "response": "error"},
#                 "error": "Authentication failed",
#                 "message": "Invalid username or password",
#                 "data": None
#             }, status=status.HTTP_401_UNAUTHORIZED)
class LoginAPIView(APIView):

    def post(self, request):
        data = request.data
        username_or_email = data.get("username")  # Allow either username or email field
        password = data.get("password")

        if not username_or_email or not password:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "error"},
                "error": "Missing credentials",
                "message": "Username/email and password are required.",
                "data": None,
            }, status=status.HTTP_400_BAD_REQUEST)

        # Basic email validation
        if re.match(r"[^@]+@[^@]+\.[^@]+", username_or_email):
            # It's an email address, fetch the user by email
            try:
                user_obj = User.objects.get(email=username_or_email)
                user = authenticate(request, username=user_obj.username, password=password)
            except User.DoesNotExist:
                user = None
        else:
            # It's a username, fetch the user by username
            user = authenticate(request, username=username_or_email, password=password)

        if user is None:
            return Response({
                "status": {"code": status.HTTP_401_UNAUTHORIZED, "response": "error"},
                "error": "Authentication failed",
                "message": "Invalid username or password",
                "data": None,
            }, status=status.HTTP_401_UNAUTHORIZED)

        login(request, user)
        token = RefreshToken.for_user(user)

        try:
            profile = Profiles.objects.get(user_id=user.id)
        except ObjectDoesNotExist:
            profile = None

        # ✅ Inactive account check — status == 2 na login-ஐயே block பண்ணும்
        if profile is not None and profile.status == 2:
            return Response({
                "status": {"code": status.HTTP_403_FORBIDDEN, "response": "error"},
                "error": "Inactive user",
                "message": "Your account is inactive. Please contact Admin.",
                "data": None,
            }, status=status.HTTP_403_FORBIDDEN)

        pro = "../static/dummy.png"
        com = "../static/dummy.png"
        companyname = "loop"
        phone = None
        role = None
        geostatus = 2  # default: office-only, profile இல்லாத fallback case-க்கும் safe

        password  = password

        if profile is not None:
            phone = profile.phone
            role = profile.role
            geostatus = profile.geostatus  # ✅ Anywhere(1) / Office-only(2)

            if profile.profiles:
                pro = profile.profiles.url

            if profile.role == 2:
                try:
                    created_company = Profiles.objects.get(user_id=profile.user_id)
                    company_creator_profile = Profiles.objects.get(user_id=created_company.created_by_id)
                    if company_creator_profile.companylogo:
                        com = company_creator_profile.companylogo.url
                        companyname = company_creator_profile.servicename
                except ObjectDoesNotExist:
                    pass
            else:
                if profile.companylogo:
                    com = profile.companylogo.url
                    companyname = profile.servicename

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": "Login success",
            "data": {
                "name": user.username,
                "id": user.id,
                "username": user.first_name,
                "email": user.email,
                "isSuper": user.is_superuser,
                "refresh": str(token),
                "token": str(token.access_token),
                "profile": pro,
                "companylogo": com,
                "companyname": companyname,
                "password" : password,
                "phone": phone,
                "role": role,
                "geostatus": geostatus,  # ✅ 1 = Anywhere attendance, 2 = Office only
            },
        })


class UserAPIView(RetrieveAPIView):
    permission_classes = (IsAuthenticated,)
    serializer_class = UserSerializer
    

    def get_object(self):
        return self.request.user

    def post(self, request):
        data = request.data

        user = User.objects.get(email=data["email"])
        profile = Profiles.objects.get(user=user)

        

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": True,
            "data": {
                "username": user.username,
                "email": user.email,
                "role": profile.role.id,
                "profile": profile.profiles.url,
            }
        })
    

def send_otp_email(email, otp):
    subject = 'Your One-Time  (OTP) for Verification'
    message = f'''Dear User,

    Your One-Time  (OTP) for verification is:

    {otp}

    Please enter this code to proceed. This OTP is valid for the next 1 minutes.

    If you did not request this code, please ignore this email.

    Thank you,  
    Pearlcon Technologies LLP
    https://pearlcons.com
    '''
    try:
        send_mail(
            subject,
            message,
            settings.DEFAULT_FROM_EMAIL,
            [email],
            fail_silently=False,
        )
    except Exception as e:
        print(f"Error sending email: {e}")
def send_otp_emails(email, otp):
   
    subject = 'Your One-Time  (OTP) for Verification'
    message = f'''Dear User,

    Your One-Time  (OTP) for verification is:

    {otp}

    Please enter this code to proceed. This OTP is valid for the next 1 minutes.

    If you did not request this code, please ignore this email.

    Thank you,  
    Pearlcon Technologies LLP
    https://pearlcons.com
        '''
    try:
        send_mail(
            subject,
            message,
            settings.DEFAULT_FROM_EMAIL,
            [email],
            fail_silently=False,
        )
    except Exception as e:
        print(f"Error sending email: {e}")

        
from django.contrib.auth import get_user_model
User = get_user_model()

class SendotpAPIView(APIView):
    def post(self, request):
        data = request.data
        try:
            email = data.get('params', {}).get('email', None)
            print('Received email:', email)

            # âœ… Step 1: Check if email exists in registered users
            if not User.objects.filter(email=email).exists():
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "Email not found"},
                    "error": True,
                    "message": "Email not registered"
                })

            # âœ… Step 2: Create OTP record and send OTP
            user = Otp.objects.create(email=email)
            otp = get_random_string(length=6, allowed_chars='1234567890')
            user.otp = otp
            user.otp_created_at = timezone.now()
            user.save()

            threading.Thread(target=send_otp_email, args=(email, otp)).start()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "Success"},
                "error": None,
                "data": {"email": email}
            })
        except Exception as e:
            print("Error:", e)
            return Response({
                "status": {"code": status.HTTP_500_INTERNAL_SERVER_ERROR, "response": "Internal Server Error"},
                'error': True,
                "message": str(e)
            })


class SendotpsAPIView(APIView):
    def post(self, request):
        data = request.data
        try:
            email = data.get('email', None)
            print('Received email:', email)

            # If this is registration
            if User.objects.filter(email=email).exists():
                return Response({
                    "status": {"code": status.HTTP_409_CONFLICT, "response": "Email already registered"},
                    "error": True,
                    "message": "This email is already registered. Please use another email."
                })

            otp = get_random_string(length=6, allowed_chars='1234567890')
            user = Otp.objects.create(email=email, otp=otp, otp_created_at=timezone.now())

            threading.Thread(target=send_otp_emails, args=(email, otp)).start()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "Success"},
                "error": None,
                "data": {"email": email}
            })
        except Exception as e:
            return Response({
                "status": {"code": status.HTTP_500_INTERNAL_SERVER_ERROR, "response": "Internal Server Error"},
                'error': True,
                "message": str(e)
            })

from datetime import timedelta
from django.utils import timezone

class VerifyotpAPIView(APIView):
    def get(self, request):
        email = request.query_params.get('email')
        otp_entered = request.query_params.get('otp')

        try:
            user_otp = Otp.objects.filter(email=email).latest('otp_created_at')

            # Expiry time check (30 seconds)
            expiration_time = user_otp.otp_created_at + timedelta(seconds=60)
            if timezone.now() > expiration_time:
                return Response({
                    "status": 401,
                    "message": "OTP expired. Please request a new one.",
                    "error": True
                }, status=status.HTTP_200_OK) 

            if otp_entered == user_otp.otp:
                return Response({
                    "status": 200,
                    "message": "OTP Verified",
                    "error": False
                }, status=status.HTTP_200_OK)
            else:
                return Response({
                    "status": 400,
                    "message": "Invalid OTP",
                    "error": True
                }, status=status.HTTP_200_OK)

        except Otp.DoesNotExist:
            return Response({
                "status": 404,
                "message": "OTP not found",
                "error": True
            }, status=status.HTTP_200_OK)




    def delete(self, request, id):
        if id is not None:
            try:
                profile = Profiles.objects.get(user_id=id)

                # Delete the image file from storage (optional)
                if profile.profiles:  # replace 'image' with your actual image field name
                    profile.profiles.delete(save=False)  # Deletes the file from MEDIA folder
                    profile.profiles = None
                    profile.save()

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "message": "Profile image deleted successfully"
                })

            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": None,
                    "message": "Profile not found"
                })

        return Response({
            "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
            "error": "ID not provided",
            "message": "User ID is required"
        })




class ProfileupdateAPIView(APIView):
    
    permission_classes = (IsAuthenticated,)

    def get(self, request, **kwargs):
        id = kwargs.get('id')


        if id is not None:
            try:
                user = User.objects.get(id=id)
                company = Profiles.objects.get(user_id=user.id)
                if company.profiles and company.profiles.url != "../static/dummy.png":
                    pr = company.profiles.url
                else:
                    pr = "../static/dummy.png"

                data = {
                    'name': company.name,
                    'email': company.email,
                    'phone': company.phone,
                    'f_name': user.first_name,
                    'l_name': user.last_name,

                    'profile' : pr,
                    'id': user.id   
                }



                

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "Success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "Failed"},
                    'error': True,
                    "message": "Company not found"
                })

    def post(self, request):
        data = request.data
        try:


            pho = data.get("prof_photo")
            # name = data.get("username")

            take = Profiles.objects.get(user_id=request.user.id)
            take.profiles = pho

            take.save()
            # # Create User instance
            # van = User(username=name, email=email, password=hashed_password)
            # van.save()

           

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Profile updated successfully"
            })

        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {str(e)}"
            })
        except Exception as e:
            return Response({
                "status": {"code": status.HTTP_500_INTERNAL_SERVER_ERROR, "response": "failed"},
                "error": True,
                "message": f"An error occurred: {str(e)}"
            })


    def put(self, request, id):
        data = request.data

        try:
            mm = Profiles.objects.get(user_id=id)
            sd = User.objects.get(id=id)

            # User table
            sd.username = data.get("first_name", sd.username)
            sd.first_name = data.get("first_names", sd.first_name)
            sd.last_name = data.get("last_name", sd.last_name)
            sd.email = data.get("email_id", sd.email)

            if data.get("password"):
                sd.password = make_password(data["password"])

           
            sd.save()

            # Profile table
            mm.email = data.get("email_id", mm.email)
            mm.phone = data.get("phone_no", mm.phone)
            mm.name = data.get("first_name", mm.name)
            mm.password = data.get("password", mm.password)

            if data.get("password"):
                mm.password = data["password"]


            # Update photo only if new photo is uploaded
            prof_photo = data.get("prof_photo")
            if prof_photo:
                mm.profiles = prof_photo

            mm.save()

            return Response({
                "status": {
                    "code": status.HTTP_200_OK,
                    "response": "success"
                },
                "error": None,
                "message": "User updated successfully"
            })

        except Profiles.DoesNotExist:
            return Response({
                "status": {
                    "code": status.HTTP_404_NOT_FOUND,
                    "response": "failed"
                },
                "error": True,
                "message": "Profile not found"
            })

        except User.DoesNotExist:
            return Response({
                "status": {
                    "code": status.HTTP_404_NOT_FOUND,
                    "response": "failed"
                },
                "error": True,
                "message": "User not found"
            })

        except Exception as e:
            return Response({
                "status": {
                    "code": status.HTTP_500_INTERNAL_SERVER_ERROR,
                    "response": "failed"
                },
                "error": True,
                "message": str(e)
            })


class RegisterAPIView(APIView):
    def get(self, request, **kwargs):
        id = kwargs.get('id')


        if id is not None:
            try:
                user = User.objects.get(id=id)
                company = Profiles.objects.get(user=user.id)
                if company.profiles and company.profiles.url != "../static/dummy.png":
                    pr = company.profiles.url
                else:
                    pr = "../static/dummy.png"

                data = {
                    'name': company.name,
                    'email': company.email,
                    'phone': company.phone,
                    'address': company.address,
                    'pincode': company.pincode,
                    'profile' : pr,
                   
                    'firstname': user.first_name,
                    'lastname': user.last_name,
                    'gender': company.gender,
                
                    'doj': company.doj,
                    'password': company.password,
                    


                    'id': user.id   
                }

                

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "Success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "Failed"},
                    'error': True,
                    "message": "Company not found"
                })
        else:

            val = request.GET.get('val')
            print(val)
            try:
                data = []


                username = request.query_params.get('vendorname', '').strip()  # Uncomment this line
                pincode = request.query_params.get('pincode', '').strip()  # Uncomment this line

                email = request.query_params.get('email', '').strip()  # Uncomment this line
                phone = request.query_params.get('phone', '').strip()  # Uncomment this line

                

                status_param = request.query_params.get('status', '').strip()

                if val == '2':
                    companies = Profiles.objects.filter(status=2,role=0).order_by('-user_id')
                else:
                    companies = Profiles.objects.filter(status=1,role=0).order_by('-user_id')


                if username:
                  
                    companies = companies.filter(name__icontains=username) 

                if email:
                  
                    companies = companies.filter(email__icontains=email) 
                if phone:
                  
                    companies = companies.filter(phone__icontains=phone) 

                if pincode:
                  
                    companies = companies.filter(pincode__icontains=pincode) 
                if status_param:
                    try:
                        status_int = int(status_param)
                        if status_int in [1, 2]:
                            companies = companies.filter(status=status_int)
                    except ValueError:
                        pass

                for company in companies:
                        


                        

                    users  = User.objects.get(id=company.user_id) 

                    # if company.profiles:
                    #     pro = company.profiles.url
                    # else:
                    #     pro = "../static/dummy.png"

                    company_data = {
                    
                        'name': company.name,
                        'id': company.user_id,
                        'phone': company.phone,
                        # 'gstnumber': company.gstnumber,
                        'pincode': company.pincode,

                        # 'gst': company.gst.url,
                        'email': company.email,
                        'address': company.address,
                        # 'profile': pro,
                        'phone': company.phone,
                        'status' : company.status,


                    }
                    data.append(company_data)
                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "Success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "Failed"},
                    "error": True,
                    "message": "Something went wrong"
                })

    def post(self, request):
        data = request.data
        try:
            # Collect data from request
            # servicename = data.get("servicename")
            name = data.get("employeename")
            # f_name = data.get("first_name")
            # l_name = data.get("last_names")

            email = data.get("email")
            phone = data.get("mobilenumber")
            address = data.get("address")
            pincode = data.get("pincode")
            # gstnumber = data.get("gstnumber")
            # state = data.get("state")


            # role = data.get("role")
            # gst = data.get("gst")
            # doj = data.get("doj")  
            # profiles = data.get("prof_photo")
            # pan = data.get("pan")
            # gender = data.get("gender")
            password = data.get("password")
            # companylogo = data.get("companyprofile")
           
            # pro = "../static/dummy.png"

            # if not profiles:
            #     profiles = pro

            hashed_password = make_password(password)

            van = User(username=name, email=email, password=hashed_password)
            van.save()

            


            profile_data = Profiles(
                password=password,
                name=name, email=email,
                phone=phone, address=address, user=van
                
            )
            profile_data.save()

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "New User Registered successfully"
            })

        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {str(e)}"
            })
        except Exception as e:
            return Response({
                "status": {"code": status.HTTP_500_INTERNAL_SERVER_ERROR, "response": "failed"},
                "error": True,
                "message": f"An error occurred: {str(e)}"
            })

    def put(self, request, id):
        val = request.GET.get('val')
        data = request.data
        ctc = request.data.get('ctc')

        if val == '1':
            po = Profiles.objects.get(user_id=id)
            if po.wfh == 1:
                po.wfh = 2
            else:
                po.wfh = 1
            po.save()

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "WFH updated successfully"
            })
        elif val == '2':
            po = Profiles.objects.get(user_id=id)
            if po.status == 1:
                po.status = 2
            else:
                po.status = 1
            po.save()

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Inactive updated successfully"
            })
        elif ctc:
            meow = Profiles.objects.get(user_id=id)
            meow.ctc = data['ctc']
            meow.gross = data['gross']
            meow.pt = data['pt']
            meow.uan = data['uan']
            meow.pan = data['pan']
            meow.account = data['account']
            meow.reim = data['reim']


            meow.save()

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Salary details updated successfully"
            })
        else:
            try:
                mm = Profiles.objects.get(user_id=id)
                sd = User.objects.get(id=id)

                mm.name = data["name"]
                mm.pemail = data["pemail"]
                mm.shift = data["shift"]
                mm.marriedstatus = data["marriedstatus"]
                mm.gender = data["gender"]
                mm.email = data["email"]
                mm.phone = data["phone"]
                mm.address = data["address"]
                mm.city = data["city"]
                mm.state = data["state"]
                mm.country = data["country"]
                mm.pincode = data["pincode"]
                mm.department_id = data["department"]
                mm.designation_id = data["designation"]
                mm.role = data["role"]
                mm.authority_id = data["authority"]
                mm.doj = data["doj"]
                mm.profiles = data["profiles"]
                mm.empcode = data["empcode"]
                mm.password = data["password"]
                mm.save()

                sd.username = data["name"]
                sd.first_name = data["firstname"]
                sd.last_name = data["lastname"]
                sd.email = data["email"]
                sd.password = make_password(data["password"])
                sd.save()

                return Response({
                    "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                    "error": None,
                    "message": "User updated successfully"
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                    "error": True,
                    "message": "Something went wrong"
                })

    def delete(self,  *args, **kwargs):
        company_id = kwargs.get('id')

        try:

            pr  = Profiles.objects.get(user_id =company_id)
            pr.delete()
            company = User.objects.get(id=company_id)
            company.delete()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": "Company deleted successfully"
            })
        except ObjectDoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "No data found"
            })



class CompanydashboardAPIView(APIView):
    # permission_classes = (IsAuthenticated,)

    def get(self, request):
        
        try:
            from django.utils.timezone import now

            op = request.user.id

            totalenquiry = Product.objects.filter(created_by_id=op).count()

            from django.utils.timezone import now
            from datetime import timedelta

            op = request.user.id

            # Get current date
            current_date = now()

            

            totalvendorlastmonth = 6
            totalvendor=5
            totalenquirytodaydate=6
            activevendor=6
            monthdata=7
            inactivevendor=8
            last_month_enquiry=9
            current_month_enquiry=8
            current_month_auction=5
            last_month_auction=4
            current_month_product=2
            last_month_product=2
            
                
            data = {
                "totalenquiry": totalenquiry,
                "totalvendor": totalvendor,
                "totalenquirytodaydate": totalenquirytodaydate,
                "activevendor" : activevendor,
                "inactivevendor" : inactivevendor,
                "monthdata" : monthdata,
                "totalvendorlastmonth" :totalvendorlastmonth,
                "current_month_enquiry" : current_month_enquiry,
                "last_month_enquiry" : last_month_enquiry,
                "current_month_auction" : current_month_auction,
                "last_month_auction" : last_month_auction,
                "current_month_product" : current_month_product,
                "last_month_product" : last_month_product

            }

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "data": data
            })
        except ObjectDoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "No data found",
            })
        





    



class CheckcompanyAPIView(APIView):
    def get(self, request, id=None):
        phone = request.query_params.get('phone')
        email = request.query_params.get('email')
        name = request.query_params.get('username')

       
        gstnumber = request.query_params.get('gstnumber') or request.query_params.get('gst')



        print("Check Params:", phone, email, gstnumber)

        if phone or email or gstnumber or name:
            query = Q()
            if name:
                query |= Q(name=name)
            if phone:
                query |= Q(phone=phone)
            if email:
                query |= Q(email=email)
            if gstnumber:
                query |= Q(gstnumber=gstnumber)

            exists = Profiles.objects.filter(query).exists()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "exists": exists
            })

        return Response({
            "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
            "error": True,
            "message": "No phone, email, or GST provided."
        }, status=status.HTTP_400_BAD_REQUEST)


# ══════════════════════════════════════════════
#  ADMIN DASHBOARD — Setting + Holiday table wise
#  (used by Dashboard.jsx — admin home screen)
# ══════════════════════════════════════════════
class DashboardapiAPIView(APIView):
    """
    GET /dashboardapi/?date=YYYY-MM-DD
 
    - Late count: Setting.shift_start + Setting.grace_minutes (NOT hardcoded)
    - Holiday / Weekly-off: Holiday table + Setting.weekly_off
    - Holiday/Weekly-off nu iruntha, "Absent" concept illa (working day illa),
      so absent_list empty-a return aagum, is_holiday/is_weekly_off flag-a paathu
      frontend banner kaatikkum.
    """
    permission_classes = (IsAuthenticated,)
 
    def get(self, request):
        date_str = request.query_params.get("date")
        try:
            target_date = (
                datetime.strptime(date_str, "%Y-%m-%d").date()
                if date_str else date_cls.today()
            )
        except ValueError:
            target_date = date_cls.today()
 
        cfg = get_attendance_config()                              # ✅ Setting table
        holiday_map = get_holiday_map(target_date, target_date)     # ✅ Holiday table
        hol_name = holiday_map.get(target_date)
        is_weekly_off = target_date.weekday() in cfg["weekly_off"]
        is_off_day = bool(hol_name) or is_weekly_off
 
        # 1. Active employees
        all_emp = Employee.objects.filter(status=1)
        total_count = all_emp.count()
 
        # 2. Indha naal present-a irukkiravanga
        present_today = Attendance.objects.filter(date=target_date, status="Present")
        present_count = present_today.count()
 
        # 3. Late count — Setting.grace_minutes base pannitu (hardcode illa)
        late_count = present_today.filter(in_time__gt=cfg["late_after"]).count()
 
        # 4. Absent list — holiday/weekly-off na "absent" nu edhuvum illa
        absent_list = []
        if not is_off_day:
            present_emp_ids = present_today.values_list("employee_id", flat=True)
            absent_emps = all_emp.exclude(id__in=present_emp_ids)
            for emp in absent_emps:
                image_url = request.build_absolute_uri(emp.photo.url).replace("http://", "https://") if getattr(emp, "photo", None) else None
                # image_url = request.build_absolute_uri(emp.photo.url) if getattr(emp, "photo", None) else None
                absent_list.append({
                    "id": emp.id,
                    "name": emp.name,
                    "image": image_url,
                })
 
        p_percent = int((present_count / total_count) * 100) if total_count > 0 else 0
        l_percent = int((late_count / present_count) * 100) if present_count > 0 else 0
 
        return Response({
            "status": {"code": 200, "response": "success"},
            "data": {
                "n_data": {
                    "label": "Active Staff",
                    "present": present_count,
                    "total": total_count,
                    "percent": p_percent,
                },
                "s_data": {
                    "label": "Late Entry",
                    "present": late_count,
                    "total": present_count,
                    "percent": l_percent,
                },
                "absent_employees": absent_list,
                "selected_date": target_date.strftime("%Y-%m-%d"),
 
                # ✅ Setting table -la irundhu — shift timing + grace
                "shift": {
                    "start": cfg["shift_start"].strftime("%I:%M %p"),
                    "end": cfg["shift_end"].strftime("%I:%M %p"),
                    "grace_minutes": cfg["grace_minutes"],
                    "late_after": cfg["late_after"].strftime("%I:%M %p"),
                    "weekly_off": sorted(
                        cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}
                    ),
                },
 
                # ✅ Holiday table + Setting.weekly_off -la irundhu
                "is_holiday": bool(hol_name),
                "holiday_name": hol_name,
                "is_weekly_off": is_weekly_off,
                "is_off_day": is_off_day,
            },
        })
 

class StudentClassCountAPIView(APIView):
    def get(self, request, id):
        try:

            # Attendance à®Ÿà¯‡à®ªà®¿à®³à®¿à®²à¯ à®‡à®°à¯à®¨à¯à®¤à¯ à®•à¯à®±à®¿à®ªà¯à®ªà®¿à®Ÿà¯à®Ÿ à®®à®¾à®£à®µà®°à¯ à®®à®±à¯à®±à¯à®®à¯ à®•à®¿à®³à®¾à®¸à¯ à®µà®•à¯ˆà®ªà¯à®ªà®Ÿà®¿ à®•à®µà¯à®£à¯à®Ÿà¯ à®Žà®Ÿà¯à®¤à¯à®¤à®²à¯
            # class_type 1=Normal, 2=Special à®Žà®© à®µà¯ˆà®¤à¯à®¤à¯à®•à¯à®•à¯Šà®³à¯à®•à®¿à®±à¯‡à®©à¯
            normal_count = Attendance.objects.filter(student_id=id, session_type=1).count()
            special_count = Attendance.objects.filter(student_id=id, session_type=2).count()


            print(normal_count,special_count,'jsabdbsdsmdbasmdbsand')

            return Response({
                "status": {"code": 200, "response": "success"},
                "data": {
                    "normal_count": normal_count,
                    "special_count": special_count
                }
            })
        except Exception as e:
            return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)


class EmployeeAPIView(APIView):
    # permission_classes = (IsAuthenticated,)

    def get(self, request, id=None):
        if id is not None:
            try:
                emp = Employee.objects.get(id=id)

                prof = Profiles.objects.get(user_id=emp.users_id)
                data = {
                    'id': emp.id,
                    'name': emp.name,
                    'mobile': emp.mobile,
                    'age': emp.age,
                    'address': emp.address,
                    'department_id': emp.department.id if emp.department else None,
                    'department_name': emp.department.name if emp.department else "N/A",
                    'designation_id': emp.designation.id if emp.designation else None,
                    'designation_name': emp.designation.name if emp.designation else "N/A",
                    'doj': emp.doj,
                    'status': emp.status,
                    'profile_pic': request.build_absolute_uri(emp.photo.url).replace("http://", "https://") if emp.photo else None,

                    'password' : prof.password


                }
                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except Employee.DoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No employee found",
                })
        else:
            try:
                employees = Employee.objects.all().order_by('-created_at')
                emp_list = []
                for index, emp in enumerate(employees, 1):


                    pro = Profiles.objects.get(user_id=emp.users_id)
                    emp_list.append({
                        'row': index,
                        'id': emp.id,
                        'name': emp.name,
                        'mobile': emp.mobile,
                        'department': emp.department.name if emp.department else "N/A",
                        'designation': emp.designation.name if emp.designation else "N/A",
                        'status': pro.status,
                        'geostatus' : pro.geostatus,
                        'profile_pic': request.build_absolute_uri(emp.photo.url).replace("http://", "https://") if emp.photo else None
                        # 'profile_pic': request.build_absolute_uri(emp.photo.url) if emp.photo else None
                    })

                print(emp_list,'HHHHHHHHHHHHHHHHHHHH')
                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": emp_list
                })
            except Exception as e:
                return Response({
                    "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                    "error": True,
                    "message": str(e),
                })

    # def post(self, request):
    #     data = request.data
    #     profile_pic = request.FILES.get('profile_pic')

    #     try:
    #         # Foreign Key à®†à®ªà¯à®œà¯†à®•à¯à®Ÿà¯à®•à®³à¯ˆ à®Žà®Ÿà¯à®¤à¯à®¤à®²à¯
    #         dept_id = data.get('department')
    #         desig_id = data.get('designation')
            
    #         dept = Department.objects.get(id=dept_id) if dept_id else None
    #         desig = Designation.objects.get(id=desig_id) if desig_id else None

    #         employee = Employee.objects.create(
    #             name=data.get('name'),
    #             mobile=data.get('mobile'),
    #             age=data.get('age'),
    #             address=data.get('address'),
    #             department=dept,
    #             designation=desig,
    #             doj=data.get('doj'),
    #             photo=profile_pic,
    #             created_by=request.user if request.user.is_authenticated else None
    #         )

    #         if profile_pic:
    #             try:
    #                 # சேமிக்கப்பட்ட படத்தைத் திறக்கவும்
    #                 img = Image.open(employee.photo.path).convert('RGB')
    #                 img_array = np.array(img)
                    
    #                 # என்கோடிங் எடுக்கவும்
    #                 encodings = face_recognition.face_encodings(img_array)
                    
    #                 if encodings:
    #                     # முதல் என்கோடிங்கை மட்டும் எடுத்து சேமிக்கவும்
    #                     employee.save_encoding(encodings[0])
    #                 else:
    #                     # முகம் தெரியவில்லை என்றால் ஊழியரை டெலிட் செய்ய வேண்டுமானால் செய்யலாம் 
    #                     # அல்லது error அனுப்பலாம்
    #                     print(f"Face not detected for {employee.name}")
    #             except Exception as face_err:
    #                 print(f"Encoding Error: {face_err}")
            
    #         return Response({
    #             "status": {"code": status.HTTP_201_CREATED, "response": "success"},
    #             "error": None,
    #             "message": "Employee created successfully",
    #         })
    #     except Exception as e:
    #         return Response({
    #             "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
    #             "error": True,
    #             "message": f"Error: {str(e)}"
    #         })
    # def put(self, request, id):
    #     data = request.data
    #     profile_pic = request.FILES.get('profile_pic')

    #     try:
    #         emp = Employee.objects.get(id=id)
            
    #         # Department & Designation அப்டேட்
    #         if 'department' in data:
    #             emp.department = Department.objects.get(id=data.get('department'))
    #         if 'designation' in data:
    #             emp.designation = Designation.objects.get(id=data.get('designation'))

    #         emp.name = data.get("name", emp.name)
    #         emp.mobile = data.get("mobile", emp.mobile)
    #         emp.age = data.get("age", emp.age)
    #         emp.address = data.get("address", emp.address)
    #         emp.doj = data.get("doj", emp.doj)
    #         emp.status = data.get("status", emp.status)
            
    #         # 1. புதிய போட்டோ வந்தால் அதை சேமிக்கிறோம்
    #         if profile_pic:
    #             emp.photo = profile_pic
    #             emp.save()
    #         else:
    #             emp.save()

    #         # 2. முக்கியமான மாற்றம்: 
    #         # புதிய போட்டோ வந்திருந்தாலோ அல்லது பழைய போட்டோ இருந்து அதற்கு இன்னும் என்கோடிங் செய்யப்படாமல் இருந்தாலோ...
    #         if profile_pic or (emp.photo and not emp.face_encoding_data):
    #             try:
    #                 # போட்டோ பாத் இருக்கிறதா என்று உறுதி செய்கிறோம்
    #                 if emp.photo and os.path.exists(emp.photo.path):
    #                     img = Image.open(emp.photo.path).convert('RGB')
    #                     encodings = face_recognition.face_encodings(np.array(img))
                        
    #                     if encodings:
    #                         emp.save_encoding(encodings[0])
    #                         print(f"Encoding successful for: {emp.name}")
    #                     else:
    #                         # முகம் தெரியவில்லை என்றால்
    #                         if profile_pic: # புதிய போட்டோவாக இருந்தால் மட்டும் நல் (Null) ஆக்கலாம்
    #                             emp.face_encoding_data = None
    #                             emp.save()
    #             except Exception as face_err:
    #                 print(f"Face encoding error: {face_err}")

    #         return Response({
    #             "status": {"code": 200, "response": "success"},
    #             "message": "Employee updated and Face ID generated"
    #         })

    #     except Employee.DoesNotExist:
    #         return Response({"status": {"code": 404, "response": "failed"}, "message": "Employee not found"})
    #     except Exception as e:
    #         return Response({"status": {"code": 400, "response": "failed"}, "message": str(e)})


    def _save_face_encoding(self, employee):
        """
        Employee photo-வில் face encoding எடுத்து save பண்ணும்.
        எந்த size photo-வும் handle பண்ணும்.
        Returns: (success: bool, message: str)
        """
        if not employee.photo:
            return False, "Photo இல்லை"
 
        photo_path = employee.photo.path
        if not os.path.exists(photo_path):
            return False, "Photo file கிடைக்கவில்லை"
 
        try:
            img_array, face_locations = preprocess_image(photo_path)
 
            if not face_locations:
                return False, "Photo-ல் முகம் தெரியவில்லை"
 
            # Register-க்கு num_jitters=5 — better accuracy
            encodings = face_recognition.face_encodings(
                img_array,
                known_face_locations=face_locations,
                num_jitters=5,
            )
            if not encodings:
                return False, "Face encode ஆகவில்லை"
 
            employee.save_encoding(encodings[0])
            invalidate_cache()
            print(f"[Encoding] Saved for: {employee.name}")
            return True, "Encoding saved"
 
        except Exception as e:
            print(f"[Encoding Error] {employee.name}: {e}")
            return False, str(e)
 
    def post(self, request):
        data = request.data
        profile_pic = request.FILES.get('profile_pic')

        try:
            dept_id = data.get('department')
            desig_id = data.get('designation')
            dept = Department.objects.get(id=dept_id) if dept_id else None
            desig = Designation.objects.get(id=desig_id) if desig_id else None

            name = (data.get('name') or "").strip()
            email = data.get('email')
            phone = data.get('mobile') or data.get('phone')
            address = data.get('address')

            if not name:
                return Response({
                    "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                    "error": True,
                    "message": "Name is required",
                })

            # ✅ Default password: "<Name>@123"
            raw_password = f"{name.replace(' ', '')}@123"
            hashed_password = make_password(raw_password)

            # ✅ Username unique
            base_username = name.replace(" ", "").lower()
            username = base_username
            counter = 1
            while User.objects.filter(username=username).exists():
                username = f"{base_username}{counter}"
                counter += 1

            # ✅ USER TABLE — hashed password
            van = User(username=username, password=hashed_password)
            van.save()

            # ✅ PROFILES TABLE — plain password
            profile_data = Profiles(
                role=3,
                password=raw_password,
                name=name,
                phone=phone,
                address=address,
                user=van,
                profiles=profile_pic,
            )
            profile_data.save()

            # ✅ EMPLOYEE TABLE — users_id link pannurom
            employee = Employee.objects.create(
                name=name,
                mobile=data.get('mobile'),
                age=data.get('age'),
                address=address,
                department=dept,
                designation=desig,
                doj=data.get('doj'),
                photo=profile_pic,
                users_id=van.id,   # ⚠️ ithu dhan users_id column - field name unga model la confirm pannunga
                created_by=request.user if request.user.is_authenticated else None,
            )

            encoding_status = "Photo இல்லை"
            if profile_pic:
                ok, encoding_status = self._save_face_encoding(employee)
                if not ok:
                    print(f"[Warning] Encoding failed for {employee.name}: {encoding_status}")

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Employee created successfully",
                "encoding_status": encoding_status,
                "login_username": username,
            })

        except Exception as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Error: {str(e)}",
            })
    def put(self, request, id):
        data        = request.data
        profile_pic = request.FILES.get('profile_pic')

        try:
            emp = Employee.objects.get(id=id)

            # ============================================
            # ✅ STEP 1: EMPLOYEE TABLE UPDATE (main update)
            # ============================================
            if 'department' in data:
                emp.department = Department.objects.get(id=data.get('department'))

            if 'designation' in data:
                emp.designation = Designation.objects.get(id=data.get('designation'))

            new_name = data.get("name", emp.name).strip() if data.get("name") else emp.name
            name_changed = (new_name != emp.name)

            emp.name    = new_name
            emp.mobile  = data.get("mobile",  emp.mobile)
            emp.age     = data.get("age",     emp.age)
            emp.address = data.get("address", emp.address)
            emp.doj     = data.get("doj",     emp.doj)
            emp.status  = data.get("status",  emp.status)

            if profile_pic:
                emp.photo = profile_pic

            emp.save()   # ✅ Employee table update save aaguthu

            # ============================================
            # ✅ STEP 2: USER + PROFILES TABLE SYNC
            # ============================================
            profile_status = "Profile link illa"
            user_id = emp.users_id

            

            if user_id:
                try:
                    van_user = User.objects.get(id=user_id)
                    profile_obj = Profiles.objects.get(user=van_user)

                    raw_password = data.get("password")

                    profile_obj.name = new_name
                    profile_obj.phone = data.get(
                        "mobile", profile_obj.phone
                    )
                    profile_obj.address = data.get(
                        "address", profile_obj.address
                    )

                    if profile_pic:
                        profile_obj.profiles = profile_pic

                    # Update name only when changed
                    if name_changed:
                        van_user.username = new_name

                    # Update password independently
                    if raw_password:
                        van_user.password = make_password(raw_password)

                        # If Profiles.password is also required,
                        # store the hash there too.
                        profile_obj.password = raw_password

                    van_user.save()
                    profile_obj.save()

                    profile_status = "Profile updated successfully"

                except User.DoesNotExist:
                    profile_status = "User record not found"

                except Profiles.DoesNotExist:
                    profile_status = "Profile record not found"


            # ============================================
            # ✅ STEP 3: FACE ENCODING (if photo changed)
            # ============================================
            encoding_status = "Photo மாறவில்லை"
            if profile_pic or (emp.photo and not emp.face_encoding_data):
                ok, encoding_status = self._save_face_encoding(emp)
                if not ok:
                    print(f"[Warning] Encoding update failed for {emp.name}: {encoding_status}")

            return Response({
                "status":          {"code": 200, "response": "success"},
                "message":         "Employee updated successfully",
                "encoding_status": encoding_status,
                "profile_status":  profile_status,
            })

        except Employee.DoesNotExist:
            return Response({
                "status":  {"code": 404, "response": "failed"},
                "message": "Employee not found",
            })
        except Exception as e:
            return Response({
                "status":  {"code": 400, "response": "failed"},
                "message": str(e),
            })
    def delete(self, request, id):
        try:
            emp = Employee.objects.get(id=id)
            emp.delete()
            return Response({
                "status": {"code": 200, "response": "success"},
                "message": "Employee deleted"
            })
        except Employee.DoesNotExist:
            return Response({"status": {"code": 404, "response": "failed"}, "message": "Not found"})

    # def delete(self, request, id):
    #     if id is not None:
    #         try:
    #             product_id_str = str(id)
    #             is_used = Enquiry.objects.filter(product__contains=[product_id_str]).exists()

    #             if is_used:
    #                 return Response({
    #                     "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
    #                     "error": "Product is used in enquiry",
    #                     "message": "Cannot delete product. It's used in one or more enquiries."
    #                 })

    #             # If not used, delete the product
    #             product = Product.objects.get(id=id)
    #             product.delete()

    #             return Response({
    #                 "status": {"code": status.HTTP_200_OK, "response": "success"},
    #                 "error": None,
    #                 "message": "Product deleted successfully"
    #             })

    #         except Product.DoesNotExist:
    #             return Response({
    #                 "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
    #                 "error": None,
    #                 "message": "No product found"
    #             })


class FetchdesignationAPIView(APIView):
    def get(self, request, id):
        company = Designation.objects.filter(department_id=id)

        data = []

        for i in company:
            data.append({
                'name': i.name,
                'id': i.id
            })

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": data
        })
    
    


class StudentStatusAPIView(APIView):
    def patch(self, request, id):
        try:
            student = Employee.objects.get(id=id)
            # 1-à®†à®• à®‡à®°à¯à®¨à¯à®¤à®¾à®²à¯ 2-à®•à¯à®•à¯ à®®à®¾à®±à¯à®±à®µà¯à®®à¯, à®‡à®²à¯à®²à¯ˆà®¯à¯†à®©à¯à®±à®¾à®²à¯ 1-à®•à¯à®•à¯ à®®à®¾à®±à¯à®±à®µà¯à®®à¯
            if student.status == 1:
                student.status = 2
            else:
                student.status = 1
            
            student.save()
            return Response({
                "status": {"code": 200, "response": "success"},
                "new_status": student.status,
                "message": "Status updated successfully"
            })
        except Employee.DoesNotExist:
            return Response({"error": "Student not found"}, status=404)
from datetime import datetime, time
import calendar

import calendar
from datetime import datetime, time, timedelta, date as date_cls

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response

from .models import Setting, Holiday, Employee, Attendance


# class MonthlyAttendanceAPIView(APIView):

#     def get(self, request):
#         val = request.query_params.get('val')
#         if val:
#             try:
#                 year, month = map(int, val.split('-'))
#             except ValueError:
#                 return Response({"error": "Invalid val format. Use YYYY-MM"}, status=400)
#         else:
#             month = int(request.query_params.get('month', datetime.now().month))
#             year  = int(request.query_params.get('year',  datetime.now().year))

#         student_id_param  = request.query_params.get('student_id')
#         office_start_time = time(8, 30, 0)

#         num_days   = calendar.monthrange(year, month)[1]
#         month_name = calendar.month_name[month][:3]

#         days_list = []
#         for d in range(1, num_days + 1):
#             dt = datetime(year, month, d)
#             # âœ… key = "2026-03-18" â€” existing frontend match à®†à®•à¯à®®à¯
#             date_key = f"{year}-{month:02d}-{d:02d}"
#             days_list.append({
#                 "day_num":  d,
#                 "display":  f"{month_name} {d:02d}",
#                 "day_name": dt.strftime("%a").upper(),
#                 "date":     date_key,
#             })

#         if student_id_param and student_id_param != "all":
#             students = Employee.objects.filter(id=student_id_param)
#         else:
#             students = Employee.objects.all()

#         attendance_records = Attendance.objects.filter(
#             date__month=month,
#             date__year=year
#         ).select_related('employee')

#         report_data = []
#         for student in students:
#             student_att = {}
#             total_p     = 0
#             total_a     = 0
#             late_count  = 0

#             for day_obj in days_list:
#                 d        = day_obj["day_num"]
#                 date_key = day_obj["date"]

#                 rec = attendance_records.filter(
#                     employee_id=student.id,
#                     date__day=d
#                 ).first()

#                 if rec:
#                     status = "P" if rec.status == "Present" else "A"
#                 else:
#                     status = "-"

#                 # âœ… Same logic as TodayAttendanceAPIView
#                 if rec and rec.in_time and rec.in_time > office_start_time:
#                     late_count += 1

#                 student_att[date_key] = {
#                     "status": status,
#                     "ot":     "00:00",
#                 }

#                 if status == "P":
#                     total_p += 1
#                 elif status == "A":
#                     total_a += 1

#             report_data.append({
#                 "name":          student.studentName,
#                 "code":          getattr(student, 'emp_id', str(student.id)),
#                 "attendance":    student_att,
#                 "total_present": total_p,
#                 "total_absent":  total_a,
#                 "late_count":    late_count,  # âœ… à®‡à®¤à¯ à®ªà¯à®¤à¯à®šà®¾ à®šà¯‡à®°à¯à®•à¯à®•à®¿à®±à¯‹à®®à¯
#                 "total_ot":      "00:00",
#             })

#         print('MONTHLY API HIT - late_count working!', flush=True)

#         return Response({
#             "month":  month,
#             "year":   year,
#             "days":   days_list,
#             "report": report_data
#         })

from datetime import date
from django.utils import timezone

import threading
import time as time_module

# ============================================================
# GLOBAL IN-MEMORY CACHE
# ============================================================
_encoding_cache = {
    "encodings":   [],
    "ids":         [],
    "last_reload": 0,
}
_cache_lock = threading.Lock()
CACHE_TTL   = 300  # 5 minutes
 
 
def load_encoding_cache(force=False):
    global _encoding_cache
    # now = time.time()
    now = time_module.time()     
    with _cache_lock:
        if not force and (now - _encoding_cache["last_reload"]) < CACHE_TTL:
            return
        employees = Employee.objects.exclude(
            face_encoding_data__isnull=True
        ).exclude(face_encoding_data='')
        encodings, ids = [], []
        for emp in employees:
            enc = emp.get_encoding()
            if enc is not None:
                encodings.append(enc)
                ids.append(emp.id)
        _encoding_cache["encodings"]   = encodings
        _encoding_cache["ids"]         = ids
        _encoding_cache["last_reload"] = now
        print(f"[Cache] {len(encodings)} encodings loaded.")
 
 
def invalidate_cache():
    with _cache_lock:
        _encoding_cache["last_reload"] = 0
 
 
# ============================================================
# SMART IMAGE PREPROCESSOR
# எந்த size, எந்த quality photo-வும் face detect ஆகும்
# ============================================================
def preprocess_image(source):
    """
    source: uploaded file object அல்லது file path string
 
    5 different attempts-ல் face detect பண்ணும்:
      1. Normal HOG
      2. HOG + upsample (small photos)
      3. Extra brightness (dark photos)
      4. 800px width resize (portrait photos)
      5. Horizontally flipped (selfie mirror issue)
 
    Returns: (img_array, face_locations)
    """
    # ── Load image ──
    if isinstance(source, str):
        pil_img = Image.open(source).convert('RGB')
    else:
        source.seek(0)
        pil_img = Image.open(io.BytesIO(source.read())).convert('RGB')
 
    w, h = pil_img.size
    print(f"[DEBUG] Original size: {w}x{h}")
 
    # ── Too small → upscale (thumbnail, WhatsApp compressed) ──
    short = min(w, h)
    if short < 200:
        scale   = 300 / short
        pil_img = pil_img.resize(
            (int(w * scale), int(h * scale)), Image.LANCZOS
        )
        w, h = pil_img.size
        print(f"[DEBUG] Upscaled to: {w}x{h}")
 
    # ── Too large → downscale (DSLR, 4K photos) ──
    long = max(w, h)
    if long > 1200:
        scale   = 1200 / long
        pil_img = pil_img.resize(
            (int(w * scale), int(h * scale)), Image.LANCZOS
        )
        w, h = pil_img.size
        print(f"[DEBUG] Downscaled to: {w}x{h}")
 
    # ── Enhance: brightness + contrast + sharpness ──
    pil_img = ImageEnhance.Brightness(pil_img).enhance(1.2)
    pil_img = ImageEnhance.Contrast(pil_img).enhance(1.3)
    pil_img = ImageEnhance.Sharpness(pil_img).enhance(1.5)
 
    img_array = np.array(pil_img)
 
    # ── Attempt 1: Normal HOG ──
    locs = face_recognition.face_locations(img_array, model="hog", number_of_times_to_upsample=1)
    print(f"[DEBUG] Attempt 1 (HOG): {len(locs)} faces")
    if locs:
        return img_array, locs
 
    # ── Attempt 2: HOG with upsample=2 (small face) ──
    locs = face_recognition.face_locations(img_array, model="hog", number_of_times_to_upsample=2)
    print(f"[DEBUG] Attempt 2 (HOG upsample=2): {len(locs)} faces")
    if locs:
        return img_array, locs
 
    # ── Attempt 3: Extra brightness (dark photo) ──
    bright_arr = np.array(ImageEnhance.Brightness(pil_img).enhance(1.8))
    locs       = face_recognition.face_locations(bright_arr, model="hog", number_of_times_to_upsample=2)
    print(f"[DEBUG] Attempt 3 (extra bright): {len(locs)} faces")
    if locs:
        return bright_arr, locs
 
    # ── Attempt 4: 800px width resize ──
    new_h      = int(800 * h / w) if w > 0 else 800
    resized    = pil_img.resize((800, new_h), Image.LANCZOS)
    resize_arr = np.array(resized)
    locs       = face_recognition.face_locations(resize_arr, model="hog", number_of_times_to_upsample=1)
    print(f"[DEBUG] Attempt 4 (800w resize): {len(locs)} faces")
    if locs:
        return resize_arr, locs
 
    # ── Attempt 5: Horizontally flipped (selfie mirror issue) ──
    flipped  = pil_img.transpose(Image.FLIP_LEFT_RIGHT)
    flip_arr = np.array(flipped)
    locs     = face_recognition.face_locations(flip_arr, model="hog", number_of_times_to_upsample=1)
    print(f"[DEBUG] Attempt 5 (flipped): {len(locs)} faces")
    if locs:
        return flip_arr, locs
 
    return img_array, []
 
 
# ============================================================
# Attendance API — GET / POST
# ============================================================
class AttendanceAPIView(APIView):
 
    def get(self, request):
        try:
            query_date   = request.query_params.get('date', date.today().isoformat())
            session_type = request.query_params.get('session_type')
            attendances  = Attendance.objects.filter(date=query_date).order_by('-created_at')
            if session_type:
                attendances = attendances.filter(session_type=session_type)
 
            attendance_list = []
            for index, att in enumerate(attendances, 1):
                attendance_list.append({
                    'row':          index,
                    'id':           att.id,
                    'studentId':    att.student.id,
                    'studentName':  att.student.studentName,
                    'status':       att.status,
                    'session_type': att.session_type,
                    'date':         att.date,
                })
            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error":  None,
                "data":   attendance_list,
            })
        except Exception as e:
            return Response({
                "status":  {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error":   True,
                "message": str(e),
            })
 
    def post(self, request):
        uploaded_file = request.FILES.get('image')
        if not uploaded_file:
            return Response({"message": "Image required"}, status=400)
 
        try:
            # ── 1. Preprocess + detect ──
            img_array, face_locations = preprocess_image(uploaded_file)
 
            if not face_locations:
                return Response({
                    "message": "முகம் சரியாக தெரியவில்லை! நேரடியாக camera-ஐ பாருங்கள்."
                }, status=400)
 
            # ── 2. Encode ──
            target_encodings = face_recognition.face_encodings(
                img_array,
                known_face_locations=face_locations,
                num_jitters=1,
            )
            if not target_encodings:
                return Response({
                    "message": "முகம் தெரிகிறது ஆனால் encode ஆகவில்லை. மீண்டும் முயற்சிக்கவும்."
                }, status=400)
 
            target_enc = target_encodings[0]
 
            # ── 3. Cache load ──
            load_encoding_cache()
            known_encodings = _encoding_cache["encodings"]
            known_ids       = _encoding_cache["ids"]
 
            if not known_encodings:
                return Response({
                    "message": "பதிவு செய்யப்பட்ட முகங்கள் எதுவும் இல்லை!"
                }, status=400)
 
            # ── 4. Vectorized match ──
            face_distances = face_recognition.face_distance(known_encodings, target_enc)
            best_idx       = int(np.argmin(face_distances))
            best_distance  = float(face_distances[best_idx])
 
            print(f"[DEBUG] Best match distance: {best_distance:.4f} (threshold: 0.50)")
 
            if best_distance > 0.50:
                return Response({
                    "message": "யார் என்று அடையாளம் காண முடியவில்லை!"
                }, status=401)
 
            # ── 5. Save attendance ──
            emp_id     = known_ids[best_idx]
            found_user = Employee.objects.get(id=emp_id)
            today      = timezone.now().date()
            now_time   = timezone.now().time()
            location = request.data.get("location_name")

 
            att, created = Attendance.objects.get_or_create(
                employee_id=found_user.id,
                date=today,
                defaults={'status': 'Present', 'in_time': now_time,'location': location,
}
            )
 
            if not created:
                att.out_time = now_time
                att.save()
                msg    = f"வெளியேறும் நேரம் பதிவு: {found_user.name}"
                action = "out"
            else:
                msg    = f"வருகை பதிவு: {found_user.name}"
                action = "in"
 
            return Response({
                "status":  "success",
                "message": msg,
                "data": {
                    "name":     found_user.name,
                    "action":   action,
                    "distance": round(best_distance, 3),
                },
            })
 
        except Exception as e:
            print(f"[ERROR] Attendance post: {e}")
            return Response({"error": str(e)}, status=500)
class DesignationAPIView(APIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request, id=None):
        if id is not None:
            try:
                designation = Designation.objects.get(id=id)
                data = {
                    'id': designation.id,
                    'name': designation.name,
                    'description': designation.description,
                    'department': designation.department_id
                }

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })
        else:
            try:
                index = 0
                designations = []

                for designation in Designation.objects.all():
                    index += 1

                    try:
                        department = Department.objects.get(id=designation.department_id)
                        department = department.name
                    except Department.DoesNotExist:
                        department = ''

                    designations.append({
                        'row': index,
                        'id': designation.id,
                        'name': designation.name,
                        'description': designation.description,
                        'department': department
                    })

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": designations
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })

    def post(self, request):
        data = request.data

        try:
            department = Designation.objects.create(
                name=data["name"],
                description=data["description"],
                department=Department.objects.get(id=data["department"]),
                created_by=request.user
            )
            department.save()

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Designation created successfully"
            })
        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {e}"
            })

    def put(self, request, id):
        data = request.data

        try:
            designation = Designation.objects.get(id=id)

            designation.name = data["name"]
            designation.description = data["description"]
            designation.department = Department.objects.get(id=data["department"])
            designation.save()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": "Designation updated successfully"
            })
        except ObjectDoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "No data found"
            })

    def delete(self, request, id):
        if id is not None:
            try:
                designation = Designation.objects.get(id=id)
                designation.delete()

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "message": "Designation deleted successfully"
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": None,
                    "message": "No data found"
                })
        else:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": None,
                "message": "Something went wrong"
            })
        
class DepartmentAPIView(APIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request, id=None):
        if id is not None:
            try:
                company = Department.objects.get(id=id)
                data = {
                    'id': company.id,
                    'name': company.name,
                    'description': company.description
                }

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })
        else:
            try:

                index = 0
                companies = []

                for company in Department.objects.all():
                    index += 1

                    companies.append({
                        'row': index,
                        'id': company.id,
                        'name': company.name,
                        'description': company.description,
                    })

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": companies
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })

    def post(self, request):
        data = request.data

        try:
            company = Department.objects.create(
                name=data["name"],
                description=data["description"],
                created_by=request.user
            )
            company.save()

            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Department created successfully"
            })
        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {e}"
            })

    def put(self, request, id):
        data = request.data

        try:
            company = Department.objects.get(id=id)
            company.name = data["name"]
            company.description = data["description"]
            company.save()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": "Department updated successfully"
            })
        except ObjectDoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "No data found"
            })
        except Exception as e:
            print(e)
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Something went wrong"
            })

    def delete(self, request, id):
        if id is not None:
            try:
                company = Department.objects.get(id=id)
                company.delete()

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "message": "Department deleted successfully"
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": None,
                    "message": "No data found"
                })

from datetime import datetime, time as dt_time
from decimal import Decimal, InvalidOperation

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response

from .models import Setting, Holiday


# ══════════════════════════════════════════════
#  HELPERS
# ══════════════════════════════════════════════
TIME_FORMATS = ["%I:%M %p", "%I:%M%p", "%H:%M:%S", "%H:%M"]


def parse_time(value):
    """
    '09:30 AM', '9:30AM', '09:30', '09:30:00' -> datetime.time
    Invalid na ValueError raise pannum.
    """
    if value in (None, ""):
        return None
    if isinstance(value, dt_time):
        return value

    raw = str(value).strip().upper().replace(".", "")
    for fmt in TIME_FORMATS:
        try:
            return datetime.strptime(raw, fmt).time()
        except ValueError:
            continue
    raise ValueError(f"Invalid time '{value}'. Use hh:mm AM/PM format.")


def parse_int(value, field):
    try:
        return int(value)
    except (TypeError, ValueError):
        raise ValueError(f"{field} must be a number.")


def parse_decimal(value, field):
    try:
        return Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        raise ValueError(f"{field} must be a number.")


def parse_bool(value):
    if isinstance(value, bool):
        return value
    return str(value).strip().lower() in ("1", "true", "yes", "on")


# ══════════════════════════════════════════════
#  SETTINGS
# ══════════════════════════════════════════════

import calendar
from datetime import datetime, time, timedelta, date as date_cls
 
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
 
from .models import Setting, Holiday, Employee, Attendance
 
 
# ══════════════════════════════════════════════
#  SHARED HELPERS — Settings + Holiday read pannum
# ══════════════════════════════════════════════
 
# Python date.weekday(): Mon=0, Tue=1, Wed=2, Thu=3, Fri=4, Sat=5, Sun=6
DAY_KEYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
 
 
def fmt_hhmm(total_minutes):
    total_minutes = max(0, int(total_minutes))
    return f"{total_minutes // 60:02d}:{total_minutes % 60:02d}"
 
 
def get_attendance_config():
    """
    Setting table (pk=1) -la irundhu shift timing, grace period,
    weekly off days edukkum. Monthly + Today rendu API-um idhe
    function-a use pannanum — settings maathi na rendu idathulayum
    automatic-a update aagum.
    """
    setting, _ = Setting.objects.get_or_create(pk=1)
 
    shift_start = setting.shift_start or time(9, 30)
    shift_end = setting.shift_end or time(18, 30)
    grace = int(setting.grace_minutes or 0)
 
    # DB-la "Sun,Sat" nu irukkum -> {"Sun", "Sat"} -> weekday index {6, 5}
    off_names = {
        p.strip()[:3].title()
        for p in (setting.weekly_off or "").split(",")
        if p.strip()
    }
    weekly_off = {i for i, nm in enumerate(DAY_KEYS) if nm in off_names}
 
    # shift_start + grace = late_after. Idhukku mela vandha "Late"
    late_after = (
        datetime.combine(date_cls.today(), shift_start) + timedelta(minutes=grace)
    ).time()
 
    return {
        "setting": setting,
        "shift_start": shift_start,
        "shift_end": shift_end,
        "grace_minutes": grace,
        "late_after": late_after,
        "weekly_off": weekly_off,          # set of weekday indexes e.g. {5, 6}
        "weekly_off_names": off_names,     # e.g. {"Sat", "Sun"}
        "half_day_hours": float(setting.half_day_hours or 4),
    }
 
 
def get_holiday_map(start_date, end_date):
    """Holiday table -la irundhu {date_obj: name} — date range kku eduthu tharum."""
    qs = Holiday.objects.filter(date__range=[start_date, end_date])
    return {h.date: h.name for h in qs}
 
 
def emp_display_name(emp):
    return getattr(emp, "name", None) or getattr(emp, "studentName", "") or "—"
 
 
def emp_display_code(emp):
    return getattr(emp, "mobile", None) or getattr(emp, "emp_id", None) or str(emp.id)
 
 
def is_late(in_time, cfg):
    return bool(in_time and in_time > cfg["late_after"])
 
 
# ✅ Standard work day = 8 hours. Idhukku mela worked-a irundha mattum
# adhu OT-a count aagum -- shift_end kadanthadhu mattum OT illa.
STANDARD_WORK_SECONDS = 8 * 3600
 
 
def ot_seconds(in_time, out_time, cfg):
    """
    Total worked time (out_time - in_time) 8 hours-a thaandi irundha,
    andha extra seconds mattum OT-a return pannum.
    in_time/out_time illama irundha, or out_time < in_time (data issue)
    irundha, OT = 0.
    """
    if not cfg["setting"].overtime_enabled:
        return 0
    if not in_time or not out_time:
        return 0
 
    d = date_cls.today()
    worked = datetime.combine(d, out_time) - datetime.combine(d, in_time)
    worked_sec = worked.total_seconds()
 
    if worked_sec <= STANDARD_WORK_SECONDS:
        return 0
    return int(worked_sec - STANDARD_WORK_SECONDS)
 
 
# ══════════════════════════════════════════════
#  SETTINGS
# ══════════════════════════════════════════════
class SettingAPIView(APIView):
    """Single settings row (pk=1) — GET fetch, PUT partial update."""
 
    def get_object(self):
        setting, _ = Setting.objects.get_or_create(pk=1)
        return setting
 
    def get(self, request):
        setting = self.get_object()
        data = {
            "shift_start": setting.shift_start.strftime("%I:%M %p") if setting.shift_start else None,
            "shift_end": setting.shift_end.strftime("%I:%M %p") if setting.shift_end else None,
            "weekly_off": [d for d in (setting.weekly_off or "").split(",") if d],
            "grace_minutes": setting.grace_minutes,
            "half_day_hours": float(setting.half_day_hours) if setting.half_day_hours is not None else 0,
            "overtime_enabled": setting.overtime_enabled,
            "geofence_enabled": setting.geofence_enabled,
            "face_sensitivity": setting.face_sensitivity,
            "company_name": setting.company_name,
            "time_zone": setting.time_zone,
        }
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": data,
        })
 
    def put(self, request):
        setting = self.get_object()
        data = request.data
 
        def parse_time(value):
            if value in (None, ""):
                return None
            raw = str(value).strip().upper().replace(".", "")
            for fmt in ["%I:%M %p", "%I:%M%p", "%H:%M:%S", "%H:%M"]:
                try:
                    return datetime.strptime(raw, fmt).time()
                except ValueError:
                    continue
            raise ValueError(f"Invalid time '{value}'. Use hh:mm AM/PM format.")
 
        try:
            if "shift_start" in data:
                setting.shift_start = parse_time(data.get("shift_start"))
            if "shift_end" in data:
                setting.shift_end = parse_time(data.get("shift_end"))
            if "weekly_off" in data:
                wo = data.get("weekly_off")
                setting.weekly_off = ",".join(wo) if isinstance(wo, list) else (wo or "")
            if "grace_minutes" in data:
                setting.grace_minutes = int(data.get("grace_minutes"))
            if "half_day_hours" in data:
                setting.half_day_hours = float(data.get("half_day_hours"))
            if "overtime_enabled" in data:
                setting.overtime_enabled = bool(data.get("overtime_enabled"))
            if "geofence_enabled" in data:
                setting.geofence_enabled = bool(data.get("geofence_enabled"))
            if "face_sensitivity" in data:
                setting.face_sensitivity = data.get("face_sensitivity")
            if "company_name" in data:
                setting.company_name = (data.get("company_name") or "").strip()
            if "time_zone" in data:
                setting.time_zone = (data.get("time_zone") or "").strip()
 
            setting.updated_by = request.user if request.user.is_authenticated else None
            setting.save()
        except (ValueError, TypeError) as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": str(e),
            }, status=status.HTTP_400_BAD_REQUEST)
 
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": "Settings updated successfully",
            "data": self.get(request).data["data"],
        })
 
 
# ══════════════════════════════════════════════
#  HOLIDAY
# ══════════════════════════════════════════════
class HolidayAPIView(APIView):
    """Holiday CRUD — List / Add / Delete."""
 
    def get(self, request, id=None):
        if id is not None:
            try:
                h = Holiday.objects.get(id=id)
            except Holiday.DoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "Holiday not found",
                }, status=status.HTTP_404_NOT_FOUND)
            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "data": {"id": h.id, "name": h.name, "date": str(h.date)},
            })
 
        holidays = Holiday.objects.all().order_by("date")
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": [{"id": h.id, "name": h.name, "date": str(h.date)} for h in holidays],
        })
 
    def post(self, request):
        name = (request.data.get("name") or "").strip()
        date_val = (request.data.get("date") or "").strip()
 
        if not name or not date_val:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Name and date are required.",
            }, status=status.HTTP_400_BAD_REQUEST)
 
        try:
            datetime.strptime(date_val, "%Y-%m-%d")
        except ValueError:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Date must be in YYYY-MM-DD format.",
            }, status=status.HTTP_400_BAD_REQUEST)
 
        holiday = Holiday.objects.create(
            name=name,
            date=date_val,
            created_by=request.user if request.user.is_authenticated else None,
        )
        return Response({
            "status": {"code": status.HTTP_201_CREATED, "response": "success"},
            "error": None,
            "message": "Holiday added successfully",
            "data": {"id": holiday.id, "name": holiday.name, "date": str(holiday.date)},
        }, status=status.HTTP_201_CREATED)
 
    def delete(self, request, id=None):
        if id is None:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Holiday id required in URL.",
            }, status=status.HTTP_400_BAD_REQUEST)
 
        try:
            holiday = Holiday.objects.get(id=id)
        except Holiday.DoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "Holiday not found",
            }, status=status.HTTP_404_NOT_FOUND)
 
        holiday.delete()
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": "Holiday deleted",
        })
 
 
# ══════════════════════════════════════════════
#  EMPLOYEE MINI LIST — filter dropdown ku
# ══════════════════════════════════════════════
class EmployeeMiniListAPIView(APIView):
    def get(self, request):
        employees = Employee.objects.filter(status=1).order_by("name")
        data = [
            {"id": e.id, "name": emp_display_name(e), "code": emp_display_code(e)}
            for e in employees
        ]
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": data,
        })
 
 
# ══════════════════════════════════════════════
#  MONTHLY ATTENDANCE
#  (Class name "MonthattendanceAPIView" — spelling idhe
#   ungal urls.py already point pannirukku, adha maathaadhinga)
# ══════════════════════════════════════════════
# class MonthattendanceAPIView(APIView):
#     """
#     Status per day: P / A / H (Holiday table) / W (Setting.weekly_off) / '--' (future)
#     Late  = in_time > shift_start + grace_minutes (Setting table)
#     3 Late = 0.5 day cut from Present (effective_present)
#     Optional filter: ?employee_id=<id>  (illa na ellame varum)
#     """
 
#     def get(self, request, *args, **kwargs):
#         val = request.GET.get("val")
#         if not val:
#             return Response(
#                 {"error": "val is required. Use ?val=YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         try:
#             first_day = datetime.strptime(val + "-01", "%Y-%m-%d").date()
#         except ValueError:
#             return Response(
#                 {"error": "Invalid val format. Use YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         if first_day.month == 12:
#             last_day = first_day.replace(year=first_day.year + 1, month=1, day=1) - timedelta(days=1)
#         else:
#             last_day = first_day.replace(month=first_day.month + 1, day=1) - timedelta(days=1)
 
#         dates_in_month = [
#             first_day + timedelta(days=i) for i in range((last_day - first_day).days + 1)
#         ]
 
#         cfg = get_attendance_config()
#         holiday_map = get_holiday_map(first_day, last_day)  # ✅ Holiday table
 
#         emp_param = request.GET.get("employee_id") or request.GET.get("student_id")
#         employees = Employee.objects.filter(status=1)
#         if emp_param and emp_param != "all":
#             employees = employees.filter(id=emp_param)
 
#         all_attendance = Attendance.objects.filter(date__range=[first_day, last_day])
#         today = date_cls.today()
#         final_report = []
 
#         for emp in employees:
#             emp_records = {r.date: r for r in all_attendance if r.employee_id == emp.id}
#             total_p = total_a = late_count = 0
#             total_ot_sec = 0
#             attendance_map = {}
 
#             for day in dates_in_month:
#                 day_str = day.strftime("%Y-%m-%d")
#                 ot_display = "00:00"
#                 wd = day.weekday()
 
#                 is_holiday = day in holiday_map              # ✅ Holiday table
#                 is_weekly_off = wd in cfg["weekly_off"]       # ✅ Setting.weekly_off
 
#                 if is_holiday:
#                     status_code = "H"
#                 elif is_weekly_off:
#                     status_code = "W"
#                 elif day > today:
#                     status_code = "--"
#                 elif day in emp_records:
#                     rec = emp_records[day]
#                     if rec.status == "Present":
#                         status_code = "P"
#                         total_p += 1
 
#                         if is_late(rec.in_time, cfg):         # ✅ Setting.grace_minutes
#                             late_count += 1
 
#                         sec = ot_seconds(rec.in_time, rec.out_time, cfg)   # ✅ Setting.shift_end
#                         if sec > 0:
#                             total_ot_sec += sec
#                             ot_display = fmt_hhmm(sec // 60)
#                     else:
#                         status_code = "A"
#                         total_a += 1
#                 else:
#                     status_code = "A"
#                     total_a += 1
 
#                 attendance_map[day_str] = {
#                     "status": status_code,
#                     "ot": ot_display,
#                     "holiday_name": holiday_map.get(day),
#                 }
 
#             half_day_cuts = late_count // 3
#             late_deduction = round(half_day_cuts * 0.5, 2)
#             effective_present = round(max(0, total_p - late_deduction), 2)
 
#             total_ot_h = int(total_ot_sec // 3600)
#             total_ot_m = int((total_ot_sec % 3600) // 60)
 
#             final_report.append({
#                 "id": emp.id,
#                 "name": emp_display_name(emp),
#                 "code": emp_display_code(emp),
#                 "total_present": total_p,
#                 "effective_present": effective_present,
#                 "total_absent": total_a,
#                 "total_ot": f"{total_ot_h:02d}:{total_ot_m:02d}",
#                 "late_count": late_count,
#                 "half_day_cuts": half_day_cuts,
#                 "late_deduction": late_deduction,
#                 "attendance": attendance_map,
#             })
 
#         return Response({
#             "days": [
#                 {
#                     "date": d.strftime("%Y-%m-%d"),
#                     "day_num": d.day,
#                     "display": d.strftime("%d"),
#                     "day_name": d.strftime("%a"),
#                     "is_weekly_off": d.weekday() in cfg["weekly_off"],   # ✅
#                     "is_holiday": d in holiday_map,                      # ✅
#                     "holiday_name": holiday_map.get(d),
#                 }
#                 for d in dates_in_month
#             ],
#             "report": final_report,
#             "config": {
#                 "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
#                 "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
#                 "grace_minutes": cfg["grace_minutes"],
#                 "late_after": cfg["late_after"].strftime("%I:%M %p"),
#                 "weekly_off": sorted(cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}),
#                 "late_rule": "3 Late = 0.5 day deduction",
#             },
#             "holidays": [
#                 {"date": d.strftime("%Y-%m-%d"), "name": n}
#                 for d, n in sorted(holiday_map.items())
#             ],
#         }, status=status.HTTP_200_OK)
from datetime import time as dt_time
 
# ──────────────────────────────────────────────
#  🔒 STATIC RULES (maathanum na inga mattum maathunga)
# ──────────────────────────────────────────────
OT_START = dt_time(18, 45)   # 6:45 PM ku mela dhaan OT
HALF_DAY_MIN_HOURS = 6       # In -> Out 6 hours ku kammi na HALF DAY (HD = 0.5)
 
 
def _to_time(value):
    """time / datetime / "HH:MM[:SS]" string -> time object (mudiyala na None)"""
    if not value:
        return None
    if isinstance(value, str):
        try:
            parts = [int(float(x)) for x in value.strip().split(":")[:3]]
            while len(parts) < 3:
                parts.append(0)
            return dt_time(*parts)
        except (ValueError, TypeError):
            return None
    if hasattr(value, "time"):   # datetime aa irundha time-ah eduthukkum
        return value.time()
    return value
 
 
def _seconds(t):
    return t.hour * 3600 + t.minute * 60 + t.second
 
 
def ot_seconds_static(in_time, out_time, cfg=None):
    """OT = out_time - 6:45 PM. out_time illa na 0."""
    out_t = _to_time(out_time)
    if not out_t:
        return 0
    return max(0, _seconds(out_t) - _seconds(OT_START))
 
 
def worked_seconds(in_time, out_time):
    """Total login -> logout neram (seconds). Edhaavadhu illa na 0."""
    in_t, out_t = _to_time(in_time), _to_time(out_time)
    if not in_t or not out_t:
        return 0
    return max(0, _seconds(out_t) - _seconds(in_t))
 
 
def is_half_day(in_time, out_time):
    """In & Out rendum irundhu, total velai neram 6 hrs ku kammi na True."""
    if not _to_time(in_time) or not _to_time(out_time):
        return False
    return worked_seconds(in_time, out_time) < HALF_DAY_MIN_HOURS * 3600
 
 
# ══════════════════════════════════════════════
#  MONTHLY ATTENDANCE
#  (Class name "MonthattendanceAPIView" — spelling idhe
#   ungal urls.py already point pannirukku, adha maathaadhinga)
# ══════════════════════════════════════════════

# ══════════════════════════════════════════════
#  MONTHLY ATTENDANCE
#  (Class name "MonthattendanceAPIView" — spelling idhe
#   ungal urls.py already point pannirukku, adha maathaadhinga)
# ══════════════════════════════════════════════
class MonthattendanceAPIView(APIView):
    """
    Status per day: P / HD / A / H (Holiday table) / W (Setting.weekly_off) / '--' (future)
    HD    = In -> Out 6 hrs ku kammi (0.5 day)
    A     = Absent, illa check-in irundhu check-out illa (past date) -> no_checkout = True
    Late  = in_time > shift_start + grace_minutes (Setting table)
    3 Late = 0.5 day cut from Present (effective_present)
    OT    = out_time - 6:45 PM (static)
    Optional filter: ?employee_id=<id>  (illa na ellame varum)
    """
 
    def get(self, request, *args, **kwargs):
        val = request.GET.get("val")
        if not val:
            return Response(
                {"error": "val is required. Use ?val=YYYY-MM"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        try:
            first_day = datetime.strptime(val + "-01", "%Y-%m-%d").date()
        except ValueError:
            return Response(
                {"error": "Invalid val format. Use YYYY-MM"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        if first_day.month == 12:
            last_day = first_day.replace(year=first_day.year + 1, month=1, day=1) - timedelta(days=1)
        else:
            last_day = first_day.replace(month=first_day.month + 1, day=1) - timedelta(days=1)
 
        dates_in_month = [
            first_day + timedelta(days=i) for i in range((last_day - first_day).days + 1)
        ]
 
        cfg = get_attendance_config()
        holiday_map = get_holiday_map(first_day, last_day)  # ✅ Holiday table
 
        emp_param = request.GET.get("employee_id") or request.GET.get("student_id")
        employees = Employee.objects.filter(status=1)
        if emp_param and emp_param != "all":
            employees = employees.filter(id=emp_param)
 
        all_attendance = Attendance.objects.filter(date__range=[first_day, last_day])
        today = date_cls.today()
        final_report = []
 
        for emp in employees:
            emp_records = {r.date: r for r in all_attendance if r.employee_id == emp.id}
            total_p = total_hd = total_a = late_count = 0
            total_ot_sec = 0
            attendance_map = {}
 
            for day in dates_in_month:
                day_str = day.strftime("%Y-%m-%d")
                ot_display = "00:00"
                no_checkout = False
                wd = day.weekday()
 
                is_holiday = day in holiday_map              # ✅ Holiday table
                is_weekly_off = wd in cfg["weekly_off"]       # ✅ Setting.weekly_off
 
                if is_holiday:
                    status_code = "H"
                elif is_weekly_off:
                    status_code = "W"
                elif day > today:
                    status_code = "--"
                elif day in emp_records:
                    rec = emp_records[day]
                    if rec.status == "Present":
                        if not rec.out_time and day < today:
                            # ❗ Check-in pannitu check-out pannala -> Absent
                            status_code = "A"
                            total_a += 1
                            no_checkout = True
                        else:
                            if rec.out_time and is_half_day(rec.in_time, rec.out_time):
                                status_code = "HD"          # 🌗 6 hrs ku kammi
                                total_hd += 1
                            else:
                                status_code = "P"
                                total_p += 1
 
                            if is_late(rec.in_time, cfg):         # ✅ Setting.grace_minutes
                                late_count += 1
 
                            # 🔒 OT after 6:45 PM, out_time irukura naal mattum
                            sec = ot_seconds_static(rec.in_time, rec.out_time) if rec.out_time else 0
                            if sec > 0:
                                total_ot_sec += sec
                                ot_display = fmt_hhmm(sec // 60)
                    else:
                        status_code = "A"
                        total_a += 1
                else:
                    status_code = "A"
                    total_a += 1
 
                attendance_map[day_str] = {
                    "status": status_code,
                    "ot": ot_display,
                    "no_checkout": no_checkout,
                    "holiday_name": holiday_map.get(day),
                }
 
            half_day_cuts = late_count // 3
            late_deduction = round(half_day_cuts * 0.5, 2)
            # Full day = 1, Half day = 0.5, late cut kazhichu
            effective_present = round(max(0, total_p + total_hd * 0.5 - late_deduction), 2)
 
            total_ot_h = int(total_ot_sec // 3600)
            total_ot_m = int((total_ot_sec % 3600) // 60)
 
            final_report.append({
                "id": emp.id,
                "name": emp_display_name(emp),
                "code": emp_display_code(emp),
                "total_present": total_p + total_hd,   # vandha naatkal (P + HD)
                "half_days": total_hd,
                "effective_present": effective_present,
                "total_absent": total_a,
                "total_ot": f"{total_ot_h:02d}:{total_ot_m:02d}",
                "late_count": late_count,
                "half_day_cuts": half_day_cuts,
                "late_deduction": late_deduction,
                "attendance": attendance_map,
            })
 
        return Response({
            "days": [
                {
                    "date": d.strftime("%Y-%m-%d"),
                    "day_num": d.day,
                    "display": d.strftime("%d"),
                    "day_name": d.strftime("%a"),
                    "is_weekly_off": d.weekday() in cfg["weekly_off"],   # ✅
                    "is_holiday": d in holiday_map,                      # ✅
                    "holiday_name": holiday_map.get(d),
                }
                for d in dates_in_month
            ],
            "report": final_report,
            "config": {
                "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
                "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
                "grace_minutes": cfg["grace_minutes"],
                "late_after": cfg["late_after"].strftime("%I:%M %p"),
                "weekly_off": sorted(cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}),
                "late_rule": "3 Late = 0.5 day deduction",
                "half_day_hours": HALF_DAY_MIN_HOURS,
                "half_day_rule": f"Below {HALF_DAY_MIN_HOURS} hrs = Half day",
            },
            "holidays": [
                {"date": d.strftime("%Y-%m-%d"), "name": n}
                for d, n in sorted(holiday_map.items())
            ],
        }, status=status.HTTP_200_OK)
 
   
# ══════════════════════════════════════════════
#  TODAY ATTENDANCE
# ══════════════════════════════════════════════
# class TodayAttendanceAPIView(APIView):
#     """Settings + Holiday table wise, particular date-oda present/absent."""
 
#     def get(self, request):
#         date_str = request.query_params.get("date")
#         try:
#             target_date = (
#                 datetime.strptime(date_str, "%Y-%m-%d").date()
#                 if date_str else date_cls.today()
#             )
#         except ValueError:
#             return Response(
#                 {"error": "Invalid date. Use YYYY-MM-DD"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         cfg = get_attendance_config()
#         holiday_map = get_holiday_map(target_date, target_date)  # ✅ Holiday table
#         hol_name = holiday_map.get(target_date)
#         is_weekly_off = target_date.weekday() in cfg["weekly_off"]  # ✅ Setting table
#         day_type = "holiday" if hol_name else ("weekly_off" if is_weekly_off else "working")
 
#         emp_param = request.query_params.get("employee_id")
#         employees = Employee.objects.filter(status=1)
#         if emp_param and emp_param != "all":
#             employees = employees.filter(id=emp_param)
 
#         records = Attendance.objects.filter(
#             date=target_date, employee__in=employees
#         ).select_related("employee")
 
#         present_list = []
#         for rec in records:
#             late = is_late(rec.in_time, cfg)
#             sec = ot_seconds(rec.in_time, rec.out_time, cfg)
#             emp = rec.employee
 
#             present_list.append({
#                 "id": emp.id,
#                 "name": emp_display_name(emp),
#                 "code": emp_display_code(emp),
#                 "photo": emp.photo.url if getattr(emp, "photo", None) else None,
#                 "in_time": rec.in_time.strftime("%I:%M %p") if rec.in_time else "--",
#                 "out_time": rec.out_time.strftime("%I:%M %p") if rec.out_time else "--",
#                 "entry_status": "Late" if late else "On Time",
#                 "late": late,
#                 "location": rec.location,

#                 "ot_time": fmt_hhmm(sec // 60),
#                 "ot_minutes": sec // 60,
#             })
 
#         present_ids = [r.employee_id for r in records]
#         absent_list = [
#             {
#                 "id": e.id,
#                 "name": emp_display_name(e),
#                 "code": emp_display_code(e),
#                 "photo": e.photo.url if getattr(e, "photo", None) else None,
#             }
#             for e in employees.exclude(id__in=present_ids)
#         ]
 
#         return Response({
#             "date": target_date.isoformat(),
#             "day_type": day_type,
#             "is_holiday": bool(hol_name),
#             "holiday_name": hol_name,
#             "is_weekly_off": is_weekly_off,
#             "present_count": len(present_list),
#             "absent_count": len(absent_list),
#             "present_list": sorted(present_list, key=lambda x: x["name"]),
#             "absent_list": sorted(absent_list, key=lambda x: x["name"]),
#             "config": {
#                 "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
#                 "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
#                 "grace_minutes": cfg["grace_minutes"],
#                 "late_after": cfg["late_after"].strftime("%I:%M %p"),
#             },
#         }, status=status.HTTP_200_OK)
 
class TodayAttendanceAPIView(APIView):
    """
    Settings + Holiday table wise, particular date-oda present/absent.
 
    ✅ Past date la check-in irundhu check-out illa na -> ABSENT list la varum
       (no_checkout = True, in_time kaattum). Admin out time update / delete pannalaam.
    ✅ Innaiku (today) check-out illa na innum velai la irukkaanga -> PRESENT.
    ✅ In -> Out 6 hrs ku kammi na PRESENT list la "half_day": True
    """
 
    def get(self, request):
        date_str = request.query_params.get("date")
        try:
            target_date = (
                datetime.strptime(date_str, "%Y-%m-%d").date()
                if date_str else date_cls.today()
            )
        except ValueError:
            return Response(
                {"error": "Invalid date. Use YYYY-MM-DD"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        today = date_cls.today()
        cfg = get_attendance_config()
        holiday_map = get_holiday_map(target_date, target_date)  # ✅ Holiday table
        hol_name = holiday_map.get(target_date)
        is_weekly_off = target_date.weekday() in cfg["weekly_off"]  # ✅ Setting table
        day_type = "holiday" if hol_name else ("weekly_off" if is_weekly_off else "working")
 
        emp_param = request.query_params.get("employee_id")
        employees = Employee.objects.filter(status=1)
        if emp_param and emp_param != "all":
            employees = employees.filter(id=emp_param)
 
        records = Attendance.objects.filter(
            date=target_date, employee__in=employees
        ).select_related("employee")
 
        present_list = []
        no_checkout_list = []
 
        for rec in records:
            emp = rec.employee
            late = is_late(rec.in_time, cfg)
            has_out = bool(rec.out_time)
 
            item = {
                "attendance_id": rec.id,          # ✅ update / delete ku venum
                "id": emp.id,
                "name": emp_display_name(emp),
                "code": emp_display_code(emp),
                "photo": emp.photo.url if getattr(emp, "photo", None) else None,
                "in_time": rec.in_time.strftime("%I:%M %p") if rec.in_time else "--",
                "out_time": rec.out_time.strftime("%I:%M %p") if has_out else "--",
                "entry_status": "Late" if late else "On Time",
                "late": late,
                "location": rec.location,
                "no_checkout": False,
                "half_day": False,
                "worked_time": "--",
                "ot_time": "00:00",
                "ot_minutes": 0,
            }
 
            # ❗ Past date + check-out illa -> Absent list (In time kaattum)
            if not has_out and target_date < today:
                item["no_checkout"] = True
                no_checkout_list.append(item)
                continue
 
            if has_out:
                worked = worked_seconds(rec.in_time, rec.out_time)
                item["worked_time"] = fmt_hhmm(worked // 60)
                item["half_day"] = is_half_day(rec.in_time, rec.out_time)
 
            sec = ot_seconds_static(rec.in_time, rec.out_time) if has_out else 0
            item["ot_time"] = fmt_hhmm(sec // 60)
            item["ot_minutes"] = sec // 60
            present_list.append(item)
 
        # Check-in record-ae illaadhavanga = normal Absent
        recorded_ids = [r.employee_id for r in records]
        plain_absent = [
            {
                "attendance_id": None,
                "id": e.id,
                "name": emp_display_name(e),
                "code": emp_display_code(e),
                "photo": e.photo.url if getattr(e, "photo", None) else None,
                "no_checkout": False,
            }
            for e in employees.exclude(id__in=recorded_ids)
        ]
 
        # No-checkout aalunga mela varanum (admin gavanam ku)
        absent_list = sorted(
            no_checkout_list + plain_absent,
            key=lambda x: (not x["no_checkout"], x["name"]),
        )
 
        return Response({
            "date": target_date.isoformat(),
            "day_type": day_type,
            "is_holiday": bool(hol_name),
            "holiday_name": hol_name,
            "is_weekly_off": is_weekly_off,
            "present_count": len(present_list),
            "absent_count": len(absent_list),
            "no_checkout_count": len(no_checkout_list),
            "half_day_count": sum(1 for p in present_list if p["half_day"]),
            "present_list": sorted(present_list, key=lambda x: x["name"]),
            "absent_list": absent_list,
            "config": {
                "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
                "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
                "grace_minutes": cfg["grace_minutes"],
                "late_after": cfg["late_after"].strftime("%I:%M %p"),
                "half_day_hours": HALF_DAY_MIN_HOURS,
            },
        }, status=status.HTTP_200_OK)
  
 
# ══════════════════════════════════════════════
#  ADMIN: OUT TIME UPDATE
#  PATCH /attendance/update-out-time/<attendance_id>/
#  body: {"out_time": "18:30"}      (24-hour HH:MM)
# ══════════════════════════════════════════════

# ══════════════════════════════════════════════
#  ADMIN: OUT TIME UPDATE
#  PATCH /attendance/update-out-time/<attendance_id>/
#  body: {"out_time": "18:30"}      (24-hour HH:MM)
# ══════════════════════════════════════════════
class UpdateAttendanceOutTimeAPIView(APIView):
 
    def patch(self, request, attendance_id):
        out_str = str(request.data.get("out_time") or "").strip()
        if not out_str:
            return Response(
                {"error": "out_time is required. Use HH:MM (24-hour)"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        parsed = None
        for fmt in ("%H:%M", "%H:%M:%S"):
            try:
                parsed = datetime.strptime(out_str, fmt).time()
                break
            except ValueError:
                continue
        if parsed is None:
            return Response(
                {"error": "Invalid out_time. Use HH:MM (24-hour)"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        try:
            rec = Attendance.objects.select_related("employee").get(id=attendance_id)
        except Attendance.DoesNotExist:
            return Response(
                {"error": "Attendance record not found"},
                status=status.HTTP_404_NOT_FOUND,
            )
 
        # Out time, In time ku appuram dhaan irukkanum
        in_t = _to_time(rec.in_time)
        if in_t is not None and parsed <= in_t:
            return Response(
                {"error": f"Out time must be after in time ({in_t.strftime('%I:%M %p')})"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        rec.out_time = parsed
        rec.save()
 
        sec = ot_seconds_static(rec.in_time, rec.out_time)
        return Response({
            "message": "Out time updated",
            "attendance_id": rec.id,
            "employee_id": rec.employee_id,
            "date": rec.date.isoformat(),
            "out_time": rec.out_time.strftime("%I:%M %p"),
            "half_day": is_half_day(rec.in_time, rec.out_time),
            "ot_time": fmt_hhmm(sec // 60),
        }, status=status.HTTP_200_OK)
 
  
# ══════════════════════════════════════════════
#  ADMIN: ATTENDANCE RECORD DELETE
#  DELETE /attendance/delete/<attendance_id>/
#  (record poyiduchu -> andha naal Absent / no check-in)
# ══════════════════════════════════════════════
class DeleteAttendanceAPIView(APIView):
 
    def delete(self, request, attendance_id):
        try:
            rec = Attendance.objects.get(id=attendance_id)
        except Attendance.DoesNotExist:
            return Response(
                {"error": "Attendance record not found"},
                status=status.HTTP_404_NOT_FOUND,
            )
 
        rec.delete()
        return Response(
            {"message": "Attendance deleted"},
            status=status.HTTP_200_OK,
        )
 
 
# ══════════════════════════════════════════════
#  EMPLOYEE DASHBOARD
#  Setting table (shift/weekly-off) + Holiday table (upcoming holiday)
#  + idhe employee-oda indha maasa summary.
# ══════════════════════════════════════════════
class EmployeeDashboardAPIView(APIView):
    """
    GET /employee-dashboard/?employee_id=<id>
 
    employee_id kudukkalana, login panna user-oda user_data-la
    irundhu employee_id anuppanum (frontend already anuppum).
    """
 
    def get(self, request):
        emp_id = request.query_params.get("employee_id")
        if not emp_id:
            return Response(
                {"error": "employee_id is required"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        try:
            emp = Employee.objects.get(id=emp_id)
        except Employee.DoesNotExist:
            return Response(
                {"error": "Employee not found"},
                status=status.HTTP_404_NOT_FOUND,
            )
 
        cfg = get_attendance_config()
        today = date_cls.today()
 
        # ── Ithu maasathoda holidays + next upcoming holiday ──
        month_start = today.replace(day=1)
        if today.month == 12:
            month_end = today.replace(year=today.year + 1, month=1, day=1) - timedelta(days=1)
        else:
            month_end = today.replace(month=today.month + 1, day=1) - timedelta(days=1)
 
        month_holidays = get_holiday_map(month_start, month_end)  # ✅ Holiday table
        upcoming = Holiday.objects.filter(date__gte=today).order_by("date").first()
 
        # ── Today status (indha employee mattum) ──
        today_rec = Attendance.objects.filter(employee_id=emp.id, date=today).first()
        is_holiday_today = today in month_holidays
        is_weekly_off_today = today.weekday() in cfg["weekly_off"]  # ✅ Setting table
 
        if is_holiday_today:
            today_status = "Holiday"
        elif is_weekly_off_today:
            today_status = "Weekly Off"
        elif today_rec and today_rec.status == "Present":
            today_status = "Late" if is_late(today_rec.in_time, cfg) else "Present"
        else:
            today_status = "Not Checked In"
 
        # ── This month quick summary (present/absent/late) ──
        month_records = Attendance.objects.filter(
            employee_id=emp.id, date__range=[month_start, month_end]
        )
        rec_by_date = {r.date: r for r in month_records}
 
        total_p = total_a = late_count = 0
        d = month_start
        while d <= min(month_end, today):
            is_off = d.weekday() in cfg["weekly_off"]
            is_hol = d in month_holidays
            if not is_off and not is_hol:
                rec = rec_by_date.get(d)
                if rec and rec.status == "Present":
                    total_p += 1
                    if is_late(rec.in_time, cfg):
                        late_count += 1
                else:
                    total_a += 1
            d += timedelta(days=1)
 
        half_day_cuts = late_count // 3
        late_deduction = round(half_day_cuts * 0.5, 2)
        effective_present = round(max(0, total_p - late_deduction), 2)
 
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "employee": {
                    "id": emp.id,
                    "name": emp_display_name(emp),
                    "code": emp_display_code(emp),
                    "photo": emp.photo.url if getattr(emp, "photo", None) else None,
                },
                "today": {
                    "date": today.isoformat(),
                    "status": today_status,
                    "in_time": today_rec.in_time.strftime("%I:%M %p") if today_rec and today_rec.in_time else "--",
                    "out_time": today_rec.out_time.strftime("%I:%M %p") if today_rec and today_rec.out_time else "--",
                },
                # ✅ Setting table -la irundhu — office shift timing
                "shift": {
                    "start": cfg["shift_start"].strftime("%I:%M %p"),
                    "end": cfg["shift_end"].strftime("%I:%M %p"),
                    "grace_minutes": cfg["grace_minutes"],
                    "late_after": cfg["late_after"].strftime("%I:%M %p"),
                    "weekly_off": sorted(DAY_KEYS[i] for i in cfg["weekly_off"]),
                    # ✅ Fully dynamic — DB-la evlo weekly-off days irundhalum
                    # (Sat, Sun, etc) automatic-a inga varum. .get() use pannuvom
                    # so oru duplicate old get_attendance_config() irundhalum crash aagaadhu.
                    "weekly_off_days": sorted(
                        cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}
                    ),
                },
                # ✅ Holiday table -la irundhu — indha maasam + next upcoming
                "holidays": {
                    "this_month": [
                        {"date": d2.isoformat(), "name": n}
                        for d2, n in sorted(month_holidays.items())
                    ],
                    "next": (
                        {"date": upcoming.date.isoformat(), "name": upcoming.name}
                        if upcoming else None
                    ),
                },
                "month_summary": {
                    "month_label": today.strftime("%B %Y"),
                    "total_present": total_p,
                    "effective_present": effective_present,
                    "total_absent": total_a,
                    "late_count": late_count,
                    "half_day_cuts": half_day_cuts,
                    "late_deduction": late_deduction,
                },
            },
        }, status=status.HTTP_200_OK)
 
 
# ══════════════════════════════════════════════
#  EMPLOYEE ATTENDANCE — single employee, single day
#  (used by EmployeeTodayAttendance.jsx)
# ══════════════════════════════════════════════
# ot_seconds_static helper (6:45 PM) - munnadi kuduththa file la irukkum,
# same views.py la irukkanum.

# ══════════════════════════════════════════════
#  EMPLOYEE: INDIVIDUAL DAY
# ══════════════════════════════════════════════
class EmployeeAttendanceAPIView(APIView):
    """
    GET /employee-attendance/?employee_id=<id>&date=YYYY-MM-DD
 
    date illama kudutha, today edukkum.
    Status: Present / Late / Half Day / H / W / Absent / Not Checked In / --
    ✅ Past date + check-in irundhu check-out illa -> Absent (no_checkout = True)
    ✅ In -> Out 6 hrs ku kammi -> Half Day
    OT = out_time - 6:45 PM (static)
    """
 
    def get(self, request):
        emp_id = request.query_params.get("employee_id")
        if not emp_id:
            return Response(
                {"error": "employee_id is required"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        try:
            emp = Employee.objects.get(id=emp_id)
        except Employee.DoesNotExist:
            return Response(
                {"error": "Employee not found"},
                status=status.HTTP_404_NOT_FOUND,
            )
 
        date_str = request.query_params.get("date")
        try:
            target_date = (
                datetime.strptime(date_str, "%Y-%m-%d").date()
                if date_str else date_cls.today()
            )
        except ValueError:
            return Response(
                {"error": "Invalid date. Use YYYY-MM-DD"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        today = date_cls.today()
        cfg = get_attendance_config()
        holiday_map = get_holiday_map(target_date, target_date)  # ✅ Holiday table
        hol_name = holiday_map.get(target_date)
        is_weekly_off = target_date.weekday() in cfg["weekly_off"]  # ✅ Setting table
 
        rec = Attendance.objects.filter(employee_id=emp.id, date=target_date).first()
 
        late = False
        sec = 0
        no_checkout = False
        half_day = False
        worked_time = "--"
 
        if hol_name:
            record_status = "H"
        elif is_weekly_off:
            record_status = "W"
        elif rec and rec.status == "Present":
            if not rec.out_time and target_date < today:
                # ❗ Check-in pannitu check-out pannala -> Absent
                record_status = "Absent"
                no_checkout = True
            else:
                late = is_late(rec.in_time, cfg)
                if rec.out_time:
                    sec = ot_seconds_static(rec.in_time, rec.out_time)  # 🔒 6:45 PM
                    worked_time = fmt_hhmm(worked_seconds(rec.in_time, rec.out_time) // 60)
                    half_day = is_half_day(rec.in_time, rec.out_time)
                if half_day:
                    record_status = "Half Day"
                else:
                    record_status = "Late" if late else "Present"
        elif rec:
            record_status = "Absent"
        elif target_date > today:
            record_status = "--"
        elif target_date < today:
            record_status = "Absent"
        else:
            record_status = "Not Checked In"
 
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "employee": {
                    "id": emp.id,
                    "name": emp_display_name(emp),
                    "code": emp_display_code(emp),
                },
                "date": target_date.isoformat(),
                "record": {
                    "status": record_status,
                    "late": late,
                    "half_day": half_day,
                    "no_checkout": no_checkout,
                    "in_time": rec.in_time.strftime("%I:%M %p") if rec and rec.in_time else "--",
                    "out_time": rec.out_time.strftime("%I:%M %p") if rec and rec.out_time else "--",
                    "worked_time": worked_time,
                    "ot_time": fmt_hhmm(sec // 60),
                    "holiday_name": hol_name,
                },
                "is_holiday": bool(hol_name),
                "is_weekly_off": is_weekly_off,
                "config": {
                    "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
                    "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
                    "grace_minutes": cfg["grace_minutes"],
                    "late_after": cfg["late_after"].strftime("%I:%M %p"),
                    "half_day_hours": HALF_DAY_MIN_HOURS,
                },
            },
        }, status=status.HTTP_200_OK)
 
# ══════════════════════════════════════════════
#  EMPLOYEE MONTHLY ATTENDANCE — single employee, full month
#  (used by EmployeeMonthlyAttendance.jsx)
# ══════════════════════════════════════════════
# class EmployeeMonthlyAttendanceAPIView(APIView):
#     """
#     GET /employee-monthly-attendance/?employee_id=<id>&val=YYYY-MM
 
#     Same day-status rules as MonthattendanceAPIView, but scoped to
#     ONE employee and returns a single "row" object instead of a list.
#     """
 
#     def get(self, request):
#         emp_id = request.query_params.get("employee_id")
#         val = request.GET.get("val")
 
#         if not emp_id:
#             return Response(
#                 {"error": "employee_id is required"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
#         if not val:
#             return Response(
#                 {"error": "val is required. Use ?val=YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         try:
#             emp = Employee.objects.get(id=emp_id)
#         except Employee.DoesNotExist:
#             return Response(
#                 {"error": "Employee not found"},
#                 status=status.HTTP_404_NOT_FOUND,
#             )
 
#         try:
#             first_day = datetime.strptime(val + "-01", "%Y-%m-%d").date()
#         except ValueError:
#             return Response(
#                 {"error": "Invalid val format. Use YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         if first_day.month == 12:
#             last_day = first_day.replace(year=first_day.year + 1, month=1, day=1) - timedelta(days=1)
#         else:
#             last_day = first_day.replace(month=first_day.month + 1, day=1) - timedelta(days=1)
 
#         dates_in_month = [
#             first_day + timedelta(days=i) for i in range((last_day - first_day).days + 1)
#         ]
 
#         cfg = get_attendance_config()
#         holiday_map = get_holiday_map(first_day, last_day)  # ✅ Holiday table
 
#         emp_records = {
#             r.date: r
#             for r in Attendance.objects.filter(employee_id=emp.id, date__range=[first_day, last_day])
#         }
 
#         today = date_cls.today()
#         total_p = total_a = late_count = 0
#         total_ot_sec = 0
#         attendance_map = {}
 
#         for day in dates_in_month:
#             day_str = day.strftime("%Y-%m-%d")
#             ot_display = "00:00"
#             wd = day.weekday()
 
#             is_holiday = day in holiday_map            # ✅ Holiday table
#             is_weekly_off = wd in cfg["weekly_off"]     # ✅ Setting.weekly_off
 
#             if is_holiday:
#                 status_code = "H"
#             elif is_weekly_off:
#                 status_code = "W"
#             elif day > today:
#                 status_code = "--"
#             elif day in emp_records:
#                 rec = emp_records[day]
#                 if rec.status == "Present":
#                     status_code = "P"
#                     total_p += 1
#                     if is_late(rec.in_time, cfg):
#                         late_count += 1
#                     sec = ot_seconds(rec.in_time, rec.out_time, cfg)
#                     if sec > 0:
#                         total_ot_sec += sec
#                         ot_display = fmt_hhmm(sec // 60)
#                 else:
#                     status_code = "A"
#                     total_a += 1
#             else:
#                 status_code = "A"
#                 total_a += 1
 
#             attendance_map[day_str] = {
#                 "status": status_code,
#                 "ot": ot_display,
#                 "holiday_name": holiday_map.get(day),
#             }
 
#         half_day_cuts = late_count // 3
#         late_deduction = round(half_day_cuts * 0.5, 2)
#         effective_present = round(max(0, total_p - late_deduction), 2)
 
#         total_ot_h = int(total_ot_sec // 3600)
#         total_ot_m = int((total_ot_sec % 3600) // 60)
 
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "data": {
#                 "employee": {
#                     "id": emp.id,
#                     "name": emp_display_name(emp),
#                     "code": emp_display_code(emp),
#                 },
#                 "days": [
#                     {
#                         "date": d.strftime("%Y-%m-%d"),
#                         "day_num": d.day,
#                         "display": d.strftime("%d"),
#                         "day_name": d.strftime("%a"),
#                         "is_weekly_off": d.weekday() in cfg["weekly_off"],   # ✅
#                         "is_holiday": d in holiday_map,                      # ✅
#                         "holiday_name": holiday_map.get(d),
#                     }
#                     for d in dates_in_month
#                 ],
#                 "row": {
#                     "total_present": total_p,
#                     "effective_present": effective_present,
#                     "total_absent": total_a,
#                     "total_ot": f"{total_ot_h:02d}:{total_ot_m:02d}",
#                     "late_count": late_count,
#                     "half_day_cuts": half_day_cuts,
#                     "late_deduction": late_deduction,
#                     "attendance": attendance_map,
#                 },
#                 "config": {
#                     "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
#                     "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
#                     "grace_minutes": cfg["grace_minutes"],
#                     "late_after": cfg["late_after"].strftime("%I:%M %p"),
#                     "weekly_off": sorted(
#                         cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}
#                     ),
#                 },
#                 "holidays_this_month": [
#                     {"date": d.strftime("%Y-%m-%d"), "name": n}
#                     for d, n in sorted(holiday_map.items())
#                 ],
#             },
#         }, status=status.HTTP_200_OK)

 
class EmployeeMonthlyAttendanceAPIView(APIView):
    """
    GET /employee-monthly-attendance/?employee_id=<id>&val=YYYY-MM
 
    Same day-status rules as MonthattendanceAPIView, but scoped to
    ONE employee and returns a single "row" object instead of a list.
    """
 
    def get(self, request):
        emp_id = request.query_params.get("employee_id")
        val = request.GET.get("val")
 
        if not emp_id:
            return Response(
                {"error": "employee_id is required"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not val:
            return Response(
                {"error": "val is required. Use ?val=YYYY-MM"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        try:
            emp = Employee.objects.get(id=emp_id)
        except Employee.DoesNotExist:
            return Response(
                {"error": "Employee not found"},
                status=status.HTTP_404_NOT_FOUND,
            )
 
        try:
            first_day = datetime.strptime(val + "-01", "%Y-%m-%d").date()
        except ValueError:
            return Response(
                {"error": "Invalid val format. Use YYYY-MM"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        if first_day.month == 12:
            last_day = first_day.replace(year=first_day.year + 1, month=1, day=1) - timedelta(days=1)
        else:
            last_day = first_day.replace(month=first_day.month + 1, day=1) - timedelta(days=1)
 
        dates_in_month = [
            first_day + timedelta(days=i) for i in range((last_day - first_day).days + 1)
        ]
 
        cfg = get_attendance_config()
        holiday_map = get_holiday_map(first_day, last_day)  # ✅ Holiday table
 
        emp_records = {
            r.date: r
            for r in Attendance.objects.filter(employee_id=emp.id, date__range=[first_day, last_day])
        }
 
        today = date_cls.today()
        total_p = total_hd = total_a = late_count = 0
        total_ot_sec = 0
        attendance_map = {}
 
        for day in dates_in_month:
            day_str = day.strftime("%Y-%m-%d")
            ot_display = "00:00"
            no_checkout = False
            wd = day.weekday()
 
            is_holiday = day in holiday_map            # ✅ Holiday table
            is_weekly_off = wd in cfg["weekly_off"]     # ✅ Setting.weekly_off
 
            if is_holiday:
                status_code = "H"
            elif is_weekly_off:
                status_code = "W"
            elif day > today:
                status_code = "--"
            elif day in emp_records:
                rec = emp_records[day]
                if rec.status == "Present":
                    if not rec.out_time and day < today:
                        # ❗ Check-in pannitu check-out pannala -> Absent
                        status_code = "A"
                        total_a += 1
                        no_checkout = True
                    else:
                        if rec.out_time and is_half_day(rec.in_time, rec.out_time):
                            status_code = "HD"          # 🌗 6 hrs ku kammi
                            total_hd += 1
                        else:
                            status_code = "P"
                            total_p += 1
 
                        if is_late(rec.in_time, cfg):
                            late_count += 1
 
                        # 🔒 OT after 6:45 PM, out_time irukura naal mattum
                        sec = ot_seconds_static(rec.in_time, rec.out_time) if rec.out_time else 0
                        if sec > 0:
                            total_ot_sec += sec
                            ot_display = fmt_hhmm(sec // 60)
                else:
                    status_code = "A"
                    total_a += 1
            else:
                status_code = "A"
                total_a += 1
 
            attendance_map[day_str] = {
                "status": status_code,
                "ot": ot_display,
                "no_checkout": no_checkout,
                "holiday_name": holiday_map.get(day),
            }
 
        half_day_cuts = late_count // 3
        late_deduction = round(half_day_cuts * 0.5, 2)
        effective_present = round(max(0, total_p + total_hd * 0.5 - late_deduction), 2)
 
        total_ot_h = int(total_ot_sec // 3600)
        total_ot_m = int((total_ot_sec % 3600) // 60)
 
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "employee": {
                    "id": emp.id,
                    "name": emp_display_name(emp),
                    "code": emp_display_code(emp),
                },
                "days": [
                    {
                        "date": d.strftime("%Y-%m-%d"),
                        "day_num": d.day,
                        "display": d.strftime("%d"),
                        "day_name": d.strftime("%a"),
                        "is_weekly_off": d.weekday() in cfg["weekly_off"],   # ✅
                        "is_holiday": d in holiday_map,                      # ✅
                        "holiday_name": holiday_map.get(d),
                    }
                    for d in dates_in_month
                ],
                "row": {
                    "total_present": total_p + total_hd,
                    "half_days": total_hd,
                    "effective_present": effective_present,
                    "total_absent": total_a,
                    "total_ot": f"{total_ot_h:02d}:{total_ot_m:02d}",
                    "late_count": late_count,
                    "half_day_cuts": half_day_cuts,
                    "late_deduction": late_deduction,
                    "attendance": attendance_map,
                },
                "config": {
                    "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
                    "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
                    "grace_minutes": cfg["grace_minutes"],
                    "late_after": cfg["late_after"].strftime("%I:%M %p"),
                    "weekly_off": sorted(
                        cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}
                    ),
                    "half_day_hours": HALF_DAY_MIN_HOURS,
                },
                "holidays_this_month": [
                    {"date": d.strftime("%Y-%m-%d"), "name": n}
                    for d, n in sorted(holiday_map.items())
                ],
            },
        }, status=status.HTTP_200_OK) 
 
# ══════════════════════════════════════════════
#  LEAVE MANAGEMENT
# ══════════════════════════════════════════════
from .models import LeaveRequests  # noqa: E402
 
 
def leave_json(lv):
    return {
        "id": lv.id,
        "employee_id": lv.employee_id,
        "employee_name": emp_display_name(lv.employee),
        "employee_code": emp_display_code(lv.employee),
        "leave_type": lv.leave_type,
        "start_date": lv.start_date.isoformat(),
        "end_date": lv.end_date.isoformat(),
        "days_count": lv.days_count,
        "description": lv.description,
        "status": lv.status,
        "admin_note": lv.admin_note,
        "applied_at": lv.applied_at.strftime("%Y-%m-%d %I:%M %p") if lv.applied_at else None,
        "reviewed_at": lv.reviewed_at.strftime("%Y-%m-%d %I:%M %p") if lv.reviewed_at else None,
    }
 
 
# ── EMPLOYEE SIDE: list own leaves + apply new leave ──
class EmployeeLeaveAPIView(APIView):
    """
    GET  /employee-leave/?employee_id=<id>              -> list own leave requests
    POST /employee-leave/  {employee_id, leave_type, start_date, end_date, description}
    """
 
    def get(self, request):
        emp_id = request.query_params.get("employee_id")
        if not emp_id:
            return Response({"error": "employee_id is required"}, status=status.HTTP_400_BAD_REQUEST)
 
        leaves = LeaveRequests.objects.filter(employee_id=emp_id).select_related("employee")
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": [leave_json(lv) for lv in leaves],
        })
 
    def post(self, request):
        data = request.data
        emp_id = data.get("employee_id")
        if not emp_id:
            return Response({"error": "employee_id is required"}, status=status.HTTP_400_BAD_REQUEST)
 
        try:
            emp = Employee.objects.get(id=emp_id)
        except Employee.DoesNotExist:
            return Response({"error": "Employee not found"}, status=status.HTTP_404_NOT_FOUND)
 
        leave_type = data.get("leave_type", "Casual")
        start_str = data.get("start_date")
        end_str = data.get("end_date")
        description = (data.get("description") or "").strip()
 
        try:
            start_date = datetime.strptime(start_str, "%Y-%m-%d").date()
            end_date = datetime.strptime(end_str, "%Y-%m-%d").date()
        except (TypeError, ValueError):
            return Response(
                {"error": "start_date/end_date required in YYYY-MM-DD format"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        if end_date < start_date:
            return Response({"error": "End date cannot be before start date"}, status=status.HTTP_400_BAD_REQUEST)
 
        lv = LeaveRequests.objects.create(
            employee=emp,
            leave_type=leave_type,
            start_date=start_date,
            end_date=end_date,
            description=description,
            status="Pending",
        )
        return Response({
            "status": {"code": status.HTTP_201_CREATED, "response": "success"},
            "error": None,
            "message": "Leave request submitted",
            "data": leave_json(lv),
        }, status=status.HTTP_201_CREATED)
 
 
# ── EMPLOYEE SIDE: view / edit / cancel ONE leave (only while Pending) ──
class EmployeeLeaveDetailAPIView(APIView):
    """
    GET    /employee-leave/<id>/
    PUT    /employee-leave/<id>/   -- edit, only allowed while status="Pending"
    DELETE /employee-leave/<id>/   -- cancel, only allowed while status="Pending"
    """
 
    def get_object(self, id):
        try:
            return LeaveRequests.objects.select_related("employee").get(id=id)
        except LeaveRequests.DoesNotExist:
            return None
 
    def get(self, request, id):
        lv = self.get_object(id)
        if not lv:
            return Response({"error": "Leave request not found"}, status=status.HTTP_404_NOT_FOUND)
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": leave_json(lv),
        })
 
    def put(self, request, id):
        lv = self.get_object(id)
        if not lv:
            return Response({"error": "Leave request not found"}, status=status.HTTP_404_NOT_FOUND)
        if lv.status != "Pending":
            return Response(
                {"error": f"Cannot edit — this leave is already {lv.status}."},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        data = request.data
        try:
            if "start_date" in data:
                lv.start_date = datetime.strptime(data["start_date"], "%Y-%m-%d").date()
            if "end_date" in data:
                lv.end_date = datetime.strptime(data["end_date"], "%Y-%m-%d").date()
        except ValueError:
            return Response({"error": "Dates must be YYYY-MM-DD"}, status=status.HTTP_400_BAD_REQUEST)
 
        if lv.end_date < lv.start_date:
            return Response({"error": "End date cannot be before start date"}, status=status.HTTP_400_BAD_REQUEST)
 
        if "leave_type" in data:
            lv.leave_type = data["leave_type"]
        if "description" in data:
            lv.description = (data.get("description") or "").strip()
 
        lv.save()
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": "Leave request updated",
            "data": leave_json(lv),
        })
 
    def delete(self, request, id):
        lv = self.get_object(id)
        if not lv:
            return Response({"error": "Leave request not found"}, status=status.HTTP_404_NOT_FOUND)
        if lv.status != "Pending":
            return Response(
                {"error": f"Cannot cancel — this leave is already {lv.status}."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        lv.delete()
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": "Leave request cancelled",
        })
 
 
# ── ADMIN SIDE: list all leaves, filter by employee/status/month ──
class AdminLeaveListAPIView(APIView):
    """
    GET /admin-leave/?employee_id=&status=&val=YYYY-MM
 
    employee_id="all" or omitted -> every employee
    status omitted -> every status
    val omitted -> every month
    """
 
    def get(self, request):
        qs = LeaveRequests.objects.select_related("employee").all()
 
        emp_id = request.query_params.get("employee_id")
        if emp_id and emp_id != "all":
            qs = qs.filter(employee_id=emp_id)
 
        status_filter = request.query_params.get("status")
        if status_filter and status_filter != "all":
            qs = qs.filter(status=status_filter)
 
        val = request.query_params.get("val")
        if val:
            try:
                year, month = map(int, val.split("-"))
                qs = qs.filter(start_date__year=year, start_date__month=month)
            except ValueError:
                pass
 
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": [leave_json(lv) for lv in qs],
            "summary": {
                "pending": qs.filter(status="Pending").count(),
                "approved": qs.filter(status="Approved").count(),
                "rejected": qs.filter(status="Rejected").count(),
            },
        })
 
 
# ── ADMIN SIDE: approve / reject a leave ──
class AdminLeaveActionAPIView(APIView):
    """
    PUT /admin-leave/<id>/action/   {status: "Approved" | "Rejected", admin_note?}
    """
 
    def put(self, request, id):
        try:
            lv = LeaveRequests.objects.select_related("employee").get(id=id)
        except LeaveRequests.DoesNotExist:
            return Response({"error": "Leave request not found"}, status=status.HTTP_404_NOT_FOUND)
 
        new_status = request.data.get("status")
        if new_status not in ("Approved", "Rejected", "Pending"):
            return Response(
                {"error": "status must be Approved, Rejected, or Pending"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
        lv.status = new_status
        lv.admin_note = (request.data.get("admin_note") or "").strip() or None
        lv.reviewed_by = request.user if request.user.is_authenticated else None
        lv.reviewed_at = datetime.now()
        lv.save()
 
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": f"Leave marked as {new_status}",
            "data": leave_json(lv),
        })
 
 
# ── ADMIN SIDE: today + upcoming leaves, employee-filterable ──
class TodayUpcomingLeaveAPIView(APIView):
    """
    GET /leave-today-upcoming/?employee_id=
 
    Used by the admin "View Employee" screen — shows who's on
    Approved leave today, and Approved leaves starting in the
    next 30 days.
    """
 
    def get(self, request):
        emp_id = request.query_params.get("employee_id")
        today = date_cls.today()
 
        qs = LeaveRequests.objects.filter(status="Approved").select_related("employee")
        if emp_id and emp_id != "all":
            qs = qs.filter(employee_id=emp_id)
 
        today_leaves = qs.filter(start_date__lte=today, end_date__gte=today)
        upcoming_leaves = qs.filter(
            start_date__gt=today, start_date__lte=today + timedelta(days=30)
        ).order_by("start_date")
 
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "today": [leave_json(lv) for lv in today_leaves],
                "upcoming": [leave_json(lv) for lv in upcoming_leaves],
            },
        })
"""
══════════════════════════════════════════════
 FEES MANAGEMENT — views.py addition (v2)
 Manju Associates

 URLs (urls.py-la add pannanum):

   path('fee-clients/', FeeClientListAPIView.as_view()),
   path('fee-clients/<int:id>/', FeeClientDetailAPIView.as_view()),
   path('fee/<int:id>/', FeeDetailAPIView.as_view()),
   path('admin-fees/', AdminFeesListAPIView.as_view()),
   path('admin-fees-summary/', AdminFeesSummaryAPIView.as_view()),
   path('employee-fees/', EmployeeFeesAPIView.as_view()),
   path('employee-collection-report/', EmployeeCollectionReportAPIView.as_view()),
   path('fee-payment/', FeePaymentCreateAPIView.as_view()),
   path('fee-payment/<int:id>/', FeePaymentDetailAPIView.as_view()),   # GET / PUT (edit) / DELETE
   path('client-history/', ClientHistoryAPIView.as_view()),
══════════════════════════════════════════════
"""
import re
from datetime import datetime, date
from decimal import Decimal, InvalidOperation

from django.db import transaction
from django.db.models import Sum, Count, Q
from django.utils import timezone
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response

from .models import Employee, FeeClient, Fee, FeePayment  # ✅ path unga project-ku set pannunga


# ══════════════════════════════════════════════
#  EDIT PERMISSION — "andha employee-ku mattum" edit panra rule
#
#  EMPLOYEE_EDIT_SAME_DAY_ONLY = True  -> employee "innaiku" (same day) entry
#      pannadhai mattum edit pannalam (money record-ku idhu safe).
#  EMPLOYEE_EDIT_SAME_DAY_ONLY = False -> avanga entry pannina ella records-um
#      edit pannalam (date limit illa).
# ══════════════════════════════════════════════
EMPLOYEE_EDIT_SAME_DAY_ONLY = True


def _is_today(dt):
    now = timezone.now()
    if timezone.is_aware(dt):
        return timezone.localtime(dt).date() == timezone.localtime(now).date()
    return dt.date() == now.date()


def _can_edit_payment(payment, employee_id):
    """Payment-ah collect pannina employee mattum (same day-na) edit pannalam."""
    if not employee_id or str(payment.collected_by_id) != str(employee_id):
        return False
    return not (EMPLOYEE_EDIT_SAME_DAY_ONLY and not _is_today(payment.created_at))


def _can_edit_fee(fee, employee_id):
    """Fee record-ah create pannina employee mattum (same day-na) service /
    due date / fee amount ellam maathalam."""
    if not employee_id or str(fee.created_by_id) != str(employee_id):
        return False
    return not (EMPLOYEE_EDIT_SAME_DAY_ONLY and not _is_today(fee.created_at))


# ══════════════════════════════════════════════
#  JSON helpers
# ══════════════════════════════════════════════
def client_json(c):
    return {
        "id": c.id,
        "client_code": c.client_code,
        "name": c.name,
        "mobile": c.mobile,
        "gst_number": c.gst_number,
        "status": c.status,
    }


def payment_json(p):
    return {
        "id": p.id,
        "fee_id": p.fee_id,  # ✅ "next payment" ku andha payment-oda parent Fee-ku direct link
        "amount": str(p.amount),
        "payment_date": p.payment_date.strftime("%Y-%m-%d"),
        "payment_mode": p.payment_mode,
        "reference_no": p.reference_no,
        "note": p.note,
        "collected_by": {
            "id": p.collected_by.id if p.collected_by else None,
            "name": p.collected_by.name if p.collected_by else "—",
        },
    }


def fee_json(f, with_payments=False):
    data = {
        "id": f.id,
        "client": client_json(f.client),
        "service": f.service if f.service != "Other" else (f.service_other or "Other"),
        "fee_amount": str(f.fee_amount),
        "billing_frequency": f.billing_frequency,
        "due_date": f.due_date.strftime("%Y-%m-%d"),
        "next_due_date": f.next_due_date.strftime("%Y-%m-%d") if f.next_due_date else None,
        "paid_amount": str(f.paid_amount),
        "balance": str(f.balance),
        "payment_status": f.payment_status,
    }
    if with_payments:
        data["payments"] = [payment_json(p) for p in f.payments.all()]
    return data


# ══════════════════════════════════════════════
#  CLIENTS — search-as-you-type dropdown data +
#  "add new client" (used from the Employee add-fees form)
# ══════════════════════════════════════════════
class FeeClientListAPIView(APIView):
    def get(self, request):
        # ✅ optional ?search= — same endpoint client-side search-ku use pannalam
        search = request.query_params.get("search", "").strip()
        clients = FeeClient.objects.filter(status=1)
        if search:
            # ✅ name / client ID (MAS12) / mobile — mooonu vazhiyilum search pannalam
            clients = clients.filter(
                Q(name__icontains=search)
                | Q(client_code__icontains=search)
                | Q(mobile__icontains=search)
            )
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": [client_json(c) for c in clients],
        })

    def post(self, request):
        data = request.data
        name = (data.get("name") or "").strip()
        mobile = (data.get("mobile") or "").strip()

        # ── validation (beginner-friendly, clear error messages) ──
        if not name:
            return Response({"error": "Client name is required"}, status=status.HTTP_400_BAD_REQUEST)
        if len(name) < 2:
            return Response({"error": "Client name is too short"}, status=status.HTTP_400_BAD_REQUEST)
        if mobile and not re.fullmatch(r"\d{10}", mobile):
            return Response(
                {"error": "Mobile number must be exactly 10 digits"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # ── duplicate check — SAME NAME already irundha (case-insensitive), pudhusa
        #    create pannாmalே, already irukura client-ah return pannidalam. Mobile
        #    check-ஐum keழே vaikkirom (rendு vazhi-layேும் duplicate katch pannalam).
        existing_by_name = FeeClient.objects.filter(name__iexact=name, status=1).first()
        if existing_by_name:
            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": f'A client named "{existing_by_name.name}" already exists.',
                "already_exists": True,
                "data": client_json(existing_by_name),
            })

        if mobile:
            existing = FeeClient.objects.filter(mobile=mobile, status=1).first()
            if existing:
                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "message": f'A client with this mobile number already exists ("{existing.name}").',
                    "already_exists": True,
                    "data": client_json(existing),
                })

        c = FeeClient.objects.create(
            name=name,
            mobile=mobile or None,
            address=data.get("address"),
            gst_number=data.get("gst_number"),
        )
        return Response({
            "status": {"code": status.HTTP_201_CREATED, "response": "success"},
            "error": None,
            "message": "Client added",
            "already_exists": False,
            "data": client_json(c),
        }, status=status.HTTP_201_CREATED)


# ══════════════════════════════════════════════
#  ONE CLIENT — pending balance + last payment, used by the "Add Fee &
#  Payment" screen the moment a client is selected (even for a brand NEW
#  fee) — so the employee sees upfront if this client already owes money
#  from an earlier fee cycle.
#  GET /fee-clients/<id>/
# ══════════════════════════════════════════════
class FeeClientDetailAPIView(APIView):
    def get(self, request, id):
        try:
            client = FeeClient.objects.get(id=id)
        except FeeClient.DoesNotExist:
            return Response({"error": "Client not found"}, status=status.HTTP_404_NOT_FOUND)

        pending_balance = Fee.objects.filter(client_id=id).aggregate(t=Sum("balance"))["t"] or 0
        last_payment = (
            FeePayment.objects.filter(fee__client_id=id)
            .order_by("-payment_date", "-created_at")
            .first()
        )

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                **client_json(client),
                "pending_balance": str(pending_balance),
                "last_payment_date": last_payment.payment_date.strftime("%d-%m-%Y") if last_payment else None,
                "last_payment_amount": str(last_payment.amount) if last_payment else None,
            },
        })


# ══════════════════════════════════════════════
#  ONE FEE — used by the "Add Fee & Payment" screen to show
#  "Total ₹6000 · Collected ₹2000 · Pending ₹4000" for an
#  existing fee before recording a new payment against it.
# ══════════════════════════════════════════════
class FeeDetailAPIView(APIView):
    def get(self, request, id):
        try:
            fee = Fee.objects.select_related("client").prefetch_related("payments").get(id=id)
        except Fee.DoesNotExist:
            return Response({"error": "Fee record not found"}, status=status.HTTP_404_NOT_FOUND)
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": fee_json(fee, with_payments=True),
        })


# ══════════════════════════════════════════════
#  ADMIN — full fee list, filterable by date range + employee + status
#  (unchanged from before — used if you still want the raw fee list)
# ══════════════════════════════════════════════
class AdminFeesListAPIView(APIView):
    def get(self, request):
        qs = Fee.objects.select_related("client").prefetch_related("payments")

        from_str = request.query_params.get("from")
        to_str = request.query_params.get("to")
        employee_id = request.query_params.get("employee_id")
        status_filter = request.query_params.get("status")
        service_filter = request.query_params.get("service")

        if from_str:
            try:
                qs = qs.filter(due_date__gte=datetime.strptime(from_str, "%Y-%m-%d").date())
            except ValueError:
                pass
        if to_str:
            try:
                qs = qs.filter(due_date__lte=datetime.strptime(to_str, "%Y-%m-%d").date())
            except ValueError:
                pass
        if status_filter and status_filter != "all":
            qs = qs.filter(payment_status=status_filter)
        if service_filter and service_filter != "all":
            qs = qs.filter(service=service_filter)
        if employee_id and employee_id != "all":
            qs = qs.filter(payments__collected_by_id=employee_id).distinct()

        qs = qs.order_by("-due_date")

        total_billed = qs.aggregate(t=Sum("fee_amount"))["t"] or 0
        total_collected = qs.aggregate(t=Sum("paid_amount"))["t"] or 0

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": [fee_json(f) for f in qs],
            "summary": {
                "total_billed": str(total_billed),
                "total_collected": str(total_collected),
                "total_pending": str(total_billed - total_collected),
                "pending_count": qs.filter(payment_status="Pending").count(),
                "partial_count": qs.filter(payment_status="Partial").count(),
                "paid_count": qs.filter(payment_status="Paid").count(),
            },
        })


def _apply_common_filters(payment_qs, request):
    """employee_id / service / payment_mode — AdminFeesSummary & EmployeeCollectionReport
    rendayum idhu 3 filters-ah share pannikuranga."""
    employee_id = request.query_params.get("employee_id")
    service = request.query_params.get("service")
    payment_mode = request.query_params.get("payment_mode")

    if employee_id and employee_id != "all":
        payment_qs = payment_qs.filter(collected_by_id=employee_id)
    if service and service != "all":
        payment_qs = payment_qs.filter(fee__service=service)
    if payment_mode and payment_mode != "all":
        payment_qs = payment_qs.filter(payment_mode=payment_mode)
    return payment_qs


# ══════════════════════════════════════════════
#  ADMIN SUMMARY — "Tab 1" of AdminFees screen.
#  GET /admin-fees-summary/?from=&to=&employee_id=&service=&payment_mode=
#
#  counts (Today / This Month / This Year / Till Today) — employee_id
#  and payment_mode filters apply, but NOT the from/to date range
#  (idhu fixed calendar periods, so date range athை touch pannaadhu).
#
#  by_service breakdown — employee_id, payment_mode AND from/to
#  ellamе apply aagum (idhu thaan "searched range" result).
# ══════════════════════════════════════════════
class AdminFeesSummaryAPIView(APIView):
    def get(self, request):
        today = date.today()

        base_qs = FeePayment.objects.select_related("fee", "fee__client", "collected_by")
        base_qs = _apply_common_filters(base_qs, request)

        counts = {
            "today": str(
                base_qs.filter(payment_date=today).aggregate(t=Sum("amount"))["t"] or 0
            ),
            "this_month": str(
                base_qs.filter(
                    payment_date__year=today.year, payment_date__month=today.month
                ).aggregate(t=Sum("amount"))["t"] or 0
            ),
            "this_year": str(
                base_qs.filter(payment_date__year=today.year).aggregate(t=Sum("amount"))["t"] or 0
            ),
            "till_today": str(
                base_qs.filter(payment_date__lte=today).aggregate(t=Sum("amount"))["t"] or 0
            ),
        }

        # ── service-wise breakdown, respecting the date range filter too ──
        range_qs = base_qs
        from_str = request.query_params.get("from")
        to_str = request.query_params.get("to")
        if from_str:
            try:
                range_qs = range_qs.filter(payment_date__gte=datetime.strptime(from_str, "%Y-%m-%d").date())
            except ValueError:
                pass
        if to_str:
            try:
                range_qs = range_qs.filter(payment_date__lte=datetime.strptime(to_str, "%Y-%m-%d").date())
            except ValueError:
                pass

        by_service_rows = (
            range_qs.values("fee__service")
            .annotate(total=Sum("amount"))
            .order_by("-total")
        )
        by_service = [
            {"service": row["fee__service"], "total": str(row["total"])}
            for row in by_service_rows
        ]
        all_services_total = range_qs.aggregate(t=Sum("amount"))["t"] or 0

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "counts": counts,
                "all_services_total": str(all_services_total),
                "by_service": by_service,
            },
        })


# ══════════════════════════════════════════════
#  EMPLOYEE — "I collected this much this month" (unchanged)
# ══════════════════════════════════════════════
class EmployeeFeesAPIView(APIView):
    """
    GET /employee-fees/?employee_id=&from=&to=

    ✅ Date-wise filter — "from"/"to" kudukkaama vittal, DEFAULT-a "TODAY" oru
    naaள் data mattume varum (munnadi "whole month" default-a irundhuchu,
    ippo employee login pண்ணும்போதே "today's collection" first-a தெரியும்).
    "from"/"to" kudutha, andha range-oda data varum. Old "month=YYYY-MM"
    param-um still support pண்ணுrom (backward-compatible).
    """

    def get(self, request):
        emp_id = request.query_params.get("employee_id")
        if not emp_id:
            return Response({"error": "employee_id is required"}, status=status.HTTP_400_BAD_REQUEST)

        from_str = request.query_params.get("from")
        to_str = request.query_params.get("to")
        month_str = request.query_params.get("month")  # "YYYY-MM" — backward-compatible

        payments = FeePayment.objects.filter(collected_by_id=emp_id).select_related(
            "fee", "fee__client"
        )

        if from_str or to_str:
            # ✅ date-range filter — employee ownga தேர்ந்தெடுத்த From/To
            if from_str:
                try:
                    from_date = datetime.strptime(from_str, "%Y-%m-%d").date()
                    payments = payments.filter(payment_date__gte=from_date)
                except ValueError:
                    pass
            if to_str:
                try:
                    to_date = datetime.strptime(to_str, "%Y-%m-%d").date()
                    payments = payments.filter(payment_date__lte=to_date)
                except ValueError:
                    pass

            if from_str and to_str and from_str == to_str:
                range_label = (
                    "Today" if from_str == date.today().strftime("%Y-%m-%d")
                    else datetime.strptime(from_str, "%Y-%m-%d").strftime("%d %b %Y")
                )
            elif from_str and to_str:
                range_label = (
                    f"{datetime.strptime(from_str, '%Y-%m-%d').strftime('%d %b')} – "
                    f"{datetime.strptime(to_str, '%Y-%m-%d').strftime('%d %b %Y')}"
                )
            else:
                range_label = "Selected range"
        elif month_str:
            try:
                year, month = map(int, month_str.split("-"))
                payments = payments.filter(payment_date__year=year, payment_date__month=month)
                range_label = date(year, month, 1).strftime("%B %Y")
            except ValueError:
                return Response({"error": "month must be YYYY-MM"}, status=status.HTTP_400_BAD_REQUEST)
        else:
            # ✅ DEFAULT — "from"/"to"/"month" edhுவும் kudukkaama vittal, TODAY mattum
            today = date.today()
            payments = payments.filter(payment_date=today)
            range_label = "Today"

        payments = payments.order_by("-payment_date")
        total_collected = payments.aggregate(t=Sum("amount"))["t"] or 0

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "month_label": range_label,
                "total_collected": str(total_collected),
                "payment_count": payments.count(),
                "payments": [
                    {
                        **payment_json(p),
                        "client_id": p.fee.client_id,
                        "client_code": p.fee.client.client_code,
                        "client_name": p.fee.client.name,
                        "service": p.fee.service,
                        "can_edit": _can_edit_payment(p, emp_id) or _can_edit_fee(p.fee, emp_id),
                    }
                    for p in payments
                ],
            },
        })


# ══════════════════════════════════════════════
#  EMPLOYEE COLLECTION REPORT — "Tab 2" of AdminFees screen.
#  GET /employee-collection-report/?employee_id=5&from=&to=&service=&payment_mode=
#
#  "Vijay – Collection Report"
#    Total Clients : 18
#    Total Collections : ₹32,500
#    Date | Client | Service | Amount | Payment Mode
# ══════════════════════════════════════════════
class EmployeeCollectionReportAPIView(APIView):
    """
    GET /employee-collection-report/?employee_id=&from=&to=&service=&payment_mode=&client_id=

    ── employee_id="all" (illa kudukkaama vittal) ──
      "mode": "all" — ella employees-oda totals-um oru list-a varum (idha vachu
      "All Employees" filter-la, andha date range-oda ella employees collection-um
      oru table-a paakalam).

    ── employee_id=<id> ──
      "mode": "employee" — andha ஒரு employee-oda full report (Total Clients,
      Total Collections, payment history table) — munnadi maari thaan.

      client_id kூda kudutha, adhoda mேlே oru "client_summary" block-um varum:
      andha client-ku last payment eppo, evlo, இப்போது pending balance evlo nu.
      (client_summary — andha client-oda ella employees payments-um vachu
      calculate aagum, ellai ippove filter panna employee mattum illa —
      "last paid when/how much" nu client-oda real status idhு.)
    """

    def get(self, request):
        emp_id = request.query_params.get("employee_id")
        from_str = request.query_params.get("from")
        to_str = request.query_params.get("to")
        service = request.query_params.get("service")
        payment_mode = request.query_params.get("payment_mode")
        client_id = request.query_params.get("client_id")

        # ══ ALL EMPLOYEES — collection totals list ══
        if not emp_id or emp_id == "all":
            qs = FeePayment.objects.select_related("fee", "fee__client", "collected_by")

            if from_str:
                try:
                    qs = qs.filter(payment_date__gte=datetime.strptime(from_str, "%Y-%m-%d").date())
                except ValueError:
                    pass
            if to_str:
                try:
                    qs = qs.filter(payment_date__lte=datetime.strptime(to_str, "%Y-%m-%d").date())
                except ValueError:
                    pass
            if service and service != "all":
                qs = qs.filter(fee__service=service)
            if payment_mode and payment_mode != "all":
                qs = qs.filter(payment_mode=payment_mode)
            if client_id and client_id != "all":
                qs = qs.filter(fee__client_id=client_id)

            rows = (
                qs.values("collected_by_id", "collected_by__name")
                .annotate(
                    total=Sum("amount"),
                    payment_count=Count("id"),
                    clients=Count("fee__client_id", distinct=True),
                )
                .order_by("-total")
            )
            total_collections = qs.aggregate(t=Sum("amount"))["t"] or 0

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "data": {
                    "mode": "all",
                    "total_collections": str(total_collections),
                    "employees": [
                        {
                            "employee_id": row["collected_by_id"],
                            "employee_name": row["collected_by__name"] or "—",
                            "total_collections": str(row["total"]),
                            "payment_count": row["payment_count"],
                            "total_clients": row["clients"],
                        }
                        for row in rows
                    ],
                },
            })

        # ══ ONE EMPLOYEE — full report ══
        try:
            employee = Employee.objects.get(id=emp_id)
        except Employee.DoesNotExist:
            return Response({"error": "Employee not found"}, status=status.HTTP_404_NOT_FOUND)

        qs = FeePayment.objects.filter(collected_by_id=emp_id).select_related("fee", "fee__client")

        if from_str:
            try:
                qs = qs.filter(payment_date__gte=datetime.strptime(from_str, "%Y-%m-%d").date())
            except ValueError:
                pass
        if to_str:
            try:
                qs = qs.filter(payment_date__lte=datetime.strptime(to_str, "%Y-%m-%d").date())
            except ValueError:
                pass
        if service and service != "all":
            qs = qs.filter(fee__service=service)
        if payment_mode and payment_mode != "all":
            qs = qs.filter(payment_mode=payment_mode)
        if client_id and client_id != "all":
            qs = qs.filter(fee__client_id=client_id)

        qs = qs.order_by("payment_date")

        total_collections = qs.aggregate(t=Sum("amount"))["t"] or 0
        total_clients = qs.values("fee__client_id").distinct().count()

        # ── client_id kudutha, andha client-oda "last payment / pending" status ──
        client_summary = None
        if client_id and client_id != "all":
            try:
                client = FeeClient.objects.get(id=client_id)
                pending_balance = (
                    Fee.objects.filter(client_id=client_id).aggregate(t=Sum("balance"))["t"] or 0
                )
                last_payment = (
                    FeePayment.objects.filter(fee__client_id=client_id)
                    .order_by("-payment_date", "-created_at")
                    .first()
                )
                client_summary = {
                    "client_code": client.client_code,
                    "name": client.name,
                    "pending_balance": str(pending_balance),
                    "last_payment_date": last_payment.payment_date.strftime("%d-%m-%Y") if last_payment else None,
                    "last_payment_amount": str(last_payment.amount) if last_payment else None,
                }
            except FeeClient.DoesNotExist:
                pass

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "mode": "employee",
                "employee_name": employee.name,
                "total_clients": total_clients,
                "total_collections": str(total_collections),
                "client_summary": client_summary,
                "payments": [
                    {
                        "date": p.payment_date.strftime("%d-%m-%Y"),
                        "client": p.fee.client.name,
                        "service": p.fee.service,
                        "amount": str(p.amount),
                        "payment_mode": p.payment_mode,
                    }
                    for p in qs
                ],
            },
        })

from django.db.models import Sum
import logging

logger = logging.getLogger(__name__)

# TWILIO_ACCOUNT_SID = "ACe2442990b7cec8f0c0b61b4291495c1b"
# TWILIO_AUTH_TOKEN = "82931b7da5cc99f654d540c46f18e5d7"
# TWILIO_PHONE_NUMBER = "+13146369087"
FAST2SMS_API_KEY="tsImh5xd6D9V8yaSQoifOPlrj41c2TELqFwp7BkHCKeNAXgJYWWpKkENHqSL35VRrnJ6XlF8xBIgZfC4"
# TWOFACOR_API_KEY = "7e825d24-XXXX-XXXX-XXXX-0200cd936042"
# TWOFACOR_URL = "https://2factor.in/API/R1/"





def send_payment_sms(fee, payment, employee):
    """Payment save aana apparam client-ku Fast2SMS SMS anuppum."""

    try:
        # ─────────────────────────────────────
        # 1. Client mobile number
        # ─────────────────────────────────────
        phone = str(fee.client.mobile or "").strip()

        if not phone:
            logger.error("Client mobile number missing")
            return False

        # Remove +91 / spaces
        phone = phone.replace("+91", "").replace(" ", "").replace("-", "")

        # If number starts with 0, remove 0
        if phone.startswith("0"):
            phone = phone[1:]

        # Basic validation
        if len(phone) != 10 or not phone.isdigit():
            logger.error(f"Invalid client mobile number: {phone}")
            return False

        # ─────────────────────────────────────
        # 2. Calculate total paid
        # ─────────────────────────────────────
        total_paid = (
            FeePayment.objects
            .filter(fee=fee)
            .aggregate(
                t=Sum("amount")
            )["t"] or 0
        )

        balance = fee.fee_amount - total_paid

        # ─────────────────────────────────────
        # 3. Service name
        # ─────────────────────────────────────
        service = (
            fee.service_other
            if fee.service == "Other" and fee.service_other
            else fee.service
        )

        # ─────────────────────────────────────
        # 4. SMS Message
        # ─────────────────────────────────────
        body = (
            f"Hi {fee.client.name},\n\n"
            "Manju Associates\n"
            "Payment Received Successfully\n\n"
            f"We have received your payment of "
            f"Rs.{payment.amount:,.0f} for {service}.\n"
            f"Balance: Rs.{balance:,.0f}\n"
            f"Collected by: {employee.name}\n"
            f"Date: {payment.payment_date.strftime('%d-%m-%Y')}\n\n"
            "Thank you for choosing Manju Associates.\n"
            "Have a nice day!"
        )

        # ─────────────────────────────────────
        # 5. Fast2SMS API
        # ─────────────────────────────────────
        url = "https://www.fast2sms.com/dev/bulkV2"

        headers = {
            "authorization": FAST2SMS_API_KEY,
            "accept": "application/json",
            "content-type": "application/json",
        }

        data = {
            "route": "q",
            "message": body,
            "language": "english",
            "flash": 0,
            "numbers": phone,
            "sms_details": "1",
        }

        # ─────────────────────────────────────
        # 6. Send SMS
        # ─────────────────────────────────────
        response = requests.post(
            url,
            headers=headers,
            json=data,
            timeout=15,
        )

        logger.info(
            "Fast2SMS Response: %s",
            response.text
        )

        # ─────────────────────────────────────
        # 7. Check response
        # ─────────────────────────────────────
        if response.status_code == 200:
            result = response.json()

            if result.get("return") is True:
                logger.info(
                    f"Payment SMS sent successfully to {phone}"
                )
                return True

            logger.error(
                f"Fast2SMS SMS failed: {result}"
            )
            return False

        logger.error(
            f"Fast2SMS HTTP Error {response.status_code}: "
            f"{response.text}"
        )

        return False

    except Exception as e:
        logger.exception(
            f"Fast2SMS SMS failed: {e}"
        )
        return False


# def send_payment_sms(fee, payment, employee):
#     """Payment save aana apparam client-ku SMS anuppum."""
#     try:
#         phone = str(fee.client.mobile or "").strip()

#         if not phone:
#             return False

#         if not phone.startswith("+"):
#             phone = "+91" + phone

#         total_paid = FeePayment.objects.filter(
#             fee=fee
#         ).aggregate(
#             t=Sum("amount")
#         )["t"] or 0

#         balance = fee.fee_amount - total_paid

#         service = (
#             fee.service_other
#             if fee.service == "Other" and fee.service_other
#             else fee.service
#         )

       
#         body = (
#             "Hi Namaste 🙏\n\n"
#             "Manju Associates\n"
#             "Payment Received Successfully\n\n"
#             f"Dear {fee.client.name},\n"
#             f"We have received your payment of Rs.{payment.amount:,.0f} for {service}.\n"
#             f"Balance: Rs.{balance:,.0f}\n"
#             f"Collected by: {employee.name}\n"
#             f"Date: {payment.payment_date.strftime('%d-%m-%Y')}\n\n"
#             "Thank you for choosing Manju Associates.\n"
#             "Have a nice day! 😊"
#         )

#         client = TwilioClient(
#             TWILIO_ACCOUNT_SID,
#             TWILIO_AUTH_TOKEN
#         )

#         client.messages.create(
#             to=phone,
#             from_=TWILIO_PHONE_NUMBER,
#             body=body,
#         )

#         return True

#     except Exception as e:
#         logger.error(f"SMS failed: {e}")
#         print("SMS failed:", e)
#         return False
# ══════════════════════════════════════════════
#  ADD PAYMENT — used by "EmployeeFeesAdd" screen (unchanged logic).
# ══════════════════════════════════════════════
class FeePaymentCreateAPIView(APIView):
    def post(self, request):
        data = request.data
        employee_id = data.get("employee_id")
        if not employee_id:
            return Response({"error": "employee_id is required"}, status=status.HTTP_400_BAD_REQUEST)

        try:
            employee = Employee.objects.get(id=employee_id)
        except Employee.DoesNotExist:
            return Response({"error": "Employee not found"}, status=status.HTTP_404_NOT_FOUND)

        amount = data.get("amount")
        if not amount:
            return Response({"error": "amount is required"}, status=status.HTTP_400_BAD_REQUEST)
        try:
            amount = Decimal(str(amount))
            if amount <= 0:
                raise InvalidOperation
        except (InvalidOperation, TypeError):
            return Response({"error": "amount must be a valid positive number"}, status=status.HTTP_400_BAD_REQUEST)

        fee_id = data.get("fee_id")
        if fee_id:
            try:
                fee = Fee.objects.get(id=fee_id)
            except Fee.DoesNotExist:
                return Response({"error": "Fee record not found"}, status=status.HTTP_404_NOT_FOUND)
        else:
            client_id = data.get("client_id")
            due_date_str = data.get("due_date")
            if not client_id or not due_date_str:
                return Response(
                    {"error": "client_id and due_date are required to create a new fee"},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            try:
                fee_client = FeeClient.objects.get(id=client_id)
            except FeeClient.DoesNotExist:
                return Response({"error": "Client not found"}, status=status.HTTP_404_NOT_FOUND)

            try:
                due_date = datetime.strptime(due_date_str, "%Y-%m-%d").date()
            except ValueError:
                return Response({"error": "due_date must be YYYY-MM-DD"}, status=status.HTTP_400_BAD_REQUEST)

            fee_amount_raw = data.get("fee_amount", amount)
            try:
                fee_amount = Decimal(str(fee_amount_raw))
                if fee_amount <= 0:
                    raise InvalidOperation
            except (InvalidOperation, TypeError):
                return Response(
                    {"error": "fee_amount must be a valid positive number"},
                    status=status.HTTP_400_BAD_REQUEST,
                )

            fee = Fee.objects.create(
                client=fee_client,
                service=data.get("service", "GST Filing"),
                service_other=data.get("service_other"),
                fee_amount=fee_amount,  # ✅ ippove Decimal-a irukும், str illa
                billing_frequency=data.get("billing_frequency", "Monthly"),
                due_date=due_date,
                created_by=employee,
            )

        payment_date_str = data.get("payment_date")
        try:
            payment_date = (
                datetime.strptime(payment_date_str, "%Y-%m-%d").date()
                if payment_date_str else date.today()
            )
        except ValueError:
            return Response({"error": "payment_date must be YYYY-MM-DD"}, status=status.HTTP_400_BAD_REQUEST)

        payment = FeePayment.objects.create(
            fee=fee,
            collected_by=employee,
            amount=amount,
            payment_date=payment_date,
            payment_mode=data.get("payment_mode", "Cash"),
            reference_no=data.get("reference_no"),
            note=data.get("note"),
        )
        fee.refresh_from_db()

        send_payment_sms(fee, payment, employee)

        return Response({
            "status": {"code": status.HTTP_201_CREATED, "response": "success"},
            "error": None,
            "message": "Payment recorded",
            "data": fee_json(fee, with_payments=True),
        }, status=status.HTTP_201_CREATED)


# ══════════════════════════════════════════════
#  CLIENT HISTORY — oru client-oda MUZHU picture (Admin "Client" tab-ku)
#  GET /client-history/?client_id=
#    -> client info + total fee / paid / pending + last payment +
#       ella fee records (service, due date...) + ella payments
#       (yaaru collect pannaanga nu-kooda)
# ══════════════════════════════════════════════
class ClientHistoryAPIView(APIView):
    def get(self, request):
        client_id = request.query_params.get("client_id")
        if not client_id:
            return Response({"error": "client_id is required"}, status=status.HTTP_400_BAD_REQUEST)
        try:
            client = FeeClient.objects.get(id=client_id)
        except (FeeClient.DoesNotExist, ValueError):
            return Response({"error": "Client not found"}, status=status.HTTP_404_NOT_FOUND)

        fees = Fee.objects.filter(client_id=client_id).select_related("client").order_by("-due_date")
        totals = fees.aggregate(fee=Sum("fee_amount"), paid=Sum("paid_amount"), pending=Sum("balance"))
        payments = (
            FeePayment.objects.filter(fee__client_id=client_id)
            .select_related("fee", "collected_by")
            .order_by("-payment_date", "-created_at")
        )
        last = payments.first()

        def svc(f):
            return f.service if f.service != "Other" else (f.service_other or "Other")

        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": {
                "client": client_json(client),
                "totals": {
                    "total_fee": str(totals["fee"] or 0),
                    "total_paid": str(totals["paid"] or 0),
                    "total_pending": str(totals["pending"] or 0),
                },
                "last_payment": {
                    "date": last.payment_date.strftime("%d-%m-%Y"),
                    "amount": str(last.amount),
                    "collected_by": last.collected_by.name if last.collected_by else "—",
                } if last else None,
                "fees": [fee_json(f) for f in fees],
                "payments": [
                    {
                        "id": p.id,
                        "fee_id": p.fee_id,
                        "date": p.payment_date.strftime("%d-%m-%Y"),
                        "amount": str(p.amount),
                        "payment_mode": p.payment_mode,
                        "service": svc(p.fee),
                        "collected_by": p.collected_by.name if p.collected_by else "—",
                    }
                    for p in payments
                ],
            },
        })


# ══════════════════════════════════════════════
#  ONE PAYMENT — view / EDIT / cancel a single collection entry
#
#  GET    /fee-payment/<id>/?employee_id=  -> payment + fee (service, due date...)
#                                             + can_edit_payment / can_edit_fee
#  PUT    /fee-payment/<id>/               -> edit; ANDHA entry pannina employee
#                                             mattum (see EMPLOYEE_EDIT_SAME_DAY_ONLY)
#  DELETE /fee-payment/<id>/?employee_id=  -> cancel; same rule
# ══════════════════════════════════════════════
class FeePaymentDetailAPIView(APIView):
    def get_object(self, id):
        try:
            return FeePayment.objects.select_related("fee", "fee__client", "collected_by").get(id=id)
        except FeePayment.DoesNotExist:
            return None

    def _detail(self, p, employee_id):
        fee = p.fee
        return {
            "payment": payment_json(p),
            "fee": {
                "id": fee.id,
                "client": client_json(fee.client),
                "service": fee.service,
                "service_other": fee.service_other,
                "fee_amount": str(fee.fee_amount),
                "billing_frequency": fee.billing_frequency,
                "due_date": fee.due_date.strftime("%Y-%m-%d"),
                "next_due_date": fee.next_due_date.strftime("%Y-%m-%d") if fee.next_due_date else None,
                "paid_amount": str(fee.paid_amount),
                "balance": str(fee.balance),
                "payment_status": fee.payment_status,
            },
            "can_edit_payment": _can_edit_payment(p, employee_id),
            "can_edit_fee": _can_edit_fee(fee, employee_id),
        }

    def get(self, request, id):
        p = self.get_object(id)
        if not p:
            return Response({"error": "Payment not found"}, status=status.HTTP_404_NOT_FOUND)
        employee_id = request.query_params.get("employee_id")
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "data": self._detail(p, employee_id),
        })

    def put(self, request, id):
        p = self.get_object(id)
        if not p:
            return Response({"error": "Payment not found"}, status=status.HTTP_404_NOT_FOUND)

        data = request.data
        employee_id = data.get("employee_id")
        if not employee_id:
            return Response({"error": "employee_id is required"}, status=status.HTTP_400_BAD_REQUEST)

        fee = p.fee
        can_pay = _can_edit_payment(p, employee_id)
        can_fee = _can_edit_fee(fee, employee_id)
        rule = " (same day only)" if EMPLOYEE_EDIT_SAME_DAY_ONLY else ""
        if not (can_pay or can_fee):
            return Response(
                {"error": f"Only the employee who entered this record can edit it{rule}."},
                status=status.HTTP_403_FORBIDDEN,
            )

        pay_keys = ("amount", "payment_date", "payment_mode", "reference_no", "note")
        fee_keys = ("service", "service_other", "billing_frequency", "fee_amount", "due_date", "next_due_date")
        if not can_pay and any(k in data for k in pay_keys):
            return Response({"error": f"You can't edit this payment{rule}."}, status=status.HTTP_403_FORBIDDEN)
        if not can_fee and any(k in data for k in fee_keys):
            return Response(
                {"error": f"You can't edit the fee details (service / due date / fee amount) of this record{rule}."},
                status=status.HTTP_403_FORBIDDEN,
            )

        def bad(msg):
            return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)

        def parse_date(value):
            return datetime.strptime(value, "%Y-%m-%d").date()

        def parse_money(value):
            v = Decimal(str(value))
            if v <= 0:
                raise InvalidOperation
            return v

        # ── validate everything FIRST, apply only if all OK ──
        new_amount = p.amount
        new_pay_date = p.payment_date
        new_mode = p.payment_mode
        new_fee_amount = Decimal(str(fee.fee_amount))
        try:
            if can_pay:
                if "amount" in data:
                    new_amount = parse_money(data["amount"])
                if data.get("payment_date"):
                    new_pay_date = parse_date(data["payment_date"])
                if data.get("payment_mode"):
                    valid = {c[0] for c in FeePayment._meta.get_field("payment_mode").choices}
                    if data["payment_mode"] not in valid:
                        return bad("Invalid payment mode")
                    new_mode = data["payment_mode"]
            if can_fee:
                if "fee_amount" in data:
                    new_fee_amount = parse_money(data["fee_amount"])
                if data.get("service"):
                    valid = {c[0] for c in Fee._meta.get_field("service").choices}
                    if data["service"] not in valid:
                        return bad("Invalid service")
                if data.get("billing_frequency"):
                    valid = {c[0] for c in Fee._meta.get_field("billing_frequency").choices}
                    if data["billing_frequency"] not in valid:
                        return bad("Invalid billing frequency")
                new_due = parse_date(data["due_date"]) if data.get("due_date") else None
                new_next_due = parse_date(data["next_due_date"]) if data.get("next_due_date") else None
            else:
                new_due = new_next_due = None
        except (InvalidOperation, TypeError):
            return bad("Amount must be a valid positive number")
        except ValueError:
            return bad("Dates must be YYYY-MM-DD")

        others = fee.payments.exclude(id=p.id).aggregate(t=Sum("amount"))["t"] or Decimal("0")
        if others + new_amount > new_fee_amount:
            return bad(
                f"Total collected (₹{others + new_amount}) would exceed the fee amount (₹{new_fee_amount})."
            )

        with transaction.atomic():
            if can_fee:
                due_changed = bool(new_due and new_due != fee.due_date)
                freq_changed = bool(
                    data.get("billing_frequency") and data["billing_frequency"] != fee.billing_frequency
                )
                fee.fee_amount = new_fee_amount
                if data.get("service"):
                    fee.service = data["service"]
                if "service_other" in data:
                    fee.service_other = (data.get("service_other") or "").strip() or None
                if data.get("billing_frequency"):
                    fee.billing_frequency = data["billing_frequency"]
                if new_due:
                    fee.due_date = new_due
                if new_next_due:
                    fee.next_due_date = new_next_due
                elif due_changed or freq_changed:
                    fee.next_due_date = fee.compute_next_due_date()
                fee.save()
            if can_pay:
                p.amount = new_amount
                p.payment_date = new_pay_date
                p.payment_mode = new_mode
                if "reference_no" in data:
                    p.reference_no = (data.get("reference_no") or "").strip() or None
                if "note" in data:
                    p.note = (data.get("note") or "").strip() or None
                p.save()  # ✅ Fee.recalc() automatic
            else:
                fee.recalc()

        p = self.get_object(id)
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": "Record updated",
            "data": self._detail(p, employee_id),
        })

    def delete(self, request, id):
        p = self.get_object(id)
        if not p:
            return Response({"error": "Payment not found"}, status=status.HTTP_404_NOT_FOUND)
        employee_id = request.query_params.get("employee_id") or request.data.get("employee_id")
        if not _can_edit_payment(p, employee_id):
            return Response(
                {"error": "Only the employee who entered this payment can cancel it."},
                status=status.HTTP_403_FORBIDDEN,
            )
        p.delete()  # ✅ Fee.recalc() automatic-a trigger aagum
        return Response({
            "status": {"code": status.HTTP_200_OK, "response": "success"},
            "error": None,
            "message": "Payment removed",
        }) 
class EmployeeStatusAPIView(APIView):
    """
    Active / Inactive toggle mattum. geostatus-ஐ இதுல தொடாது —
    அது தனி concept, தனி button, தனி API (கீழ EmployeeGeoStatusAPIView).
    """
 
    def patch(self, request, id):
        try:
            employee = Profiles.objects.get(user_id=id)
        except Profiles.DoesNotExist:   # ✅ bug fix: Employee.DoesNotExist தவறு, Profiles.DoesNotExist சரி
            return Response({
                "status": {"code": 404, "response": "failed"},
                "error": "Employee not found",
            }, status=404)
 
        # 1-ஆ இருந்தா 2-க்கு மாற்றவும், இல்லையென்றால் 1-க்கு மாற்றவும்
        employee.status = 2 if employee.status == 1 else 1
        employee.save()
 
        return Response({
            "status": {"code": 200, "response": "success"},
            "new_status": employee.status,
            "message": "Status updated successfully",
        })
 
 
class EmployeeGeoStatusAPIView(APIView):
    """
    ✅ புது API — "Anywhere Attendance" / WFH toggle.
    geostatus = 1 -> office geofence-ஐ bypass பண்ணி எங்கிருந்தும் attendance mark பண்ணலாம்
    geostatus = 2 (அல்லது default) -> office radius-க்குள் தான் attendance mark பண்ண முடியும்
 
    PATCH /employee-geostatus/<id>/
    """
    def patch(self, request, id):
        try:
            employee = Profiles.objects.get(user_id=id)
        except Profiles.DoesNotExist:
            return Response({
                "status": {"code": 404, "response": "failed"},
                "error": "Employee not found",
            }, status=404)

        print(id,'RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR')

 
        employee.geostatus = 2 if employee.geostatus == 1 else 1
        employee.save()
 
        return Response({
            "status": {"code": 200, "response": "success"},
            "new_geostatus": employee.geostatus,
            "message": (
                "Anywhere attendance enabled"
                if employee.geostatus == 1
                else "Anywhere attendance disabled — office location required"
            ),
        })
 
 

# import calendar
# from datetime import datetime, time, timedelta, date as date_cls
 
# from rest_framework import status
# from rest_framework.views import APIView
# from rest_framework.response import Response
 
# from .models import Setting, Holiday, Employee, Attendance
 
 
# # ══════════════════════════════════════════════
# #  SHARED HELPERS — Settings + Holiday read pannum
# # ══════════════════════════════════════════════
 
# # Python date.weekday(): Mon=0, Tue=1, Wed=2, Thu=3, Fri=4, Sat=5, Sun=6
# DAY_KEYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
 
 
# def fmt_hhmm(total_minutes):
#     total_minutes = max(0, int(total_minutes))
#     return f"{total_minutes // 60:02d}:{total_minutes % 60:02d}"
 
 
# def get_attendance_config():
#     """
#     Setting table (pk=1) -la irundhu shift timing, grace period,
#     weekly off days edukkum. Monthly + Today rendu API-um idhe
#     function-a use pannanum — settings maathi na rendu idathulayum
#     automatic-a update aagum.
#     """
#     setting, _ = Setting.objects.get_or_create(pk=1)
 
#     shift_start = setting.shift_start or time(9, 30)
#     shift_end = setting.shift_end or time(18, 30)
#     grace = int(setting.grace_minutes or 0)
 
#     # DB-la "Sun,Sat" nu irukkum -> {"Sun", "Sat"} -> weekday index {6, 5}
#     off_names = {
#         p.strip()[:3].title()
#         for p in (setting.weekly_off or "").split(",")
#         if p.strip()
#     }
#     weekly_off = {i for i, nm in enumerate(DAY_KEYS) if nm in off_names}
 
#     # shift_start + grace = late_after. Idhukku mela vandha "Late"
#     late_after = (
#         datetime.combine(date_cls.today(), shift_start) + timedelta(minutes=grace)
#     ).time()
 
#     return {
#         "setting": setting,
#         "shift_start": shift_start,
#         "shift_end": shift_end,
#         "grace_minutes": grace,
#         "late_after": late_after,
#         "weekly_off": weekly_off,          # set of weekday indexes e.g. {5, 6}
#         "weekly_off_names": off_names,     # e.g. {"Sat", "Sun"}
#         "half_day_hours": float(setting.half_day_hours or 4),
#     }
 
 
# def get_holiday_map(start_date, end_date):
#     """Holiday table -la irundhu {date_obj: name} — date range kku eduthu tharum."""
#     qs = Holiday.objects.filter(date__range=[start_date, end_date])
#     return {h.date: h.name for h in qs}
 
 
# def emp_display_name(emp):
#     return getattr(emp, "name", None) or getattr(emp, "studentName", "") or "—"
 
 
# def emp_display_code(emp):
#     return getattr(emp, "mobile", None) or getattr(emp, "emp_id", None) or str(emp.id)
 
 
# def is_late(in_time, cfg):
#     return bool(in_time and in_time > cfg["late_after"])
 
 
# def ot_seconds(out_time, cfg):
#     """Shift-end kadandha out_time irundha, adhukku mela irukkura seconds thara."""
#     if not out_time or out_time <= cfg["shift_end"]:
#         return 0
#     d = date_cls.today()
#     diff = datetime.combine(d, out_time) - datetime.combine(d, cfg["shift_end"])
#     return max(0, int(diff.total_seconds()))
 
 
# # ══════════════════════════════════════════════
# #  SETTINGS
# # ══════════════════════════════════════════════
# class SettingAPIView(APIView):
#     """Single settings row (pk=1) — GET fetch, PUT partial update."""
 
#     def get_object(self):
#         setting, _ = Setting.objects.get_or_create(pk=1)
#         return setting
 
#     def get(self, request):
#         setting = self.get_object()
#         data = {
#             "shift_start": setting.shift_start.strftime("%I:%M %p") if setting.shift_start else None,
#             "shift_end": setting.shift_end.strftime("%I:%M %p") if setting.shift_end else None,
#             "weekly_off": [d for d in (setting.weekly_off or "").split(",") if d],
#             "grace_minutes": setting.grace_minutes,
#             "half_day_hours": float(setting.half_day_hours) if setting.half_day_hours is not None else 0,
#             "overtime_enabled": setting.overtime_enabled,
#             "geofence_enabled": setting.geofence_enabled,
#             "face_sensitivity": setting.face_sensitivity,
#             "company_name": setting.company_name,
#             "time_zone": setting.time_zone,
#         }
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "data": data,
#         })
 
#     def put(self, request):
#         setting = self.get_object()
#         data = request.data
 
#         def parse_time(value):
#             if value in (None, ""):
#                 return None
#             raw = str(value).strip().upper().replace(".", "")
#             for fmt in ["%I:%M %p", "%I:%M%p", "%H:%M:%S", "%H:%M"]:
#                 try:
#                     return datetime.strptime(raw, fmt).time()
#                 except ValueError:
#                     continue
#             raise ValueError(f"Invalid time '{value}'. Use hh:mm AM/PM format.")
 
#         try:
#             if "shift_start" in data:
#                 setting.shift_start = parse_time(data.get("shift_start"))
#             if "shift_end" in data:
#                 setting.shift_end = parse_time(data.get("shift_end"))
#             if "weekly_off" in data:
#                 wo = data.get("weekly_off")
#                 setting.weekly_off = ",".join(wo) if isinstance(wo, list) else (wo or "")
#             if "grace_minutes" in data:
#                 setting.grace_minutes = int(data.get("grace_minutes"))
#             if "half_day_hours" in data:
#                 setting.half_day_hours = float(data.get("half_day_hours"))
#             if "overtime_enabled" in data:
#                 setting.overtime_enabled = bool(data.get("overtime_enabled"))
#             if "geofence_enabled" in data:
#                 setting.geofence_enabled = bool(data.get("geofence_enabled"))
#             if "face_sensitivity" in data:
#                 setting.face_sensitivity = data.get("face_sensitivity")
#             if "company_name" in data:
#                 setting.company_name = (data.get("company_name") or "").strip()
#             if "time_zone" in data:
#                 setting.time_zone = (data.get("time_zone") or "").strip()
 
#             setting.updated_by = request.user if request.user.is_authenticated else None
#             setting.save()
#         except (ValueError, TypeError) as e:
#             return Response({
#                 "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
#                 "error": True,
#                 "message": str(e),
#             }, status=status.HTTP_400_BAD_REQUEST)
 
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "message": "Settings updated successfully",
#             "data": self.get(request).data["data"],
#         })
 
 
# # ══════════════════════════════════════════════
# #  HOLIDAY
# # ══════════════════════════════════════════════
# class HolidayAPIView(APIView):
#     """Holiday CRUD — List / Add / Delete."""
 
#     def get(self, request, id=None):
#         if id is not None:
#             try:
#                 h = Holiday.objects.get(id=id)
#             except Holiday.DoesNotExist:
#                 return Response({
#                     "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
#                     "error": True,
#                     "message": "Holiday not found",
#                 }, status=status.HTTP_404_NOT_FOUND)
#             return Response({
#                 "status": {"code": status.HTTP_200_OK, "response": "success"},
#                 "error": None,
#                 "data": {"id": h.id, "name": h.name, "date": str(h.date)},
#             })
 
#         holidays = Holiday.objects.all().order_by("date")
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "data": [{"id": h.id, "name": h.name, "date": str(h.date)} for h in holidays],
#         })
 
#     def post(self, request):
#         name = (request.data.get("name") or "").strip()
#         date_val = (request.data.get("date") or "").strip()
 
#         if not name or not date_val:
#             return Response({
#                 "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
#                 "error": True,
#                 "message": "Name and date are required.",
#             }, status=status.HTTP_400_BAD_REQUEST)
 
#         try:
#             datetime.strptime(date_val, "%Y-%m-%d")
#         except ValueError:
#             return Response({
#                 "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
#                 "error": True,
#                 "message": "Date must be in YYYY-MM-DD format.",
#             }, status=status.HTTP_400_BAD_REQUEST)
 
#         holiday = Holiday.objects.create(
#             name=name,
#             date=date_val,
#             created_by=request.user if request.user.is_authenticated else None,
#         )
#         return Response({
#             "status": {"code": status.HTTP_201_CREATED, "response": "success"},
#             "error": None,
#             "message": "Holiday added successfully",
#             "data": {"id": holiday.id, "name": holiday.name, "date": str(holiday.date)},
#         }, status=status.HTTP_201_CREATED)
 
#     def delete(self, request, id=None):
#         if id is None:
#             return Response({
#                 "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
#                 "error": True,
#                 "message": "Holiday id required in URL.",
#             }, status=status.HTTP_400_BAD_REQUEST)
 
#         try:
#             holiday = Holiday.objects.get(id=id)
#         except Holiday.DoesNotExist:
#             return Response({
#                 "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
#                 "error": True,
#                 "message": "Holiday not found",
#             }, status=status.HTTP_404_NOT_FOUND)
 
#         holiday.delete()
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "message": "Holiday deleted",
#         })
 
# class EmployeeMiniListAPIView(APIView):
#     def get(self, request):
#         employees = Employee.objects.filter(status=1).order_by("name")
#         data = [
#             {"id": e.id, "name": emp_display_name(e), "code": emp_display_code(e)}
#             for e in employees
#         ]
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "data": data,
#         })



 
# # ══════════════════════════════════════════════
# #  MONTHLY ATTENDANCE
# #  (Class name "MonthattendanceAPIView" — spelling idhe
# #   ungal urls.py already point pannirukku, adha maathaadhinga)
# # ══════════════════════════════════════════════
# class MonthattendanceAPIView(APIView):
#     """
#     Status per day: P / A / H (Holiday table) / W (Setting.weekly_off) / '--' (future)
#     Late  = in_time > shift_start + grace_minutes (Setting table)
#     3 Late = 0.5 day cut from Present (effective_present)
#     Optional filter: ?employee_id=<id>  (illa na ellame varum)
#     """
 
#     def get(self, request, *args, **kwargs):
#         val = request.GET.get("val")
#         if not val:
#             return Response(
#                 {"error": "val is required. Use ?val=YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         try:
#             first_day = datetime.strptime(val + "-01", "%Y-%m-%d").date()
#         except ValueError:
#             return Response(
#                 {"error": "Invalid val format. Use YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         if first_day.month == 12:
#             last_day = first_day.replace(year=first_day.year + 1, month=1, day=1) - timedelta(days=1)
#         else:
#             last_day = first_day.replace(month=first_day.month + 1, day=1) - timedelta(days=1)
 
#         dates_in_month = [
#             first_day + timedelta(days=i) for i in range((last_day - first_day).days + 1)
#         ]
 
#         cfg = get_attendance_config()
#         holiday_map = get_holiday_map(first_day, last_day)  # ✅ Holiday table
 
#         emp_param = request.GET.get("employee_id") or request.GET.get("student_id")
#         employees = Employee.objects.filter(status=1)
#         if emp_param and emp_param != "all":
#             employees = employees.filter(id=emp_param)
 
#         all_attendance = Attendance.objects.filter(date__range=[first_day, last_day])
#         today = date_cls.today()
#         final_report = []
 
#         for emp in employees:
#             emp_records = {r.date: r for r in all_attendance if r.employee_id == emp.id}
#             total_p = total_a = late_count = 0
#             total_ot_sec = 0
#             attendance_map = {}
 
#             for day in dates_in_month:
#                 day_str = day.strftime("%Y-%m-%d")
#                 ot_display = "00:00"
#                 wd = day.weekday()
 
#                 is_holiday = day in holiday_map              # ✅ Holiday table
#                 is_weekly_off = wd in cfg["weekly_off"]       # ✅ Setting.weekly_off
 
#                 if is_holiday:
#                     status_code = "H"
#                 elif is_weekly_off:
#                     status_code = "W"
#                 elif day > today:
#                     status_code = "--"
#                 elif day in emp_records:
#                     rec = emp_records[day]
#                     if rec.status == "Present":
#                         status_code = "P"
#                         total_p += 1
 
#                         if is_late(rec.in_time, cfg):         # ✅ Setting.grace_minutes
#                             late_count += 1
 
#                         sec = ot_seconds(rec.out_time, cfg)   # ✅ Setting.shift_end
#                         if sec > 0:
#                             total_ot_sec += sec
#                             ot_display = fmt_hhmm(sec // 60)
#                     else:
#                         status_code = "A"
#                         total_a += 1
#                 else:
#                     status_code = "A"
#                     total_a += 1
 
#                 attendance_map[day_str] = {
#                     "status": status_code,
#                     "ot": ot_display,
#                     "holiday_name": holiday_map.get(day),
#                 }
 
#             half_day_cuts = late_count // 3
#             late_deduction = round(half_day_cuts * 0.5, 2)
#             effective_present = round(max(0, total_p - late_deduction), 2)
 
#             total_ot_h = int(total_ot_sec // 3600)
#             total_ot_m = int((total_ot_sec % 3600) // 60)
 
#             final_report.append({
#                 "id": emp.id,
#                 "name": emp_display_name(emp),
#                 "code": emp_display_code(emp),
#                 "total_present": total_p,
#                 "effective_present": effective_present,
#                 "total_absent": total_a,
#                 "total_ot": f"{total_ot_h:02d}:{total_ot_m:02d}",
#                 "late_count": late_count,
#                 "half_day_cuts": half_day_cuts,
#                 "late_deduction": late_deduction,
#                 "attendance": attendance_map,
#             })
 
#         return Response({
#             "days": [
#                 {
#                     "date": d.strftime("%Y-%m-%d"),
#                     "day_num": d.day,
#                     "display": d.strftime("%d"),
#                     "day_name": d.strftime("%a"),
#                     "is_weekly_off": d.weekday() in cfg["weekly_off"],   # ✅
#                     "is_holiday": d in holiday_map,                      # ✅
#                     "holiday_name": holiday_map.get(d),
#                 }
#                 for d in dates_in_month
#             ],
#             "report": final_report,
#             "config": {
#                 "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
#                 "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
#                 "grace_minutes": cfg["grace_minutes"],
#                 "late_after": cfg["late_after"].strftime("%I:%M %p"),
#                 # "weekly_off": sorted(cfg["weekly_off_names"]),
#                 "weekly_off": sorted(DAY_KEYS[i] for i in cfg["weekly_off"]),
#                 "late_rule": "3 Late = 0.5 day deduction",
#             },
#             "holidays": [
#                 {"date": d.strftime("%Y-%m-%d"), "name": n}
#                 for d, n in sorted(holiday_map.items())
#             ],
#         }, status=status.HTTP_200_OK)
 
 
# # ══════════════════════════════════════════════
# #  TODAY ATTENDANCE
# # ══════════════════════════════════════════════
# class TodayAttendanceAPIView(APIView):
#     """Settings + Holiday table wise, particular date-oda present/absent."""
 
#     def get(self, request):
#         date_str = request.query_params.get("date")
#         try:
#             target_date = (
#                 datetime.strptime(date_str, "%Y-%m-%d").date()
#                 if date_str else date_cls.today()
#             )
#         except ValueError:
#             return Response(
#                 {"error": "Invalid date. Use YYYY-MM-DD"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         cfg = get_attendance_config()
#         holiday_map = get_holiday_map(target_date, target_date)  # ✅ Holiday table
#         hol_name = holiday_map.get(target_date)
#         is_weekly_off = target_date.weekday() in cfg["weekly_off"]  # ✅ Setting table
#         day_type = "holiday" if hol_name else ("weekly_off" if is_weekly_off else "working")
 
#         emp_param = request.query_params.get("employee_id")
#         employees = Employee.objects.filter(status=1)
#         if emp_param and emp_param != "all":
#             employees = employees.filter(id=emp_param)
 
#         records = Attendance.objects.filter(
#             date=target_date, employee__in=employees
#         ).select_related("employee")
 
#         present_list = []
#         for rec in records:
#             late = is_late(rec.in_time, cfg)
#             sec = ot_seconds(rec.out_time, cfg)
#             emp = rec.employee
 
#             present_list.append({
#                 "id": emp.id,
#                 "name": emp_display_name(emp),
#                 "code": emp_display_code(emp),
#                 "photo": emp.photo.url if getattr(emp, "photo", None) else None,
#                 "in_time": rec.in_time.strftime("%I:%M %p") if rec.in_time else "--",
#                 "out_time": rec.out_time.strftime("%I:%M %p") if rec.out_time else "--",
#                 "entry_status": "Late" if late else "On Time",
#                 "late": late,
#                 "ot_time": fmt_hhmm(sec // 60),
#                 "ot_minutes": sec // 60,
#             })
 
#         present_ids = [r.employee_id for r in records]
#         absent_list = [
#             {
#                 "id": e.id,
#                 "name": emp_display_name(e),
#                 "code": emp_display_code(e),
#                 "photo": e.photo.url if getattr(e, "photo", None) else None,
#             }
#             for e in employees.exclude(id__in=present_ids)
#         ]
 
#         return Response({
#             "date": target_date.isoformat(),
#             "day_type": day_type,
#             "is_holiday": bool(hol_name),
#             "holiday_name": hol_name,
#             "is_weekly_off": is_weekly_off,
#             "present_count": len(present_list),
#             "absent_count": len(absent_list),
#             "present_list": sorted(present_list, key=lambda x: x["name"]),
#             "absent_list": sorted(absent_list, key=lambda x: x["name"]),
#             "config": {
#                 "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
#                 "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
#                 "grace_minutes": cfg["grace_minutes"],
#                 "late_after": cfg["late_after"].strftime("%I:%M %p"),
#             },
#         }, status=status.HTTP_200_OK)




# # ══════════════════════════════════════════════
# #  EMPLOYEE DASHBOARD
# #  Setting table (shift/weekly-off) + Holiday table (upcoming holiday)
# #  + idhe employee-oda indha maasa summary.
# # ══════════════════════════════════════════════
# class EmployeeDashboardAPIView(APIView):
#     """
#     GET /employee-dashboard/?employee_id=<id>
 
#     employee_id kudukkalana, login panna user-oda user_data-la
#     irundhu employee_id anuppanum (frontend already anuppum).
#     """
 
#     def get(self, request):
#         emp_id = request.query_params.get("employee_id")
#         if not emp_id:
#             return Response(
#                 {"error": "employee_id is required"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         try:
#             emp = Employee.objects.get(id=emp_id)
#         except Employee.DoesNotExist:
#             return Response(
#                 {"error": "Employee not found"},
#                 status=status.HTTP_404_NOT_FOUND,
#             )
 
#         cfg = get_attendance_config()
#         today = date_cls.today()
 
#         # ── Ithu maasathoda holidays + next upcoming holiday ──
#         month_start = today.replace(day=1)
#         if today.month == 12:
#             month_end = today.replace(year=today.year + 1, month=1, day=1) - timedelta(days=1)
#         else:
#             month_end = today.replace(month=today.month + 1, day=1) - timedelta(days=1)
 
#         month_holidays = get_holiday_map(month_start, month_end)  # ✅ Holiday table
#         upcoming = Holiday.objects.filter(date__gte=today).order_by("date").first()
 
#         # ── Today status (indha employee mattum) ──
#         today_rec = Attendance.objects.filter(employee_id=emp.id, date=today).first()
#         is_holiday_today = today in month_holidays
#         is_weekly_off_today = today.weekday() in cfg["weekly_off"]  # ✅ Setting table
 
#         if is_holiday_today:
#             today_status = "Holiday"
#         elif is_weekly_off_today:
#             today_status = "Weekly Off"
#         elif today_rec and today_rec.status == "Present":
#             today_status = "Late" if is_late(today_rec.in_time, cfg) else "Present"
#         else:
#             today_status = "Not Checked In"
 
#         # ── This month quick summary (present/absent/late) ──
#         month_records = Attendance.objects.filter(
#             employee_id=emp.id, date__range=[month_start, month_end]
#         )
#         rec_by_date = {r.date: r for r in month_records}
 
#         total_p = total_a = late_count = 0
#         d = month_start
#         while d <= min(month_end, today):
#             is_off = d.weekday() in cfg["weekly_off"]
#             is_hol = d in month_holidays
#             if not is_off and not is_hol:
#                 rec = rec_by_date.get(d)
#                 if rec and rec.status == "Present":
#                     total_p += 1
#                     if is_late(rec.in_time, cfg):
#                         late_count += 1
#                 else:
#                     total_a += 1
#             d += timedelta(days=1)
 
#         half_day_cuts = late_count // 3
#         late_deduction = round(half_day_cuts * 0.5, 2)
#         effective_present = round(max(0, total_p - late_deduction), 2)
 
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "data": {
#                 "employee": {
#                     "id": emp.id,
#                     "name": emp_display_name(emp),
#                     "code": emp_display_code(emp),
#                     "photo": emp.photo.url if getattr(emp, "photo", None) else None,
#                 },
#                 "today": {
#                     "date": today.isoformat(),
#                     "status": today_status,
#                     "in_time": today_rec.in_time.strftime("%I:%M %p") if today_rec and today_rec.in_time else "--",
#                     "out_time": today_rec.out_time.strftime("%I:%M %p") if today_rec and today_rec.out_time else "--",
#                 },
#                 # ✅ Setting table -la irundhu — office shift timing
#                 "shift": {
#                     "start": cfg["shift_start"].strftime("%I:%M %p"),
#                     "end": cfg["shift_end"].strftime("%I:%M %p"),
#                     "grace_minutes": cfg["grace_minutes"],
#                     "late_after": cfg["late_after"].strftime("%I:%M %p"),
#                     "weekly_off": sorted(DAY_KEYS[i] for i in cfg["weekly_off"]),
#                     # ✅ Fully dynamic — DB-la evlo weekly-off days irundhalum
#                     # (Sat, Sun, etc) automatic-a inga varum. .get() use pannuvom
#                     # so oru duplicate old get_attendance_config() irundhalum crash aagaadhu.
#                     "weekly_off_days": sorted(
#                         cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}
#                     ),
#                 },
#                 # ✅ Holiday table -la irundhu — indha maasam + next upcoming
#                 "holidays": {
#                     "this_month": [
#                         {"date": d2.isoformat(), "name": n}
#                         for d2, n in sorted(month_holidays.items())
#                     ],
#                     "next": (
#                         {"date": upcoming.date.isoformat(), "name": upcoming.name}
#                         if upcoming else None
#                     ),
#                 },
#                 "month_summary": {
#                     "month_label": today.strftime("%B %Y"),
#                     "total_present": total_p,
#                     "effective_present": effective_present,
#                     "total_absent": total_a,
#                     "late_count": late_count,
#                     "half_day_cuts": half_day_cuts,
#                     "late_deduction": late_deduction,
#                 },
#             },
#         }, status=status.HTTP_200_OK)


 
# # ══════════════════════════════════════════════
# #  EMPLOYEE ATTENDANCE — single employee, single day
# #  (used by EmployeeTodayAttendance.jsx)
# # ══════════════════════════════════════════════
# class EmployeeAttendanceAPIView(APIView):
#     """
#     GET /employee-attendance/?employee_id=<id>&date=YYYY-MM-DD
 
#     date illama kudutha, today edukkum.
#     Status: Present / Late / H (Holiday table) / W (Setting.weekly_off) / Absent / Not Checked In
#     """
 
#     def get(self, request):
#         emp_id = request.query_params.get("employee_id")
#         if not emp_id:
#             return Response(
#                 {"error": "employee_id is required"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         try:
#             emp = Employee.objects.get(id=emp_id)
#         except Employee.DoesNotExist:
#             return Response(
#                 {"error": "Employee not found"},
#                 status=status.HTTP_404_NOT_FOUND,
#             )
 
#         date_str = request.query_params.get("date")
#         try:
#             target_date = (
#                 datetime.strptime(date_str, "%Y-%m-%d").date()
#                 if date_str else date_cls.today()
#             )
#         except ValueError:
#             return Response(
#                 {"error": "Invalid date. Use YYYY-MM-DD"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         cfg = get_attendance_config()
#         holiday_map = get_holiday_map(target_date, target_date)  # ✅ Holiday table
#         hol_name = holiday_map.get(target_date)
#         is_weekly_off = target_date.weekday() in cfg["weekly_off"]  # ✅ Setting table
 
#         rec = Attendance.objects.filter(employee_id=emp.id, date=target_date).first()
 
#         late = False
#         sec = 0
#         if hol_name:
#             record_status = "H"
#         elif is_weekly_off:
#             record_status = "W"
#         elif rec and rec.status == "Present":
#             late = is_late(rec.in_time, cfg)
#             sec = ot_seconds(rec.out_time, cfg)
#             record_status = "Late" if late else "Present"
#         elif rec:
#             record_status = "Absent"
#         elif target_date > date_cls.today():
#             record_status = "--"
#         elif target_date < date_cls.today():
#             record_status = "Absent"
#         else:
#             record_status = "Not Checked In"
 
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "data": {
#                 "employee": {
#                     "id": emp.id,
#                     "name": emp_display_name(emp),
#                     "code": emp_display_code(emp),
#                 },
#                 "date": target_date.isoformat(),
#                 "record": {
#                     "status": record_status,
#                     "late": late,
#                     "in_time": rec.in_time.strftime("%I:%M %p") if rec and rec.in_time else "--",
#                     "out_time": rec.out_time.strftime("%I:%M %p") if rec and rec.out_time else "--",
#                     "ot_time": fmt_hhmm(sec // 60),
#                     "holiday_name": hol_name,
#                 },
#                 "is_holiday": bool(hol_name),
#                 "is_weekly_off": is_weekly_off,
#                 "config": {
#                     "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
#                     "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
#                     "grace_minutes": cfg["grace_minutes"],
#                     "late_after": cfg["late_after"].strftime("%I:%M %p"),
#                 },
#             },
#         }, status=status.HTTP_200_OK)
 
 
# # ══════════════════════════════════════════════
# #  EMPLOYEE MONTHLY ATTENDANCE — single employee, full month
# #  (used by EmployeeMonthlyAttendance.jsx)
# # ══════════════════════════════════════════════
# class EmployeeMonthlyAttendanceAPIView(APIView):
#     """
#     GET /employee-monthly-attendance/?employee_id=<id>&val=YYYY-MM
 
#     Same day-status rules as MonthattendanceAPIView, but scoped to
#     ONE employee and returns a single "row" object instead of a list.
#     """
 
#     def get(self, request):
#         emp_id = request.query_params.get("employee_id")
#         val = request.GET.get("val")
 
#         if not emp_id:
#             return Response(
#                 {"error": "employee_id is required"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
#         if not val:
#             return Response(
#                 {"error": "val is required. Use ?val=YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         try:
#             emp = Employee.objects.get(id=emp_id)
#         except Employee.DoesNotExist:
#             return Response(
#                 {"error": "Employee not found"},
#                 status=status.HTTP_404_NOT_FOUND,
#             )
 
#         try:
#             first_day = datetime.strptime(val + "-01", "%Y-%m-%d").date()
#         except ValueError:
#             return Response(
#                 {"error": "Invalid val format. Use YYYY-MM"},
#                 status=status.HTTP_400_BAD_REQUEST,
#             )
 
#         if first_day.month == 12:
#             last_day = first_day.replace(year=first_day.year + 1, month=1, day=1) - timedelta(days=1)
#         else:
#             last_day = first_day.replace(month=first_day.month + 1, day=1) - timedelta(days=1)
 
#         dates_in_month = [
#             first_day + timedelta(days=i) for i in range((last_day - first_day).days + 1)
#         ]
 
#         cfg = get_attendance_config()
#         holiday_map = get_holiday_map(first_day, last_day)  # ✅ Holiday table
 
#         emp_records = {
#             r.date: r
#             for r in Attendance.objects.filter(employee_id=emp.id, date__range=[first_day, last_day])
#         }
 
#         today = date_cls.today()
#         total_p = total_a = late_count = 0
#         total_ot_sec = 0
#         attendance_map = {}
 
#         for day in dates_in_month:
#             day_str = day.strftime("%Y-%m-%d")
#             ot_display = "00:00"
#             wd = day.weekday()
 
#             is_holiday = day in holiday_map            # ✅ Holiday table
#             is_weekly_off = wd in cfg["weekly_off"]     # ✅ Setting.weekly_off
 
#             if is_holiday:
#                 status_code = "H"
#             elif is_weekly_off:
#                 status_code = "W"
#             elif day > today:
#                 status_code = "--"
#             elif day in emp_records:
#                 rec = emp_records[day]
#                 if rec.status == "Present":
#                     status_code = "P"
#                     total_p += 1
#                     if is_late(rec.in_time, cfg):
#                         late_count += 1
#                     sec = ot_seconds(rec.out_time, cfg)
#                     if sec > 0:
#                         total_ot_sec += sec
#                         ot_display = fmt_hhmm(sec // 60)
#                 else:
#                     status_code = "A"
#                     total_a += 1
#             else:
#                 status_code = "A"
#                 total_a += 1
 
#             attendance_map[day_str] = {
#                 "status": status_code,
#                 "ot": ot_display,
#                 "holiday_name": holiday_map.get(day),
#             }
 
#         half_day_cuts = late_count // 3
#         late_deduction = round(half_day_cuts * 0.5, 2)
#         effective_present = round(max(0, total_p - late_deduction), 2)
 
#         total_ot_h = int(total_ot_sec // 3600)
#         total_ot_m = int((total_ot_sec % 3600) // 60)
 
#         return Response({
#             "status": {"code": status.HTTP_200_OK, "response": "success"},
#             "error": None,
#             "data": {
#                 "employee": {
#                     "id": emp.id,
#                     "name": emp_display_name(emp),
#                     "code": emp_display_code(emp),
#                 },
#                 "days": [
#                     {
#                         "date": d.strftime("%Y-%m-%d"),
#                         "day_num": d.day,
#                         "display": d.strftime("%d"),
#                         "day_name": d.strftime("%a"),
#                         "is_weekly_off": d.weekday() in cfg["weekly_off"],   # ✅
#                         "is_holiday": d in holiday_map,                      # ✅
#                         "holiday_name": holiday_map.get(d),
#                     }
#                     for d in dates_in_month
#                 ],
#                 "row": {
#                     "total_present": total_p,
#                     "effective_present": effective_present,
#                     "total_absent": total_a,
#                     "total_ot": f"{total_ot_h:02d}:{total_ot_m:02d}",
#                     "late_count": late_count,
#                     "half_day_cuts": half_day_cuts,
#                     "late_deduction": late_deduction,
#                     "attendance": attendance_map,
#                 },
#                 "config": {
#                     "shift_start": cfg["shift_start"].strftime("%I:%M %p"),
#                     "shift_end": cfg["shift_end"].strftime("%I:%M %p"),
#                     "grace_minutes": cfg["grace_minutes"],
#                     "late_after": cfg["late_after"].strftime("%I:%M %p"),
#                     "weekly_off": sorted(
#                         cfg.get("weekly_off_names") or {DAY_KEYS[i] for i in cfg["weekly_off"]}
#                     ),
#                 },
#                 "holidays_this_month": [
#                     {"date": d.strftime("%Y-%m-%d"), "name": n}
#                     for d, n in sorted(holiday_map.items())
#                 ],
#             },
#         }, status=status.HTTP_200_OK)
  
 
class AnnAPIView(APIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request, id=None):
        if id is not None:
            try:
                company = Announcement.objects.get(id=id)
                data = {
                    'id': company.id,
                    'date': company.date,
                    'description': company.description
                }

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })
        else:
            try:

                index = 0
                companies = []

                for company in Announcement.objects.all():
                    index += 1

                    companies.append({
                        'row': index,
                        'id': company.id,
                        'date': company.date,
                        'description': company.description,
                    })

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": companies
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })

    def post(self, request):
        data = request.data

        try:
            date=data["date"]
            description=data["description"]
            company = Announcement.objects.create(
                date=data["date"],
                description=data["description"],
                created_by=request.user
            )
            company.save()


            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Announcement created successfully"
            })
        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {e}"
            })

    def put(self, request, id):
        data = request.data

        try:

            date = data["date"]
            description = data["description"]
            company = Announcement.objects.get(id=id)
            company.date = data["date"]
            company.description = data["description"]
            company.save()




           

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": "Announcement updated successfully"
            })
        except ObjectDoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "No data found"
            })
        except Exception as e:
            print(e)
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Something went wrong"
            })

    def delete(self, request, id):
        if id is not None:
            try:
                company = Announcement.objects.get(id=id)
                company.delete()

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "message": "Department deleted successfully"
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": None,
                    "message": "No data found"
                })


class LeaveAPIView(APIView):
    permission_classes = (IsAuthenticated,)


   
    def get(self, request, id=None):
        if id is not None:
            try:
                company = LeaveRequest.objects.get(id=id)
               
                data = {
                    'id': company.id,
                    'name': company.type,
                    'description': company.reason,
                    'totaldays': company.total_days,
                    'leavedates': company.leave_dates,
                    'status' : company.status
                }

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })
        else:
            try:

                index = 0
                companies = []

                for company in LeaveRequest.objects.filter(created_by_id=request.user):
                    index += 1
                    tf = company.created_at  # tf is already a datetime object
                    # Format the datetime object to the desired string format
                    date_str = tf.strftime('%Y-%m-%d %H:%M:%S')

                    companies.append({
                        'row': index,
                        'id': company.id,
                        'type': company.type,
                        'reason': company.reason,
                        'td': company.total_days,
                        'date': date_str,
                        'status': company.status,
                        'why' : company.why,

                    })

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": companies
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })

    def post(self, request):
        data = request.data

        try:
            casual = data['name']
            man = data['totalday']
            leave_request = LeaveRequest(
            type=data['name'],
            reason=data['description'],
            total_days=data['totalday'],
            leave_dates=data['leavedate'],
            created_by=request.user
            )
            leave_request.save()


            jk = Profiles.objects.get(user_id=request.user)

            meesi = Profiles.objects.get(user_id=jk.authority_id)


            ro = meesi.email
        
            login_url = "https://pearlpackagingindia.com/manageleave"

            subject = "Leave request"
            body = f"""
            Hi am {jk.name},

            This is my leave request for {casual}.

            Total days: {man}

            

            You can log in using the following link and check:
            {login_url}

            

            
            """
            we = jk.email
            # Create the email message
            email = EmailMessage(
                subject=subject,
                body=body,
                from_email=settings.EMAIL_HOST_USER,
                to=[ro],
                # to=[fieldstaffemail],  # Uncomment and use this if you have a dynamic recipient
            )

            # Send the email
            email.send()


            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Leave request created successfully"
            })
        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {e}"
            })

    def put(self, request, id):
        data = request.data

        try:
            company = LeaveRequest.objects.get(id=id)
            company.type = data["name"]
            company.reason = data["description"]
            company.leave_dates = data["leavedate"]
            company.total_days = data["totalday"]

            company.save()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": "Department updated successfully"
            })
        except ObjectDoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "No data found"
            })
        except Exception as e:
            print(e)
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Something went wrong"
            })

    def delete(self, request, id):
        if id is not None:
            try:
                company = Department.objects.get(id=id)
                company.delete()

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "message": "Department deleted successfully"
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": None,
                    "message": "No data found"
                })


class ManageleaveAPIView(APIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request, id=None):
        if id is not None:
            try:

                


                company = LeaveRequest.objects.get(id=id)
               
                data = {
                    'id': company.id,
                    'name': company.type,
                    'description': company.reason,
                    'totaldays': int(company.total_days),
                    'leavedates': company.leave_dates,
                    'why': company.why,
                }

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })
        else:
            try:

                index = 0
                companies = []

                erp = Profiles.objects.filter(authority_id=request.user)
                for m in erp:

                    for company in LeaveRequest.objects.filter(created_by=m.user_id):
                        index += 1
                        tf = company.created_at  # tf is already a datetime object
                        # Format the datetime object to the desired string format
                        date_str = tf.strftime('%Y-%m-%d %H:%M:%S')

                        try:
                            user_id = company.created_by_id
                            val = User.objects.get(id=user_id)
                            username = val.first_name + ' ' + val.last_name
                        except (ValueError, User.DoesNotExist):
                            username = "Unknown"

                        companies.append({
                            'row': index,
                            'id': company.id,
                            'type': company.type,
                            'reason': company.reason,
                            'td': company.total_days,
                            'date': date_str,
                            'status': company.status,
                            'username': username

                        })

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": companies
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })

    
    def put(self, request, id):
        data = request.data

        try:
            # Attempt to convert totalday to float, or set a default value if None
            try:
                rto = float(data["totalday"]) if data["totalday"] is not None else 0.0
            except (ValueError, TypeError):
                rto = 0.0

            sta = data.get("status")
            ml = data.get("totalday", 0.0)
            names = data.get("name", "")

            if sta == '2':
                we = "Approved"
            elif sta == '3':
                we = "Rejected"
            else:
                we = "Pending"

            try:
                company = LeaveRequest.objects.get(id=id)
                company.type = names
                company.reason = data.get("description", "")
                company.leave_dates = data.get("leavedate", "")
                company.total_days = ml
                company.status = sta
                company.why = data.get("why", "")

                company.save()

                if sta == '2':
                    try:
                        pr = Profiles.objects.get(user_id=company.created_by_id)
                        if pr.total_leave is None:
                            pr.total_leave = 0  # Set to 0 if None
                        
                        # Ensure total_leave is treated as an integer
                            pr.total_leave = int(pr.total_leave)
                            pr.save()
                    except Profiles.DoesNotExist:
                        # Handle case where Profiles object doesn't exist
                        print(f"Profile for user ID {company.created_by_id} does not exist.")
                        pass

            except LeaveRequest.DoesNotExist:
                # Handle case where LeaveRequest object doesn't exist
                print(f"LeaveRequest with ID {id} does not exist.")
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found"
                })

            # Send an email notification
            try:
                jk = Profiles.objects.get(user_id=request.user)
                pr = Profiles.objects.get(user_id=company.created_by_id)

                ro = jk.email
                login_url = "https://pearlpackagingindia.com/leaverequest"

                subject = "Leave request"
                body = f"""
                Your leave request {names} is {we}.
                Total days: {ml}

                You can log in using the following link and check:
                {login_url}
                """

                recipient_email = pr.email
                print(recipient_email, 'Sending email to recipient')

                email = EmailMessage(
                    subject=subject,
                    body=body,
                    from_email=settings.EMAIL_HOST_USER,
                    to=[recipient_email],
                )

                email.send()

            except Profiles.DoesNotExist:
                print(f"Profile for user ID {company.created_by_id} does not exist.")
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "Profile not found for email notification"
                })

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": "Request updated successfully"
            })

        except ObjectDoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "No data found"
            })

        except Exception as e:
            print(f"Error occurred: {e}")
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Something went wrong"
            })
        

# from datetime import datetime, time
# class TodayAttendanceAPIView(APIView):
#     def get(self, request):
#         # 1. à®¤à¯‡à®¤à®¿à®¯à¯ˆ à®ªà¯†à®±à¯à®¤à®²à¯ (Default: Today)
#         date_str = request.query_params.get('date', None)
#         if date_str:
#             target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
#         else:
#             target_date = datetime.now().date()

#         # 2. à®¨à¯‡à®° à®µà®°à¯ˆà®¯à®±à¯ˆà®•à®³à¯
#         office_start_time = time(8, 30, 0)  # 8:30 AM

#         # 6:00 PM-à® 5:00 PM (17:00) à®†à®• à®®à®¾à®±à¯à®±à¯à®•à®¿à®±à¯‹à®®à¯   
#         office_end_time = time(17, 0, 0)    # 5:00 PM

#         # 3. à®µà®¨à¯à®¤à®µà®°à¯à®•à®³à¯ à®ªà®Ÿà¯à®Ÿà®¿à®¯à®²à¯ (Present List)
#         attendance_records = Attendance.objects.filter(date=target_date).select_related('employee')
#         present_list = []

#         for record in attendance_records:
#             entry_status = "On Time"
#             overtime = "0:00"

#             # Late Calculation
#             if record.in_time and record.in_time > office_start_time:
#                 entry_status = "Late"
#             elif record.in_time and record.in_time <= office_start_time:
#                 entry_status = "Early/On-Time"

#             # OT Calculation (à®®à®¾à®²à¯ˆ 6 à®®à®£à®¿à®•à¯à®•à¯ à®®à¯‡à®²à¯ à®‡à®°à¯à®¨à¯à®¤à®¾à®²à¯)
#             if record.out_time and record.out_time > office_end_time:
#                 # à®¨à¯‡à®° à®µà®¿à®¤à¯à®¤à®¿à®¯à®¾à®šà®¤à¯à®¤à¯ˆ à®•à®£à®•à¯à®•à®¿à®Ÿà¯à®¤à®²à¯
#                 dummy_today = datetime.today()
#                 dt_out = datetime.combine(dummy_today, record.out_time)
#                 dt_off = datetime.combine(dummy_today, office_end_time)
#                 diff = dt_out - dt_off
#                 hours, remainder = divmod(diff.seconds, 3600)
#                 minutes, _ = divmod(remainder, 60)
#                 overtime = f"{hours}h {minutes}m"

#             present_list.append({
#                 "id": record.employee.id,
#                 "name": record.employee.name,
#                 "photo": record.employee.photo.url if record.employee.photo else None,
#                 "in_time": record.in_time.strftime('%I:%M %p') if record.in_time else "--",
#                 "out_time": record.out_time.strftime('%I:%M %p') if record.out_time else "--",
#                 "entry_status": entry_status,
#                 "ot_time": overtime
#             })

#         # 4. à®µà®°à®¾à®¤à®µà®°à¯à®•à®³à¯ à®ªà®Ÿà¯à®Ÿà®¿à®¯à®²à¯ (Absent List)
#         present_ids = attendance_records.values_list('employee_id', flat=True)
#         absent_employees = Employee.objects.exclude(id__in=present_ids)
        
#         absent_list = [{
#             "id": emp.id,
#             "name": emp.name,
#             "photo": emp.photo.url if emp.photo else None
#         } for emp in absent_employees]

#         return Response({
#             "date": target_date,
#             "present_count": len(present_list),
#             "absent_count": len(absent_list),
#             "present_list": present_list,
#             "absent_list": absent_list
#         }, status=status.HTTP_200_OK)




class FpAPIView(APIView):

    

    def post(self, request):
        data = request.data

        try:
            
            params = data.get('params', {})

            # Now retrieve 'email' and 'password' from the 'params' dictionary
            email = params.get('email')  # Access the 'email' field
            password = params.get('password')  # Access the 'password' field


            # Hash the password
            hashed_password = make_password(password)

            man = User.objects.get(email=email)

            man.password = hashed_password

            man.save()



            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": "Password reset successfully"
            })
        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {e}"
            })
import calendar
from datetime import datetime, time, timedelta, date as date_cls

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response

from .models import Setting, Holiday, Employee, Attendance


DAY_KEYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]  # weekday(): 0=Mon..6=Sun


def fmt_hhmm(total_minutes):
    total_minutes = max(0, int(total_minutes))
    return f"{total_minutes // 60:02d}:{total_minutes % 60:02d}"


def get_attendance_config():
    setting, _ = Setting.objects.get_or_create(pk=1)
    shift_start = setting.shift_start or time(9, 30)
    shift_end = setting.shift_end or time(18, 30)
    grace = int(setting.grace_minutes or 0)

    off_names = {
        p.strip()[:3].title()
        for p in (setting.weekly_off or "").split(",")
        if p.strip()
    }
    weekly_off = {i for i, nm in enumerate(DAY_KEYS) if nm in off_names}

    late_after = (
        datetime.combine(date_cls.today(), shift_start) + timedelta(minutes=grace)
    ).time()

    return {
        "setting": setting,
        "shift_start": shift_start,
        "shift_end": shift_end,
        "grace_minutes": grace,
        "late_after": late_after,
        "weekly_off": weekly_off,
    }


def is_late(in_time, cfg):
    return bool(in_time and in_time > cfg["late_after"])


def ot_minutes(out_time, cfg):
    if not out_time or out_time <= cfg["shift_end"]:
        return 0
    d = date_cls.today()
    diff = datetime.combine(d, out_time) - datetime.combine(d, cfg["shift_end"])
    return int(diff.total_seconds() // 60)

 
class SalaryAPIView(APIView):

    def get(self, request, id=None):
        if id is not None:
            try:
                company = Department.objects.get(id=id)
                data = {
                    'id': company.id,
                    'name': company.name,
                    'description': company.description
                }

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except ObjectDoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "No data found",
                })
        else:
            try:
                now = datetime.now()
                month = int(request.query_params.get('month', now.month))
                year  = int(request.query_params.get('year',  now.year))

                total_days = calendar.monthrange(year, month)[1]

                # âœ… à®…à®¨à¯à®¤ month-à®‡à®©à¯ date range
                from datetime import date
                month_start = date(year, month, 1)
                month_end   = date(year, month, total_days)

                # âœ… Late threshold â€” 09:00 AM
                LATE_TIME = time(9, 0, 0)

                # âœ… Late deduction rule: 3 lates = 1 day cut
                LATES_PER_DAY_CUT = 3

                index = 0
                companies = []

                for company in Employee.objects.all():
                    index += 1

                    # â”€â”€ 1. à®…à®¨à¯à®¤ employee-à®‡à®©à¯ à®…à®¨à¯à®¤ month attendance à®Žà®Ÿà¯à®•à¯à®•à®¿à®±à¯‹à®®à¯ â”€â”€
                    attendance_qs = Attendance.objects.filter(
                        employee_id=company.id,
                        date__gte=month_start,
                        date__lte=month_end,
                    )

                    # â”€â”€ 2. Present days count â”€â”€
                    present_days = attendance_qs.filter(status='Present').count()

                    # â”€â”€ 3. Late count â€” in_time > 09:00:00 â”€â”€
                    late_count = 0
                    for att in attendance_qs.filter(status='Present'):
                        if att.in_time and att.in_time > LATE_TIME:
                            late_count += 1

                    # â”€â”€ 4. Late deduction days calculate â”€â”€
                    # 3 lates = 1 day cut, 6 lates = 2 days cut

                    late_deduction_days = (late_count // 3) * 0.5   # âœ… float: 0.5, 1.0, 1.5...


                    working_days = max(0.0, present_days - late_deduction_days) 


                    companies.append({
                        'row':           index,
                        'id':            company.id,
                        'name':          company.name,
                        'pan':           company.pan,
                        'ctc':           float(company.ctc),
                        'grossSalary':   float(company.grossSalary),
                        'basic':         float(company.basic),
                        'hra':           float(company.hra),
                        'conveyance':    float(company.conveyance),
                        'otherAllowance':float(company.otherAllowance),
                        'totalDays':     total_days,
                        'workingDays':   working_days,       # âœ… auto calculated
                        'presentDays':   present_days,       # âœ… info only
                        'lateCount':     late_count,         # âœ… frontend-à®²à¯ à®•à®¾à®Ÿà¯à®Ÿà®²à®¾à®®à¯
                        'lateDeduction': late_deduction_days,# âœ… frontend-à®²à¯ à®•à®¾à®Ÿà¯à®Ÿà®²à®¾à®®à¯
                        'fixedPT':       float(company.fixedPT),
                        'pfLimit':       float(company.pfLimit),
                        'advance':       float(company.advance),
                        'ot':            float(company.ot),
                        'reimbursement': float(company.reimbursement),
                        'incentive':     float(company.incentive),
                        'month':         month,
                        'year':          year,
                    })


                    print(companies)

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": companies
                })

            except Exception as e:
                print(f"Error: {e}")
                return Response({
                    "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                    "error": True,
                    "message": str(e)
                })
    def put(self, request, id):
        data = request.data

        try:
            # ID-à®¯à¯ˆ à®µà¯ˆà®¤à¯à®¤à¯ à®šà®°à®¿à®¯à®¾à®© à®Žà®®à¯à®ªà¯à®³à®¾à®¯à®¿à®¯à¯ˆ à®Žà®Ÿà¯à®•à¯à®•à®¿à®±à¯‹à®®à¯
            employee = Employee.objects.get(id=id)
            
            # --- à®ªà¯‡à®šà®¿à®•à¯ à®‡à®©à¯à®ªà¯‹ à®…à®ªà¯à®Ÿà¯‡à®Ÿà¯ ---
            employee.pan = data.get("pan", employee.pan)
            
            # --- à®šà¯‡à®²à®°à®¿ à®®à®±à¯à®±à¯à®®à¯ à®…à®Ÿà¯à®Ÿà¯†à®£à¯à®Ÿà®©à¯à®¸à¯ à®Ÿà¯‡à®Ÿà¯à®Ÿà®¾ à®…à®ªà¯à®Ÿà¯‡à®Ÿà¯ ---
            # data.get() à®ªà®¯à®©à¯à®ªà®Ÿà¯à®¤à¯à®¤à¯à®µà®¤à®¾à®²à¯, à®’à®°à¯à®µà¯‡à®³à¯ˆ à®Ÿà¯‡à®Ÿà¯à®Ÿà®¾ à®µà®°à®¾à®µà®¿à®Ÿà¯à®Ÿà®¾à®²à¯à®®à¯ à®ªà®´à¯ˆà®¯ à®µà¯‡à®²à¯à®¯à¯‚ à®…à®ªà¯à®ªà®Ÿà®¿à®¯à¯‡ à®‡à®°à¯à®•à¯à®•à¯à®®à¯
            employee.ctc = data.get("ctc", employee.ctc)
            employee.grossSalary = data.get("grossSalary", employee.grossSalary)
            employee.basic = data.get("basic", employee.basic)
            employee.hra = data.get("hra", employee.hra)
            employee.conveyance = data.get("conveyance", employee.conveyance)
            employee.otherAllowance = data.get("otherAllowance", employee.otherAllowance)
            
            # employee.totalDays = data.get("totalDays", employee.totalDays)
            # employee.workingDays = data.get("workingDays", employee.workingDays)
            
            # --- à®•à®´à®¿à®µà¯à®•à®³à¯ à®®à®±à¯à®±à¯à®®à¯ à®‡à®¤à®° à®µà¯‡à®²à¯à®¯à¯‚à®•à¯à®•à®³à¯ ---
            employee.fixedPT = data.get("fixedPT", employee.fixedPT)
            employee.pfLimit = data.get("pfLimit", employee.pfLimit)
            employee.advance = data.get("advance", employee.advance)
            employee.ot = data.get("ot", employee.ot)
            employee.reimbursement = data.get("reimbursement", employee.reimbursement)
            employee.incentive = data.get("incentive", employee.incentive)

            # à®Ÿà¯‡à®Ÿà¯à®Ÿà®¾à®ªà¯‡à®¸à®¿à®²à¯ à®šà¯‡à®®à®¿à®•à¯à®•à®¿à®±à¯‹à®®à¯
            employee.save()

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": f"Employee {employee.name} updated successfully"
            })

        except Employee.DoesNotExist:
            return Response({
                "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                "error": True,
                "message": "Employee not found"
            })
        except Exception as e:
            print(f"Error: {e}")
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": "Something went wrong during update"
            })


class PayslipAPIView(APIView):
    # permission_classes = [IsAuthenticated]

    def get(self, request, id=None):
        if id is not None:
            try:
                company = Payslip.objects.get(id=id)
                data = {
                    'id':               company.id,
                    'empname':          company.empname,
                    'empid':            company.empid,
                    'emppan':           company.emppan,
                    'ctc':              company.ctc,
                    'grosssalary':      company.grosssalary,
                    'perdaysalary':     company.perdaysalary,
                    'basicsalary':      company.basicsalary,
                    'hra':              company.hra,
                    'conveyance':       company.conveyance,
                    'otherallowance':   company.otherallowance,
                    'totaldays':        company.totaldays,
                    'workingdays':      company.workingdays,
                    'baiscnew':         company.baiscnew,
                    'hranew':           company.hranew,
                    'conveyancenew':    company.conveyancenew,
                    'otherallowancenew':company.otherallowancenew,
                    'grosspay':         company.grosspay,
                    'variablepfgross':  company.variablepfgross,
                    'pfless':           company.pfless,
                    'esiless':          company.esiless,
                    'lesspt':           company.lesspt,
                    'pf':               company.pf,
                    'esi':              company.esi,
                    'bank':             company.bank,
                    'uan':              company.uan,
                    'advance':          company.advance,
                    'netpay':           company.netpay,
                    'ot':               company.ot,
                    'reimbursement':    company.reimbursement,
                    'incentive':        company.incentive,
                    'topay':            company.topay,
                    'month':            company.month,
                }
                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": data
                })
            except Payslip.DoesNotExist:
                return Response({
                    "status": {"code": status.HTTP_404_NOT_FOUND, "response": "failed"},
                    "error": True,
                    "message": "Payslip not found",
                })
        else:
            try:
             

                index = 0
                companies = []

                for company in Payslip.objects.all():
                    index += 1

             
                    companies.append({
                        'row':           index,
                        'id':            company.id,
                        'name':          company.empname,
                        'topay':           company.topay,
                        'month':           company.month,

                       
                    })


                    print(companies)

                return Response({
                    "status": {"code": status.HTTP_200_OK, "response": "success"},
                    "error": None,
                    "data": companies
                })

            except Exception as e:
                print(f"Error: {e}")
                return Response({
                    "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                    "error": True,
                    "message": str(e)
                })

    def post(self, request):
        try:
            payslips_data = request.data.get("payslips", [])

            if not payslips_data:
                return Response({
                    "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                    "error": True,
                    "message": "Payslip data à®‡à®²à¯à®²à¯ˆ"
                })

            created_list = []
            updated_list = []


            MONTH_NAMES = {
                1: "January", 2: "February", 3: "March",
                4: "April",   5: "May",      6: "June",
                7: "July",    8: "August",   9: "September",
                10: "October",11: "November",12: "December"
            }

            for p in payslips_data:
                month_val = p.get("month")
                emp_id    = p.get("employee")

                # âœ… FIX: Employee instance 
                month_name = MONTH_NAMES.get(int(month_val), str(month_val))

                try:
                    employee_instance = Employee.objects.get(id=emp_id)
                except Employee.DoesNotExist:
                    continue  # à®‡à®²à¯à®²à®¾à®¤ employee skip à®ªà®£à¯à®±à¯‹à®®à¯

                existing = Payslip.objects.filter(
                    employee=employee_instance,
                    month=month_name
                ).first()

               

                if existing:
                    # âœ… UPDATE â€” field by field safe à®†à®• à®šà¯†à®Ÿà¯ à®ªà®£à¯à®±à¯‹à®®à¯
                    existing.empname           = p.get("empname", existing.empname)
                    existing.empid             = p.get("empid", existing.empid)
                    existing.emppan            = p.get("emppan", existing.emppan)
                    existing.ctc               = p.get("ctc", existing.ctc)
                    existing.grosssalary       = p.get("grosssalary", existing.grosssalary)
                    existing.perdaysalary      = p.get("perdaysalary", existing.perdaysalary)
                    existing.basicsalary       = p.get("basicsalary", existing.basicsalary)
                    existing.hra               = p.get("hra", existing.hra)
                    existing.conveyance        = p.get("conveyance", existing.conveyance)
                    existing.otherallowance    = p.get("otherallowance", existing.otherallowance)
                    existing.totaldays         = p.get("totaldays", existing.totaldays)
                    existing.workingdays       = p.get("workingdays", existing.workingdays)
                    existing.baiscnew          = p.get("baiscnew", existing.baiscnew)
                    existing.hranew            = p.get("hranew", existing.hranew)
                    existing.conveyancenew     = p.get("conveyancenew", existing.conveyancenew)
                    existing.otherallowancenew = p.get("otherallowancenew", existing.otherallowancenew)
                    existing.grosspay          = p.get("grosspay", existing.grosspay)
                    existing.variablepfgross   = p.get("variablepfgross", existing.variablepfgross)
                    existing.pfless            = p.get("pfless", existing.pfless)
                    existing.esiless           = p.get("esiless", existing.esiless)
                    existing.lesspt            = p.get("lesspt", existing.lesspt)
                    existing.pf                = p.get("pf", existing.pf)
                    existing.esi               = p.get("esi", existing.esi)
                    existing.bank              = p.get("bank", existing.bank)
                    existing.uan               = p.get("uan", existing.uan)
                    existing.advance           = p.get("advance", existing.advance)
                    existing.netpay            = p.get("netpay", existing.netpay)
                    existing.ot                = p.get("ot", existing.ot)
                    existing.reimbursement     = p.get("reimbursement", existing.reimbursement)
                    existing.incentive         = p.get("incentive", existing.incentive)
                    existing.topay             = p.get("topay", existing.topay)
                    existing.month = month_name  # âœ… "March" store à®†à®•à¯à®®à¯

                    existing.status            = 1
                    existing.save()
                    updated_list.append(emp_id)
                else:
                    # âœ… CREATE â€” employee instance pass à®ªà®£à¯à®±à¯‹à®®à¯
                    Payslip.objects.create(
                        employee         = employee_instance,  # âœ… instance
                        empname          = p.get("empname", ""),
                        empid            = p.get("empid", ""),
                        emppan           = p.get("emppan", ""),
                        ctc              = p.get("ctc", "0"),
                        grosssalary      = p.get("grosssalary", "0"),
                        perdaysalary     = p.get("perdaysalary", "0"),
                        basicsalary      = p.get("basicsalary", "0"),
                        hra              = p.get("hra", "0"),
                        conveyance       = p.get("conveyance", "0"),
                        otherallowance   = p.get("otherallowance", "0"),
                        totaldays        = p.get("totaldays", "0"),
                        workingdays      = p.get("workingdays", "0"),
                        baiscnew         = p.get("baiscnew", "0"),
                        hranew           = p.get("hranew", "0"),
                        conveyancenew    = p.get("conveyancenew", "0"),
                        otherallowancenew= p.get("otherallowancenew", "0"),
                        grosspay         = p.get("grosspay", "0"),
                        variablepfgross  = p.get("variablepfgross", "0"),
                        pfless           = p.get("pfless", "0"),
                        esiless          = p.get("esiless", "0"),
                        lesspt           = p.get("lesspt", "0"),
                        pf               = p.get("pf", "0"),
                        esi              = p.get("esi", "0"),
                        bank             = p.get("bank", ""),
                        uan              = p.get("uan", ""),
                        advance          = p.get("advance", "0"),
                        netpay           = p.get("netpay", "0"),
                        ot               = p.get("ot", "0"),
                        reimbursement    = p.get("reimbursement", "0"),
                        incentive        = p.get("incentive", "0"),
                        topay            = p.get("topay", "0"),
                        month    = month_name,       # âœ… "March" store à®†à®•à¯à®®à¯

                        status           = 1,
                    )
                    created_list.append(emp_id)

            return Response({
                "status": {"code": status.HTTP_200_OK, "response": "success"},
                "error": None,
                "message": f"{len(created_list)} created, {len(updated_list)} updated",
                "created": len(created_list),
                "updated": len(updated_list),
            })

        except Exception as e:
            print(f"Payslip Error: {e}")
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": str(e)
            })

class ProductactivestatusupdateAPIView(APIView):
    
    def put(self, request, id):
        

        try:
            


            ven = Product.objects.get(id=id)

            if ven.status == 1:
                ven.status = 2
                ven.save()

            else:
                ven.status = 1
                ven.save()
            

           

            



           
    
            return Response({
                "status": {"code": status.HTTP_201_CREATED, "response": "success"},
                "error": None,
                "message": "Quote accepted successfully"
            })
        except KeyError as e:
            return Response({
                "status": {"code": status.HTTP_400_BAD_REQUEST, "response": "failed"},
                "error": True,
                "message": f"Missing required field: {e}"
            })

# ===========================================================================
import io
import os
from datetime import date, datetime, timedelta

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.paginator import Paginator
from django.db import IntegrityError, transaction
from django.db.models import Count, Prefetch, ProtectedError, Q
from django.http import FileResponse, HttpResponse
from django.utils import timezone
from rest_framework import status
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.exceptions import TokenError
from rest_framework_simplejwt.tokens import RefreshToken

from .models import (DOC_TYPE_CHOICES, STATUS_ACTIVE, STATUS_INACTIVE, Client, ClientDocument,
                     ClientService, Profiles, RenewalHistory, Service)

ADMIN_ROLE = 1          # Profiles.role
EMPLOYEE_ROLE = 3
PAGE_SIZE = 10          # rows per page
MAX_PAGE_SIZE = 50
EXPORT_LIMIT = 5000
MAX_UPLOAD_MB = 5
MAX_DOCS_PER_CLIENT = 30


# ============================== small helpers ==============================
def today_date():
    """Today's date. Works whether settings.USE_TZ is True or False."""
    return timezone.localdate() if settings.USE_TZ else date.today()


def now_local():
    """Current date and time. Works whether settings.USE_TZ is True or False."""
    return timezone.localtime() if settings.USE_TZ else datetime.now()


def success(data=None, message=None, code=status.HTTP_200_OK, **extra):
    body = {"status": {"code": code, "response": "success"}, "error": None, "data": data}
    if message:
        body["message"] = message
    body.update(extra)
    return Response(body, status=code)


def failed(message, code=status.HTTP_400_BAD_REQUEST, errors=None):
    body = {"status": {"code": code, "response": "failed"}, "error": True, "message": message}
    if errors:
        body["errors"] = errors
    return Response(body, status=code)


def get_role(user):
    if user.is_superuser:
        return ADMIN_ROLE
    return Profiles.objects.filter(user_id=user.id).values_list("role", flat=True).first()


def check_role(request, admin_only=False):
    """Returns an error Response if this user may not use the API, otherwise None."""
    allowed = (ADMIN_ROLE,) if admin_only else (ADMIN_ROLE, EMPLOYEE_ROLE)
    if get_role(request.user) not in allowed:
        return failed("You do not have permission to do this.", status.HTTP_403_FORBIDDEN)
    return None


def paginate(request, queryset):
    """?page=2&page_size=10  ->  (rows of that page, pagination info)"""
    try:
        page = max(int(request.query_params.get("page", 1)), 1)
    except (TypeError, ValueError):
        page = 1
    try:
        size = int(request.query_params.get("page_size", PAGE_SIZE))
    except (TypeError, ValueError):
        size = PAGE_SIZE
    size = min(max(size, 1), MAX_PAGE_SIZE)

    paginator = Paginator(queryset, size)
    page_obj = paginator.get_page(page)          # page too high -> last page
    info = {
        "count": paginator.count,
        "page": page_obj.number,
        "page_size": size,
        "total_pages": paginator.num_pages,
        "has_next": page_obj.has_next(),
        "has_previous": page_obj.has_previous(),
    }
    return page_obj.object_list, info


def error_dict(exc):
    """Django ValidationError -> {"field": "first message"}"""
    if hasattr(exc, "message_dict"):
        return {k: ("This field is required." if v and v[0] == "This field cannot be blank." else (v[0] if v else "Invalid value"))
                for k, v in exc.message_dict.items()}
    return {"__all__": "; ".join(exc.messages)}


def first_error(errors):
    return next(iter(errors.values()))


def to_bool(value):
    return value in (True, 1, "1", "true", "True", "yes", "Yes")


def fmt_date(d):
    return d.strftime("%d-%m-%Y") if d else ""


def mask_aadhaar(v):
    return f"XXXX XXXX {v[-4:]}" if v else ""


def mask_account(v):
    return f"{'X' * max(len(v) - 4, 0)}{v[-4:]}" if v else ""


# ============================== token helpers ==============================
class MeAPIView(APIView):
    """GET /api/v1/me/  -> who owns this token + role (checked on the server)."""
    permission_classes = (IsAuthenticated,)

    def get(self, request):
        user = request.user
        profile = Profiles.objects.filter(user_id=user.id).first()
        if profile is not None and profile.status == 2:
            return failed("Your account is inactive. Please contact Admin.", status.HTTP_403_FORBIDDEN)
        return success({
            "id": user.id,
            "name": user.username,            # same key names as the login response
            "username": user.first_name,
            "email": user.email,
            "isSuper": user.is_superuser,
            "phone": profile.phone if profile else None,
            "role": profile.role if profile else None,
            "geostatus": profile.geostatus if profile else 2,
        })


class LogoutAPIView(APIView):
    """POST /api/v1/logout/  {"refresh": "..."}  -> blacklists the refresh token."""
    permission_classes = (IsAuthenticated,)

    def post(self, request):
        try:
            RefreshToken(request.data.get("refresh")).blacklist()
        except TokenError:
            pass                                   # already invalid = already logged out
        return Response(status=status.HTTP_205_RESET_CONTENT)


# ============================== SERVICE MASTER ==============================
def service_to_dict(s):
    return {
        "id": s.id,
        "name": s.name,
        "status": s.status,
        "clients_count": getattr(s, "clients_count", None),
        "created_at": fmt_date(s.created_at),
    }


class ServiceAPIView(APIView):
    """
    GET    /api/v1/services/              list, 10 per page   (?page=&search=&status=)
    GET    /api/v1/services/?val=options  all services for dropdowns
    GET    /api/v1/services/?val=summary  counts for dashboard
    GET    /api/v1/services/<id>/         one service
    POST   /api/v1/services/              create   (admin)
    PUT    /api/v1/services/<id>/         update   (admin)
    DELETE /api/v1/services/<id>/         delete   (admin)
    """
    permission_classes = (IsAuthenticated,)

    def get(self, request, id=None):
        denied = check_role(request)
        if denied:
            return denied

        if id is not None:
            try:
                s = Service.objects.annotate(clients_count=Count("client_services")).get(id=id)
            except Service.DoesNotExist:
                return failed("Service not found", status.HTTP_404_NOT_FOUND)
            return success(service_to_dict(s))

        val = request.query_params.get("val", "")
        if val == "options":
            rows = Service.objects.order_by("name").values("id", "name", "status")
            return success([{"value": r["id"], "label": r["name"], "inactive": r["status"] == STATUS_INACTIVE} for r in rows])
        if val == "summary":
            total = Service.objects.count()
            active = Service.objects.filter(status=STATUS_ACTIVE).count()
            return success({"total": total, "active": active, "inactive": total - active})

        services = Service.objects.annotate(clients_count=Count("client_services")).order_by("name")
        search = request.query_params.get("search", "").strip()
        status_param = request.query_params.get("status", "").strip()
        if search:
            services = services.filter(name__icontains=search)
        if status_param in ("1", "2"):
            services = services.filter(status=int(status_param))

        rows, info = paginate(request, services)
        return success([service_to_dict(s) for s in rows], pagination=info)

    def _save(self, request, service, creating):
        data = request.data
        if "name" in data:
            service.name = data["name"]
        if "status" in data:
            service.status = data["status"]

        try:
            service.name = (service.name or "").strip()
            service.clean_fields()
        except ValidationError as e:
            errors = error_dict(e)
            return failed(first_error(errors), errors=errors)

        if Service.objects.filter(name__iexact=service.name).exclude(pk=service.pk).exists():
            msg = "A service with this name already exists."
            return failed(msg, errors={"name": msg})

        if creating:
            service.created_by = request.user
        service.updated_by = request.user
        try:
            service.save()
        except IntegrityError:
            return failed("A service with this name already exists.", status.HTTP_409_CONFLICT)

        service = Service.objects.annotate(clients_count=Count("client_services")).get(pk=service.pk)
        return success(
            service_to_dict(service),
            message="Service created successfully" if creating else "Service updated successfully",
            code=status.HTTP_201_CREATED if creating else status.HTTP_200_OK,
        )

    def post(self, request):
        denied = check_role(request, admin_only=True)
        if denied:
            return denied
        return self._save(request, Service(), creating=True)

    def put(self, request, id):
        denied = check_role(request, admin_only=True)
        if denied:
            return denied
        try:
            service = Service.objects.get(id=id)
        except Service.DoesNotExist:
            return failed("Service not found", status.HTTP_404_NOT_FOUND)
        return self._save(request, service, creating=False)

    def delete(self, request, id):
        denied = check_role(request, admin_only=True)
        if denied:
            return denied
        try:
            Service.objects.get(id=id).delete()
        except Service.DoesNotExist:
            return failed("Service not found", status.HTTP_404_NOT_FOUND)
        except ProtectedError:
            return failed("This service is used by clients and can't be deleted. Mark it Inactive instead.", status.HTTP_409_CONFLICT)
        return success(message="Service deleted successfully")


# ============================== CLIENT MASTER ==============================
CLIENT_TEXT_FIELDS = (
    "name", "business_name", "address", "city", "pincode", "phone", "email",
    "pan", "aadhaar", "gstin", "bank_name", "account_holder", "account_number", "ifsc",
    "status", "notes",
)


def document_to_dict(d):
    return {
        "id": d.id,
        "doc_type": d.doc_type,
        "doc_type_label": d.get_doc_type_display(),
        "original_name": d.original_name,
        "content_type": d.content_type,
        "size": d.size,
        "remarks": d.remarks,
        "uploaded_at": fmt_date(d.created_at),
    }


def client_queryset():
    """Clients with services + documents loaded in 2 extra queries (no N+1)."""
    return Client.objects.prefetch_related(
        Prefetch("client_services", queryset=ClientService.objects.select_related("service")),
        "documents",
    )


def client_to_dict(c, full=False):
    """List + exports use masked Aadhaar / account number. full=True is only for the edit form."""
    links = sorted(c.client_services.all(), key=lambda cs: cs.service.name.lower())
    return {
        "id": c.id,
        "client_code": c.client_code,
        "name": c.name,
        "business_name": c.business_name,
        "address": c.address,
        "city": c.city,
        "pincode": c.pincode,
        "phone": c.phone,
        "email": c.email,
        "pan": c.pan,
        "gstin": c.gstin,
        "aadhaar": c.aadhaar if full else mask_aadhaar(c.aadhaar),
        "bank_name": c.bank_name,
        "account_holder": c.account_holder,
        "account_number": c.account_number if full else mask_account(c.account_number),
        "ifsc": c.ifsc,
        "services": [
            {
                "id": cs.service_id,
                "name": cs.service.name,
                "renewal_date": cs.renewal_date.isoformat() if cs.renewal_date else None,
                "renewal_label": fmt_date(cs.renewal_date),
            }
            for cs in links
        ],
        "file_delivered": c.file_delivered,
        "file_delivered_on": fmt_date(c.file_delivered_on),
        "documents_count": len(c.documents.all()),
        "documents": [document_to_dict(d) for d in c.documents.all()] if full else [],
        "status": c.status,
        "notes": c.notes,
        "created_at": fmt_date(c.created_at),
        "updated_at": fmt_date(c.updated_at),
    }


def filtered_clients(request):
    clients = client_queryset().order_by("-id")
    p = request.query_params
    search = p.get("search", "").strip()
    status_param = p.get("status", "").strip()
    service = p.get("service", "").strip()
    delivered = p.get("file_delivered", "").strip()

    if status_param in ("1", "2"):
        clients = clients.filter(status=int(status_param))
    if delivered in ("0", "1"):
        clients = clients.filter(file_delivered=(delivered == "1"))
    if service.isdigit():
        clients = clients.filter(client_services__service_id=int(service))
    if search:
        clients = clients.filter(
            Q(name__icontains=search) | Q(business_name__icontains=search) | Q(client_code__icontains=search)
            | Q(phone__icontains=search) | Q(email__icontains=search) | Q(pan__icontains=search)
            | Q(gstin__icontains=search) | Q(city__icontains=search)
        )
    return clients


def parse_service_rows(raw):
    """[{"service_id": 3, "renewal_date": "2026-10-10"}, ...]  ->  [(3, date(2026,10,10)), ...]"""
    if raw in (None, ""):
        return []
    if not isinstance(raw, list):
        raise ValidationError({"services": "Services must be a list."})
    rows, seen = [], set()
    for row in raw:
        if not isinstance(row, dict):
            raise ValidationError({"services": "Invalid service selected."})
        try:
            service_id = int(row.get("service_id"))
        except (TypeError, ValueError):
            raise ValidationError({"services": "Invalid service selected."})
        if service_id in seen:
            raise ValidationError({"services": "The same service is selected twice."})
        seen.add(service_id)

        renewal = None
        raw_date = row.get("renewal_date")
        if raw_date not in (None, ""):
            try:
                renewal = datetime.strptime(str(raw_date)[:10], "%Y-%m-%d").date()
            except ValueError:
                raise ValidationError({"services": "Renewal date must be a valid date."})
            if not (date(2000, 1, 1) <= renewal <= date(2100, 12, 31)):
                raise ValidationError({"services": "Renewal date is out of range."})
        rows.append((service_id, renewal))
    return rows


class ClientAPIView(APIView):
    """
    GET    /api/v1/clients/              list, 10 per page  (?page=&search=&status=1|2&service=&file_delivered=0|1)
    GET    /api/v1/clients/?val=summary  dashboard numbers + latest clients
    GET    /api/v1/clients/?val=excel    download Excel  (same filters)
    GET    /api/v1/clients/?val=pdf      download PDF    (same filters)
    GET    /api/v1/clients/<id>/         full details
    POST   /api/v1/clients/              create   (admin + employee)
    PUT    /api/v1/clients/<id>/         update   (admin + employee)  - also used to toggle file_delivered
    DELETE /api/v1/clients/<id>/         delete   (admin only)
    """
    permission_classes = (IsAuthenticated,)

    def get(self, request, id=None):
        denied = check_role(request)
        if denied:
            return denied

        if id is not None:
            try:
                c = client_queryset().get(id=id)
            except Client.DoesNotExist:
                return failed("Client not found", status.HTTP_404_NOT_FOUND)
            return success(client_to_dict(c, full=True))

        val = request.query_params.get("val", "")

        if val == "summary":
            total = Client.objects.count()
            active = Client.objects.filter(status=STATUS_ACTIVE).count()
            pending = Client.objects.filter(status=STATUS_ACTIVE, file_delivered=False).count()
            latest = client_queryset().order_by("-id")[:5]
            return success({
                "total": total, "active": active, "inactive": total - active, "file_pending": pending,
                "latest": [client_to_dict(c) for c in latest],
            })

        if val in ("excel", "pdf"):
            clients = list(filtered_clients(request)[:EXPORT_LIMIT])
            try:
                if val == "excel":
                    content = build_clients_excel(clients)
                    content_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                    ext = "xlsx"
                else:
                    content = build_clients_pdf(clients)
                    content_type, ext = "application/pdf", "pdf"
            except ImportError:
                return failed("Install on the server:  pip install openpyxl reportlab", status.HTTP_501_NOT_IMPLEMENTED)
            response = HttpResponse(content, content_type=content_type)
            response["Content-Disposition"] = f'attachment; filename="clients-{today_date():%Y-%m-%d}.{ext}"'
            return response

        rows, info = paginate(request, filtered_clients(request))
        return success([client_to_dict(c) for c in rows], pagination=info)

    # create and update use the same code
    def _save(self, request, client, creating):
        data = request.data
        for field in CLIENT_TEXT_FIELDS:
            if field in data:
                setattr(client, field, data[field] if data[field] is not None else "")

        # Client ID: typed for old clients, blank = auto (MAS-00001 ...)
        if "client_code" in data:
            typed = str(data["client_code"] or "").strip().upper()
            if typed:
                client.client_code = typed
            elif creating:
                client.client_code = None            # editing with an empty box keeps the old ID

        # File delivered toggle (+ automatic delivered date)
        if "file_delivered" in data:
            delivered = to_bool(data["file_delivered"])
            if delivered and not client.file_delivered:
                client.file_delivered_on = today_date()
            if not delivered:
                client.file_delivered_on = None
            client.file_delivered = delivered

        try:
            client.validate_fields()
            service_rows = parse_service_rows(data.get("services")) if "services" in data else None
        except ValidationError as e:
            errors = error_dict(e)
            return failed(first_error(errors), errors=errors)

        errors = {}
        if not client.name:
            errors["name"] = "Client name is required."
        if not client.phone:
            errors["phone"] = "Mobile number is required."

        if client.client_code:
            clash = Client.objects.filter(client_code=client.client_code).exclude(pk=client.pk).first()
            if clash:
                errors["client_code"] = f"Client ID {clash.client_code} is already used by {clash.name}."
        for field, label in (("pan", "PAN"), ("aadhaar", "Aadhaar")):
            value = getattr(client, field)
            if value:
                other = Client.objects.filter(**{field: value}).exclude(pk=client.pk).first()
                if other:
                    errors[field] = f"{label} already belongs to client {other.client_code} ({other.name})."

        found = {}
        if service_rows is not None:
            found = {s.id: s for s in Service.objects.filter(id__in=[sid for sid, _ in service_rows])}
            if len(found) != len(service_rows):
                errors["services"] = "One or more selected services do not exist."
            else:
                already = set() if creating else set(client.client_services.values_list("service_id", flat=True))
                if any(found[sid].status == STATUS_INACTIVE and sid not in already for sid, _ in service_rows):
                    errors["services"] = "Inactive services can't be newly assigned."

        if errors:
            return failed(first_error(errors), errors=errors)

        try:
            with transaction.atomic():
                if creating:
                    client.created_by = request.user
                client.updated_by = request.user
                client.save()

                if service_rows is not None:
                    existing = {cs.service_id: cs for cs in client.client_services.all()}
                    for service_id, renewal in service_rows:
                        link = existing.pop(service_id, None)
                        if link is None:
                            ClientService.objects.create(client=client, service_id=service_id, renewal_date=renewal)
                        elif link.renewal_date != renewal:
                            link.renewal_date = renewal
                            link.save(update_fields=["renewal_date"])
                    for leftover in existing.values():          # services that were un-selected
                        leftover.delete()
        except IntegrityError:
            return failed("Another client already uses one of these details (Client ID / PAN / Aadhaar).", status.HTTP_409_CONFLICT)

        client = client_queryset().get(pk=client.pk)
        return success(
            client_to_dict(client, full=True),
            message="Client created successfully" if creating else "Client updated successfully",
            code=status.HTTP_201_CREATED if creating else status.HTTP_200_OK,
        )

    def post(self, request):
        denied = check_role(request)
        if denied:
            return denied
        return self._save(request, Client(), creating=True)

    def put(self, request, id):
        denied = check_role(request)
        if denied:
            return denied
        try:
            client = Client.objects.get(id=id)
        except Client.DoesNotExist:
            return failed("Client not found", status.HTTP_404_NOT_FOUND)
        return self._save(request, client, creating=False)

    def delete(self, request, id):
        denied = check_role(request, admin_only=True)
        if denied:
            return denied
        try:
            Client.objects.get(id=id).delete()        # documents + their files are removed too
        except Client.DoesNotExist:
            return failed("Client not found", status.HTTP_404_NOT_FOUND)
        return success(message="Client deleted successfully")


# ============================== NEXT RENEWALS ==============================
class RenewalAPIView(APIView):
    """
    GET /api/v1/renewals/?when=tomorrow|today|week|overdue&search=&page=
    Default = tomorrow: clients whose renewal date is one day away (shown every day).
    Only ACTIVE clients are listed.  `counts` gives the number for every tab.
    """
    permission_classes = (IsAuthenticated,)

    def get(self, request):
        denied = check_role(request)
        if denied:
            return denied

        today = today_date()
        tabs = {
            "tomorrow": Q(renewal_date=today + timedelta(days=1)),
            "today": Q(renewal_date=today),
            "week": Q(renewal_date__gt=today, renewal_date__lte=today + timedelta(days=7)),
            "overdue": Q(renewal_date__lt=today),
        }
        when = request.query_params.get("when", "tomorrow")
        if when not in tabs:
            when = "tomorrow"

        base = ClientService.objects.select_related("client", "service").filter(renewal_date__isnull=False, client__status=STATUS_ACTIVE)
        rows = base.filter(tabs[when]).order_by("renewal_date", "client__name")
        search = request.query_params.get("search", "").strip()
        if search:
            rows = rows.filter(
                Q(client__name__icontains=search) | Q(client__client_code__icontains=search)
                | Q(client__phone__icontains=search) | Q(client__business_name__icontains=search)
                | Q(service__name__icontains=search)
            )

        page_rows, info = paginate(request, rows)
        data = [{
            "id": cs.id,
            "client_id": cs.client_id,
            "client_code": cs.client.client_code,
            "client_name": cs.client.name,
            "business_name": cs.client.business_name,
            "phone": cs.client.phone,
            "service_name": cs.service.name,
            "renewal_date": cs.renewal_date.isoformat(),
            "renewal_label": fmt_date(cs.renewal_date),
            "days_left": (cs.renewal_date - today).days,
        } for cs in page_rows]
        counts = {name: base.filter(q).count() for name, q in tabs.items()}
        return success(data, pagination=info, counts=counts, when=when, today=today.isoformat())


def read_date(value):
    """'2026-10-10' -> date, or None when it is not a real date."""
    try:
        return datetime.strptime(str(value)[:10], "%Y-%m-%d").date()
    except (TypeError, ValueError):
        return None


class RenewalRenewAPIView(APIView):
    """
    POST /api/v1/renewals/<client_service id>/renew/     ("Mark renewed" button)
         { "new_date": "2027-10-10",            next renewal date (required)
           "renewed_on": "2026-10-09",          day it was renewed (default today, not in the future)
           "old_date": "2026-10-10",            the date the screen showed (stops double click / two staff)
           "remarks": "paid by UPI" }
    Saves a RenewalHistory row and moves the service to the new renewal date.
    """
    permission_classes = (IsAuthenticated,)

    def post(self, request, id):
        denied = check_role(request)
        if denied:
            return denied

        data = request.data
        today = today_date()
        errors = {}

        renewed_on = today
        if data.get("renewed_on") not in (None, ""):
            renewed_on = read_date(data.get("renewed_on"))
            if renewed_on is None:
                errors["renewed_on"] = "Renewed date must be a valid date."
            elif renewed_on > today:
                errors["renewed_on"] = "Renewed date can't be in the future."
            elif renewed_on < date(2000, 1, 1):
                errors["renewed_on"] = "Renewed date is out of range."

        new_date = read_date(data.get("new_date"))
        if new_date is None:
            errors["new_date"] = "Next renewal date is required."
        elif not (date(2000, 1, 1) <= new_date <= date(2100, 12, 31)):
            errors["new_date"] = "Next renewal date is out of range."
        elif renewed_on and new_date <= renewed_on:
            errors["new_date"] = "Next renewal date must be after the renewed date."

        remarks = str(data.get("remarks") or "").strip()
        if len(remarks) > 255:
            errors["remarks"] = "Remarks can be 255 letters at most."
        if errors:
            return failed(first_error(errors), errors=errors)

        with transaction.atomic():
            try:
                link = ClientService.objects.select_for_update().select_related("client", "service").get(id=id)
            except ClientService.DoesNotExist:
                return failed("Renewal not found", status.HTTP_404_NOT_FOUND)
            if link.client.status != STATUS_ACTIVE:
                return failed("This client is inactive.")

            shown = str(data.get("old_date") or "")[:10]
            current = link.renewal_date.isoformat() if link.renewal_date else ""
            if shown and shown != current:
                return failed("This renewal was already updated. Please refresh the list.", status.HTTP_409_CONFLICT)

            RenewalHistory.objects.create(
                client=link.client, service=link.service, old_date=link.renewal_date, new_date=new_date,
                renewed_on=renewed_on, remarks=remarks, created_by=request.user,
            )
            link.renewal_date = new_date
            link.save(update_fields=["renewal_date"])

        return success({
            "id": link.id, "client_id": link.client_id, "service_name": link.service.name,
            "renewal_date": new_date.isoformat(), "renewal_label": fmt_date(new_date),
        }, message="Renewal saved successfully")


class ClientRenewalHistoryAPIView(APIView):
    """GET /api/v1/clients/<client_id>/renewal-history/?page=&service=   newest first, 10 per page"""
    permission_classes = (IsAuthenticated,)

    def get(self, request, client_id):
        denied = check_role(request)
        if denied:
            return denied
        if not Client.objects.filter(id=client_id).exists():
            return failed("Client not found", status.HTTP_404_NOT_FOUND)

        history = RenewalHistory.objects.filter(client_id=client_id).select_related("service", "created_by")
        service = request.query_params.get("service", "").strip()
        if service.isdigit():
            history = history.filter(service_id=int(service))

        rows, info = paginate(request, history)
        data = [{
            "id": h.id,
            "service_id": h.service_id,
            "service_name": h.service.name,
            "old_date": fmt_date(h.old_date),
            "new_date": fmt_date(h.new_date),
            "renewed_on": fmt_date(h.renewed_on),
            "remarks": h.remarks,
            "done_by": (h.created_by.first_name or h.created_by.username) if h.created_by else "",
        } for h in rows]
        return success(data, pagination=info)


# ============================== CLIENT DOCUMENTS (files) ==============================
UPLOAD_SIGNATURES = [
    (b"%PDF-", "pdf", "application/pdf"),
    (b"\xff\xd8\xff", "jpg", "image/jpeg"),
    (b"\x89PNG\r\n\x1a\n", "png", "image/png"),
]


def sniff_upload(f):
    """Looks at the real file bytes (not the file name) -> (ext, content_type) or None."""
    head = f.read(8)
    f.seek(0)
    for signature, ext, content_type in UPLOAD_SIGNATURES:
        if head.startswith(signature):
            return ext, content_type
    return None


class ClientDocumentAPIView(APIView):
    """
    POST   /api/v1/clients/<client_id>/documents/   multipart: file, doc_type, remarks
    GET    /api/v1/client-documents/<id>/           the file (login needed; ?download=1 to save)
    DELETE /api/v1/client-documents/<id>/           removes the record AND the file from the media folder
    Saved in MEDIA_ROOT/client_documents/<client id>/  - never served as a public /media/ link.
    """
    permission_classes = (IsAuthenticated,)
    parser_classes = (MultiPartParser, FormParser)

    def post(self, request, client_id):
        denied = check_role(request)
        if denied:
            return denied
        try:
            client = Client.objects.get(id=client_id)
        except Client.DoesNotExist:
            return failed("Client not found", status.HTTP_404_NOT_FOUND)

        upload = request.FILES.get("file")
        doc_type = (request.data.get("doc_type") or "other").strip()
        remarks = (request.data.get("remarks") or "").strip()[:255]

        if not upload:
            return failed("Please choose a file.", errors={"file": "Please choose a file."})
        if doc_type not in dict(DOC_TYPE_CHOICES):
            return failed("Invalid document type.", errors={"doc_type": "Invalid document type."})
        if upload.size == 0:
            return failed("The file is empty.", errors={"file": "The file is empty."})
        if upload.size > MAX_UPLOAD_MB * 1024 * 1024:
            msg = f"File is too large. Maximum size is {MAX_UPLOAD_MB} MB."
            return failed(msg, errors={"file": msg})
        kind = sniff_upload(upload)
        if not kind:
            msg = "Only PDF, JPG or PNG files are allowed."
            return failed(msg, errors={"file": msg})
        if client.documents.count() >= MAX_DOCS_PER_CLIENT:
            return failed(f"A client can have at most {MAX_DOCS_PER_CLIENT} documents.")

        ext, content_type = kind
        original_name = os.path.basename(upload.name)[:255]
        upload.name = f"upload.{ext}"          # extension comes from the real file type

        doc = ClientDocument(
            client=client, doc_type=doc_type, file=upload, original_name=original_name,
            content_type=content_type, size=upload.size, remarks=remarks, created_by=request.user,
        )
        doc.save()
        return success(document_to_dict(doc), message="Document uploaded successfully", code=status.HTTP_201_CREATED)

    def get(self, request, id):
        denied = check_role(request)
        if denied:
            return denied
        try:
            doc = ClientDocument.objects.get(id=id)
            handle = doc.file.open("rb")
        except (ClientDocument.DoesNotExist, FileNotFoundError, ValueError):
            return failed("File not found", status.HTTP_404_NOT_FOUND)
        response = FileResponse(
            handle, content_type=doc.content_type or "application/octet-stream",
            as_attachment=request.query_params.get("download") == "1",
            filename=doc.original_name or f"document-{doc.id}",
        )
        response["X-Content-Type-Options"] = "nosniff"
        response["Cache-Control"] = "private, no-store"
        return response

    def delete(self, request, id):
        denied = check_role(request)
        if denied:
            return denied
        try:
            ClientDocument.objects.get(id=id).delete()      # post_delete signal removes the file
        except ClientDocument.DoesNotExist:
            return failed("File not found", status.HTTP_404_NOT_FOUND)
        return success(message="Document deleted successfully")


# ============================== EXPORTS (Excel / PDF) ==============================
def services_text(c):
    parts = []
    for cs in sorted(c.client_services.all(), key=lambda x: x.service.name.lower()):
        parts.append(f"{cs.service.name} (renewal {fmt_date(cs.renewal_date)})" if cs.renewal_date else cs.service.name)
    return ", ".join(parts)


def build_clients_excel(clients):
    from openpyxl import Workbook
    from openpyxl.styles import Alignment, Font, PatternFill
    from openpyxl.utils import get_column_letter

    wb = Workbook()
    ws = wb.active
    ws.title = "Clients"
    headers = [
        ("Client ID", 13), ("Client Name", 26), ("Shop / Business Name", 28), ("Shop Address", 38), ("City", 16),
        ("Pincode", 10), ("Mobile", 14), ("Email", 28), ("PAN", 13), ("Aadhaar", 17), ("GSTIN", 18),
        ("Services (renewal dates)", 40), ("Bank Name", 20), ("Account Holder", 22), ("Account No.", 18), ("IFSC", 13),
        ("Status", 10), ("File Delivered", 14), ("Delivered On", 13), ("Notes", 34), ("Documents", 28), ("Added On", 12),
    ]
    ws.append([h for h, _ in headers])
    for i, (_, width) in enumerate(headers, 1):
        ws.column_dimensions[get_column_letter(i)].width = width
        cell = ws.cell(row=1, column=i)
        cell.font = Font(bold=True, color="FFFFFF")
        cell.fill = PatternFill("solid", fgColor="6B0F14")
        cell.alignment = Alignment(vertical="center", wrap_text=True)
    ws.row_dimensions[1].height = 24

    for c in clients:
        ws.append([
            c.client_code, c.name, c.business_name, c.address, c.city, c.pincode, c.phone, c.email,
            c.pan, mask_aadhaar(c.aadhaar), c.gstin, services_text(c),
            c.bank_name, c.account_holder, mask_account(c.account_number), c.ifsc,
            "Active" if c.status == STATUS_ACTIVE else "Inactive",
            "Delivered" if c.file_delivered else "Pending", fmt_date(c.file_delivered_on), c.notes,
            ", ".join(sorted({d.get_doc_type_display() for d in c.documents.all()})), fmt_date(c.created_at),
        ])
    for row in ws.iter_rows(min_row=2):
        for cell in row:
            cell.alignment = Alignment(vertical="top", wrap_text=True)
    for col in (6, 7, 10, 15):                      # keep pincode / mobile / Aadhaar / account as text
        for r in range(2, ws.max_row + 1):
            ws.cell(row=r, column=col).number_format = "@"
    ws.freeze_panes = "C2"
    ws.auto_filter.ref = ws.dimensions

    buffer = io.BytesIO()
    wb.save(buffer)
    return buffer.getvalue()


def build_clients_pdf(clients):
    from xml.sax.saxutils import escape

    from reportlab.lib import colors
    from reportlab.lib.pagesizes import A4, landscape
    from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
    from reportlab.lib.units import mm
    from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle

    cell = ParagraphStyle("cell", fontName="Helvetica", fontSize=7, leading=9)
    bold = ParagraphStyle("bold", parent=cell, fontName="Helvetica-Bold")
    head = ParagraphStyle("head", parent=bold, textColor=colors.white)
    styles = getSampleStyleSheet()

    def P(text, style=cell):
        return Paragraph(escape(str(text or "")).replace("\n", "<br/>"), style)

    def stack(*lines):
        return P("\n".join(l for l in lines if l))

    data = [[P(h, head) for h in ("Client ID", "Client / Business", "Contact", "PAN / Aadhaar / GSTIN", "Services", "Address", "Bank", "Status")]]
    for c in clients:
        service_lines = [
            f"{cs.service.name}" + (f" - {fmt_date(cs.renewal_date)}" if cs.renewal_date else "")
            for cs in sorted(c.client_services.all(), key=lambda x: x.service.name.lower())
        ]
        data.append([
            P(c.client_code, bold),
            Paragraph(f"<b>{escape(c.name)}</b><br/>{escape(c.business_name)}", cell),
            stack(c.phone, c.email),
            stack(c.pan and f"PAN: {c.pan}", c.aadhaar and f"Aadhaar: {mask_aadhaar(c.aadhaar)}", c.gstin and f"GST: {c.gstin}"),
            stack(*service_lines),
            stack(c.address, " ".join(x for x in (c.city, c.pincode) if x)),
            stack(c.bank_name, c.account_holder, c.account_number and f"A/c: {mask_account(c.account_number)}", c.ifsc and f"IFSC: {c.ifsc}"),
            stack("Active" if c.status == STATUS_ACTIVE else "Inactive", "File: Delivered" if c.file_delivered else "File: Pending"),
        ])

    def footer(canvas, doc):
        canvas.saveState()
        canvas.setFont("Helvetica", 7)
        canvas.setFillColor(colors.grey)
        canvas.drawString(12 * mm, 8 * mm, "Manju Associates - Confidential")
        canvas.drawRightString(landscape(A4)[0] - 12 * mm, 8 * mm, f"Page {doc.page}")
        canvas.restoreState()

    buffer = io.BytesIO()
    doc = SimpleDocTemplate(buffer, pagesize=landscape(A4), leftMargin=12 * mm, rightMargin=12 * mm, topMargin=12 * mm, bottomMargin=14 * mm,
                            title="Client List", author="Manju Associates")
    width = landscape(A4)[0] - 24 * mm
    ratios = [0.085, 0.15, 0.14, 0.15, 0.125, 0.125, 0.14, 0.085]
    table = Table(data, colWidths=[width * r for r in ratios], repeatRows=1)
    table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#6B0F14")),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#FBF1DE")]),
        ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#D9C9A3")),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
    ]))
    story = [
        Paragraph("Manju Associates - Client List", styles["Title"]),
        Paragraph(f"Generated on {now_local():%d-%m-%Y %H:%M} | {len(clients)} client(s)", styles["Normal"]),
        Spacer(1, 6 * mm),
        table,
    ]
    doc.build(story, onFirstPage=footer, onLaterPages=footer)
    return buffer.getvalue()

