'use server';

import { z } from 'zod';
import { getDbClient } from '../dbClient';
import { Bill, BillAttachment, BillingLocation, YearMonth } from '../db-types';
import { ObjectId } from 'mongodb';
import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth';
import { gotoHome } from './navigationActions';
import { getTranslations } from "next-intl/server";
import { IntlTemplateFn } from '@/app/i18n';

export type State = {
    errors?: {
        billName?: string[];
        billAttachment?: string[],
        billNotes?: string[],
        payedAmount?: string[],
    };
    message?:string | null;
}

/**
 * Schema for validating bill form fields
 * @description this is defined as factory function so that it can be used with the next-intl library
*/
const FormSchema = (t:IntlTemplateFn) => z.object({
    _id: z.string(),
    billName: z.coerce.string().min(1, t("bill-name-required")),
    billNotes: z.string(),
    addToSubsequentMonths: z.boolean().optional(),
    payedAmount: z.string().nullable().transform((val, ctx) => {

        if(!val || val === '') {
            return null;
        }

        const parsed = parseFloat(val.replace(',', '.'));

        if (isNaN(parsed)) {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: t("not-a-number"),
            });
        
            // This is a special symbol you can use to
            // return early from the transform function.
            // It has type `never` so it does not affect the
            // inferred return type.
            return z.NEVER;
        }

        if (parsed < 0) {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: t("negative-number")
            });
        
            // This is a special symbol you can use to
            // return early from the transform function.
            // It has type `never` so it does not affect the
            // inferred return type.
            return z.NEVER;
        }
  
        return Math.floor(parsed * 100); // value is stored in cents

      }),
  });

/**
 * converts the file to a format stored in the database
 * @param billAttachment 
 * @returns 
 */
const serializeAttachment = async (billAttachment: File | null) => {

    if (!billAttachment) {
        return null;
    }

    const {
        name: fileName,
        size: fileSize,
        type: fileType,
        lastModified: fileLastModified,
    } = billAttachment;

    if(!fileName || fileName === 'undefined' || fileSize === 0) {
        return null;
    }

    // convert the billAttachment file contents to format that can be stored in the database
    const fileContents = await billAttachment.arrayBuffer();
    const fileContentsBase64 = Buffer.from(fileContents).toString('base64');

    // create an object to store the file in the database
    return({
        fileName,
        fileSize,
        fileType,
        fileLastModified,
        fileContentsBase64,
    } as BillAttachment);
}

/**
 * Server-side action which adds or updates a bill
 * @param locationId location of the bill
 * @param billId ID of the bill
 * @param prevState previous state of the form
 * @param formData form data
 * @returns 
 */
