from rest_framework import serializers
from django.db import transaction
from crm.models import Quote, Invoice, QuoteItem, Lead, Payment, Office, ItemTemplate

class ItemTemplateSerializer(serializers.ModelSerializer):
    class Meta:
        model = ItemTemplate
        fields = ['id', 'name', 'description', 'rate', 'is_active']


class QuoteItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = QuoteItem
        fields = ['item_name', 'description', 'qty', 'rate', 'amount']


class QuoteSerializer(serializers.ModelSerializer):
    lead = serializers.PrimaryKeyRelatedField(queryset=Lead.objects.all())
    office = serializers.PrimaryKeyRelatedField(queryset=Office.objects.filter(is_active=True), required=False, allow_null=True)
    office_locations = serializers.ListField(required=False, write_only=True, help_text="Office locations array from frontend")
    lead_data = serializers.SerializerMethodField()
    items = QuoteItemSerializer(many=True)

    class Meta:
        model = Quote
        fields = '__all__'
        read_only_fields = ['quote_number', 'pdf_file']
        extra_fields = ['total_amount']

    def get_lead_data(self, obj):
        return obj.get_lead_data() if hasattr(obj, 'get_lead_data') else {}

    def to_internal_value(self, data):
        # Handle office field when it comes as a dict from frontend
        if 'office' in data and isinstance(data['office'], dict):
            office_data = data['office']
            if 'location_code' in office_data:
                try:
                    office = Office.objects.get(location_code=office_data['location_code'], is_active=True)
                    data = data.copy()  # Create a copy to avoid modifying original
                    data['office'] = office.id  # Convert to primary key
                except Office.DoesNotExist:
                    # Fall back to default office if location_code not found
                    default_office = Office.get_default_office()
                    if default_office:
                        data = data.copy()
                        data['office'] = default_office.id
                    else:
                        data = data.copy()
                        data['office'] = None
            else:
                # If no location_code, try to find by name or other fields
                if 'name' in office_data:
                    try:
                        office = Office.objects.get(name=office_data['name'], is_active=True)
                        data = data.copy()
                        data['office'] = office.id
                    except Office.DoesNotExist:
                        data = data.copy()
                        data['office'] = None
        
        return super().to_internal_value(data)

    def create(self, validated_data):
        items_data = validated_data.pop('items')
        # Handle office_locations array from frontend
        office_locations = validated_data.pop('office_locations', [])
        
        # Extract office from office_locations array if provided
        if office_locations and len(office_locations) > 0:
            office_data = office_locations[0]  # Take the first office
            if isinstance(office_data, dict) and 'location_code' in office_data:
                # Find office by location_code
                try:
                    office = Office.objects.get(location_code=office_data['location_code'], is_active=True)
                    validated_data['office'] = office
                except Office.DoesNotExist:
                    # Fall back to default office if location_code not found
                    validated_data['office'] = Office.get_default_office()
        # If office is still not set but office pk provided in 'office', keep it
        # Otherwise default office will be assigned by model save()
        
        # Always create with is_invoice_converted=False first
        is_invoice_converted = validated_data.pop('is_invoice_converted', False)
        quote = Quote.objects.create(**validated_data, is_invoice_converted=False)
        for item_data in items_data:
            QuoteItem.objects.create(quote=quote, **item_data)
        quote.generate_pdf()
        # Now set is_invoice_converted if needed, after items are saved
        if is_invoice_converted:
            quote.is_invoice_converted = True
            quote.save()
        return quote

    def update(self, instance, validated_data):
        # Extract nested data and flags up-front
        items_data = validated_data.pop('items', None)
        # Handle office_locations array from frontend
        office_locations = validated_data.pop('office_locations', [])
        
        # Extract office from office_locations array if provided
        if office_locations and len(office_locations) > 0:
            office_data = office_locations[0]  # Take the first office
            if isinstance(office_data, dict) and 'location_code' in office_data:
                # Find office by location_code
                try:
                    office = Office.objects.get(location_code=office_data['location_code'], is_active=True)
                    validated_data['office'] = office
                except Office.DoesNotExist:
                    # Fall back to default office if location_code not found
                    validated_data['office'] = Office.get_default_office()
        
        # Defer conversion until after item updates to ensure invoice copies latest items
        requested_conversion_flag = validated_data.pop(
            'is_invoice_converted', instance.is_invoice_converted
        )

        with transaction.atomic():
            # Update primitive fields
            for attr, value in validated_data.items():
                setattr(instance, attr, value)
            instance.save()

            # Replace items if provided in payload
            if items_data is not None:
                instance.items.all().delete()
                for item in items_data:
                    QuoteItem.objects.create(quote=instance, **item)
                # Recalculate totals and regenerate PDF via model logic/signals
                instance.save()

            # Trigger conversion only after items are in final state
            if requested_conversion_flag and not instance.is_invoice_converted:
                instance.is_invoice_converted = True
                instance.save()

        return instance