export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationId: string, billId:string|undefined, billYear:number|undefined, billMonth:number|undefined, prevState:State, formData: FormData) => {

    const { id: userId } = user;

    const t = await getTranslations("bill-edit-form.validation");

    // FormSchema
    const validatedFields = FormSchema(t)
        .omit({ _id: true })
        .safeParse({
            billName: formData.get('billName'),
            billNotes: formData.get('billNotes'),
            addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on',
            payedAmount: formData.get('payedAmount'),
        });

    // If form validation fails, return errors early. Otherwise, continue...
    if(!validatedFields.success) {
        console.log("updateBill.validation-error");
        return({
            errors: validatedFields.error.flatten().fieldErrors,
            message: t("form-error-message"),
        });
    }

    const {
        billName,
        billNotes,
        addToSubsequentMonths,
        payedAmount,
    } = validatedFields.data;

    const billPaid = formData.get('billPaid') === 'on';
    const barcodeImage = formData.get('barcodeImage')?.valueOf() as string;

    // update the bill in the mongodb
    const dbClient = await getDbClient();
    
    const billAttachment = await serializeAttachment(formData.get('billAttachment') as File);

    if(billId) {

        // if there is an attachment, update the attachment field
        // otherwise, do not update the attachment field
        const mongoDbSet = billAttachment ? {
            "bills.$[elem].name": billName,
            "bills.$[elem].paid": billPaid,
            "bills.$[elem].attachment": billAttachment,
            "bills.$[elem].notes": billNotes,
            "bills.$[elem].payedAmount": payedAmount,
            "bills.$[elem].barcodeImage": barcodeImage,
            
        }: {
            "bills.$[elem].name": billName,
            "bills.$[elem].paid": billPaid,
            "bills.$[elem].notes": billNotes,
            "bills.$[elem].payedAmount": payedAmount,
            "bills.$[elem].barcodeImage": barcodeImage,
        };

        // find a location with the given locationID
        const post = await dbClient.collection<BillingLocation>("lokacije").updateOne(
            {
                _id: locationId, // find a location with the given locationID
                userId // make sure that the location belongs to the user
            },
            {
                $set: mongoDbSet
            }, {
                arrayFilters: [
                    { "elem._id": { $eq: billId } } // find a bill with the given billID
                ]
            });
    } else {
        // Create new bill - add to current location first
        const newBill = {
            _id: (new ObjectId()).toHexString(),
            name: billName,
            paid: billPaid,
            attachment: billAttachment,
            notes: billNotes,
            payedAmount,
            barcodeImage,
        };

        // Add to current location
        await dbClient.collection<BillingLocation>("lokacije").updateOne(
            {
                _id: locationId, // find a location with the given locationID
                userId // make sure that the location belongs to the user
            },
            {
                $push: {
                    bills: newBill
                }
            });

        // If addToSubsequentMonths is enabled, add to subsequent months
        if (addToSubsequentMonths && billYear && billMonth) {
            // Get the current location to find its name
            const currentLocation = await dbClient.collection<BillingLocation>("lokacije")
                .findOne({ _id: locationId, userId }, { projection: { name: 1 } });

            if (currentLocation) {
                // Find all subsequent months that have the same location name
                const subsequentLocations = await dbClient.collection<BillingLocation>("lokacije")
                    .find({
                        userId,
                        name: currentLocation.name,
                        $or: [
                            { "yearMonth.year": { $gt: billYear } },
                            { 
                                "yearMonth.year": billYear, 
                                "yearMonth.month": { $gt: billMonth } 
                            }
                        ]
                    }, { projection: { _id: 1 } })
                    .toArray(); 

                // For each subsequent location, check if bill with same name already exists
                const updateOperations = [];
                for (const location of subsequentLocations) {
                    const existingBill = await dbClient.collection<BillingLocation>("lokacije")
                        .findOne({
                            _id: location._id,
                            "bills.name": billName
                        }, {
                            // We only need to know if a matching bill exists; avoid conflicting projections
                            projection: { _id: 1 }
                        });

                    // Only add if bill with same name doesn't already exist
                    if (!existingBill) {
                        updateOperations.push({
                            updateOne: {
                                filter: { _id: location._id, userId },
                                update: {
                                    $push: {
                                        bills: {
                                            _id: (new ObjectId()).toHexString(),
                                            name: billName,
                                            paid: false, // New bills in subsequent months are unpaid
                                            attachment: null, // No attachment for subsequent months
                                            notes: billNotes,
                                            payedAmount: null,
                                            barcodeImage: undefined,
                                        }
                                    }
                                }
                            }
                        });
                    }
                }

                // Execute all update operations at once if any
                if (updateOperations.length > 0) {
                    await dbClient.collection<BillingLocation>("lokacije").bulkWrite(updateOperations);
                }
            }
        }
    }
    if(billYear && billMonth    ) {
        await gotoHome({ year: billYear, month: billMonth });
    }
})
/*
Funkcija zamijenjena sa `fetchBillByUserAndId`, koja brže radi i ne treba korisnika

export const fetchBillByUserAndId = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, includeAttachmentBinary:boolean = false) => {

    const { id: userId } = user;

    const dbClient = await getDbClient();

    // don't include the attachment binary data in the response
    // if the attachment binary data is not needed
    const projection = includeAttachmentBinary ? {} : {
        "bills.attachment.fileContentsBase64": 0,
    };

    // find a location with the given locationID
    const billLocation = await dbClient.collection<BillingLocation>("lokacije").findOne(
        {
            _id: locationID,
            userId
        },
        {
            projection
        })

    if(!billLocation) {
        console.log(`Location ${locationID} not found`);
        return(null);
    }

    // find a bill with the given billID
    const bill = billLocation?.bills.find(({ _id }) => _id.toString() === billID);

    if(!bill) {
        console.log('Bill not found');
        return(null);
    }

    return([billLocation, bill] as [BillingLocation, Bill]);
})
*/

export const fetchBillById = async (locationID:string, billID:string, includeAttachmentBinary:boolean = false) => {


    const dbClient = await getDbClient();

    // don't include the attachment binary data in the response
    // if the attachment binary data is not needed
    const projection = includeAttachmentBinary ? {} : {
        "bills.attachment.fileContentsBase64": 0,
    };

    // find a location with the given locationID
    const billLocation = await dbClient.collection<BillingLocation>("lokacije").findOne(
        {
            _id: locationID,
        },
        {
            projection
        })

    if(!billLocation) {
        console.log(`Location ${locationID} not found`);
        return(null);
    }

    // find a bill with the given billID
    const bill = billLocation?.bills.find(({ _id }) => _id.toString() === billID);

    if(!bill) {
        console.log('Bill not found');
        return(null);
    }

    return([billLocation, bill] as [BillingLocation, Bill]);
};

export const deleteBillById = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, year:number, month:number, _prevState:any, formData?: FormData) => {

    const { id: userId } = user;

    const dbClient = await getDbClient();
    
    const deleteInSubsequentMonths = formData?.get('deleteInSubsequentMonths') === 'on';

    if (deleteInSubsequentMonths) {
        // Get the current location and bill to find the bill name and location name
        const location = await dbClient.collection<BillingLocation>("lokacije")
            .findOne({ _id: locationID, userId }, { 
                projection: {
                    "name": 1,
                    "bills._id": 1,
                    "bills.name": 1
                } 
            });

        if (location) {
            const bill = location.bills.find(b => b._id === billID);
            
            if (bill) {
                // Find all subsequent locations with the same name that have the same bill
                const subsequentLocations = await dbClient.collection<BillingLocation>("lokacije")
                    .find({
                        userId,
                        name: location.name,
                        $or: [
                            { "yearMonth.year": { $gt: year } },
                            { 
                                "yearMonth.year": year, 
                                "yearMonth.month": { $gt: month } 
                            }
                        ],
                        "bills.name": bill.name
                    }, { projection: { _id: 1 } })
                    .toArray();

                // Delete the bill from all subsequent locations (by name)
                const updateOperations = subsequentLocations.map(loc => ({
                    updateOne: {
                        filter: { _id: loc._id, userId },
                        update: {
                            $pull: {
                                bills: { name: bill.name } as Partial<Bill>
                            }
                        }
                    }
                }));

                // Also delete from current location (by ID for precision)
                updateOperations.push({
                    updateOne: {
                        filter: { _id: locationID, userId },
                        update: {
                            $pull: {
                                bills: { _id: billID }
                            }
                        }
                    }
                });

                // Execute all delete operations
                if (updateOperations.length > 0) {
                    await dbClient.collection<BillingLocation>("lokacije").bulkWrite(updateOperations);
                }
            }
        }
    } else {
        // Delete only from current location (original behavior)
        await dbClient.collection<BillingLocation>("lokacije").updateOne(
            {
                _id: locationID, // find a location with the given locationID
                userId // make sure that the location belongs to the user
            },
            {
                // remove the bill with the given billID
                $pull: {
                    bills: {
                        _id: billID
                    }
                }
            });
    }
    
    await gotoHome({year, month});
    return { message: null };
});