class PaymentSerializer(serializers.ModelSerializer):
    company_name = serializers.SerializerMethodField()
    invoice_number = serializers.SerializerMethodField()
    invoice_details = serializers.SerializerMethodField()
    # Allow frontend to pass office id to update invoice's office before payment PDF is generated
    office = serializers.PrimaryKeyRelatedField(queryset=Office.objects.filter(is_active=True), required=False, allow_null=True, write_only=True)

    class Meta:
        model = Payment
        fields = [
            'id', 'invoice', 'amount', 'payment_date', 'payment_method', 'transaction_id',
            'pdf_file', 'notes', 'company_name', 'invoice_number', 'invoice_details', 'office'
        ]

    def get_company_name(self, obj):
        return obj.invoice.lead.company if hasattr(obj.invoice, 'lead') and obj.invoice.lead else None

    def get_invoice_number(self, obj):
        return obj.invoice.invoice_number if hasattr(obj.invoice, 'invoice_number') else None

    def get_invoice_details(self, obj):
        invoice = getattr(obj, 'invoice', None)
        if not invoice:
            return None
        office = getattr(invoice, 'office', None)
        # Fallback to quote.office for display if invoice.office is not set
        if office is None and getattr(invoice, 'quote', None):
            office = getattr(invoice.quote, 'office', None)
        office_data = None
        if office:
            office_data = {
                'id': office.id,
                'name': office.name,
                'location_code': office.location_code,
                'address': office.address,
                'phone': office.phone,
                'email': office.email,
                'website': office.website,
                'gstin': office.gstin,
            }
        return {
            'invoice_number': getattr(invoice, 'invoice_number', None),
            'company_name': invoice.lead.company if getattr(invoice, 'lead', None) else None,
            'office': office_data,
        }

    def create(self, validated_data):
        # If an office id is provided, update the related invoice's office BEFORE creating the payment
        office = validated_data.pop('office', None)
        invoice = validated_data.get('invoice')
        if invoice and office:
            invoice.office = office
            invoice.save(update_fields=['office'])
        return super().create(validated_data)

class InvoiceSerializer(serializers.ModelSerializer):
    office = serializers.PrimaryKeyRelatedField(queryset=Office.objects.filter(is_active=True), required=False, allow_null=True)
    office_locations = serializers.ListField(required=False, write_only=True, help_text="Office locations array from frontend")
    total_paid = serializers.SerializerMethodField()
    pending_amount = serializers.SerializerMethodField()
    payments = PaymentSerializer(many=True, read_only=True)
    latest_payment = serializers.SerializerMethodField()
    company_name = serializers.SerializerMethodField()
    quote_number = serializers.SerializerMethodField()
    quote_data = serializers.SerializerMethodField()
    lead_data = serializers.SerializerMethodField()

    class Meta:
        model = Invoice
        fields = '__all__'
        read_only_fields = ['invoice_number', 'pdf_file']
        extra_fields = ['total_paid', 'pending_amount', 'payments', 'latest_payment', 'quote_data', 'lead_data']

    def get_total_paid(self, obj):
        return obj.get_total_paid()

    def get_pending_amount(self, obj):
        return obj.get_pending_amount()

    def get_latest_payment(self, obj):
        latest = obj.payments.order_by('-payment_date').first()
        return PaymentSerializer(latest).data if latest else None
    
    def get_company_name(self, obj):
        return obj.lead.company if obj.lead else None
    
    def get_quote_number(self, obj):
        return obj.quote.quote_number if obj.quote else None
    
    def get_quote_data(self, obj):
        if obj.quote:
            return {
                'quote_number': obj.quote.quote_number,
                'items': [
                    {
                        'item_name': item.item_name,
                        'description': item.description,
                        'qty': item.qty,
                        'rate': float(item.rate),
                        'amount': float(item.amount)
                    }
                    for item in obj.quote.items.all()
                ]
            }
        return None
    
    def get_lead_data(self, obj):
        if obj.lead:
            return {
                'company': obj.lead.company,
                'first_name': obj.lead.first_name,
                'last_name': obj.lead.last_name,
                'address': obj.lead.address
            }
        return None
    
    def to_internal_value(self, data):
        # Handle office field when it comes as a dict from frontend
        if 'office' in data and isinstance(data['office'], dict):
            office_data = data['office']
            if 'location_code' in office_data:
                try:
                    office = Office.objects.get(location_code=office_data['location_code'], is_active=True)
                    data = data.copy()  # Create a copy to avoid modifying original
                    data['office'] = office.id  # Convert to primary key
                except Office.DoesNotExist:
                    # Fall back to default office if location_code not found
                    default_office = Office.get_default_office()
                    if default_office:
                        data = data.copy()
                        data['office'] = default_office.id
                    else:
                        data = data.copy()
                        data['office'] = None
            else:
                # If no location_code, try to find by name or other fields
                if 'name' in office_data:
                    try:
                        office = Office.objects.get(name=office_data['name'], is_active=True)
                        data = data.copy()
                        data['office'] = office.id
                    except Office.DoesNotExist:
                        data = data.copy()
                        data['office'] = None
        
        return super().to_internal_value(data)
    
    def create(self, validated_data):
        # Handle office_locations array from frontend
        office_locations = validated_data.pop('office_locations', [])
        
        # Extract office from office_locations array if provided
        if office_locations and len(office_locations) > 0:
            office_data = office_locations[0]  # Take the first office
            if isinstance(office_data, dict) and 'location_code' in office_data:
                # Find office by location_code
                try:
                    office = Office.objects.get(location_code=office_data['location_code'], is_active=True)
                    validated_data['office'] = office
                except Office.DoesNotExist:
                    # Fall back to default office if location_code not found
                    validated_data['office'] = Office.get_default_office()
        
        return super().create(validated_data)