// server/src/lib/actions/contractLineActions.ts
'use server'
import ContractLine from '../models/contractLine';
import { DeletionValidationResult, IContractLine, IContractLineFixedConfig } from '@alga-psa/types'; // Added IContractLineFixedConfig
import { createTenantKnex } from '@alga-psa/db';
import { Knex } from 'knex'; // Import Knex type
import { ContractLineServiceConfigurationService } from '../services/contractLineServiceConfigurationService';
import { IContractLineServiceFixedConfig } from '@alga-psa/types';
import ContractLineFixedConfig from '../models/contractLineFixedConfig'; // Added import for new model
import { withTransaction } from '@alga-psa/db';
import { withAuth } from '@alga-psa/auth';
import { hasPermission } from '@alga-psa/auth/rbac';
import { getAnalyticsAsync } from '../lib/authHelpers';
import { deleteEntityWithValidation } from '@alga-psa/core';
import { resolveBillingCycleAlignmentForCompatibility } from '@shared/billingClients/billingCycleAlignmentCompatibility';
import {
    DEFAULT_RECURRING_AUTHORING_CADENCE_OWNER,
    resolveRecurringAuthoringPolicy,
} from '@shared/billingClients/recurringAuthoringPolicy';
import {
    normalizeLiveRecurringStorage,
    normalizeTemplateRecurringStorage,
} from '@shared/billingClients/recurrenceStorageModel';
import { syncRecurringServicePeriodsForContractLine } from './recurringServicePeriodSync';

async function assertContractLineIsAuthorable(
    trx: Knex.Transaction,
    tenant: string,
    contractLineId: string,
): Promise<void> {
    const contract = await trx('contract_lines as cl')
        .join('contracts as c', function joinContracts() {
            this.on('cl.contract_id', '=', 'c.contract_id')
                .andOn('cl.tenant', '=', 'c.tenant');
        })
        .where('cl.tenant', tenant)
        .andWhere('cl.contract_line_id', contractLineId)
        .first('c.is_system_managed_default');

    if (contract?.is_system_managed_default === true) {
        throw new Error('System-managed default contracts are attribution-only; contract-line authoring is disabled.');
    }
}

export const getContractLines = withAuth(async (
    user,
    { tenant }
): Promise<IContractLine[]> => {
    try {
        const isBypass = process.env.E2E_AUTH_BYPASS === 'true';

        const { knex } = await createTenantKnex();
        if (!tenant) {
            throw new Error("tenant context not found");
        }

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!isBypass && !await hasPermission(user, 'billing', 'read')) {
                throw new Error('Permission denied: Cannot read contract lines');
            }

            const plans = await ContractLine.getAll(trx);
            const enrichedPlans = plans.map((plan) => normalizeLiveRecurringStorage(plan));

            return enrichedPlans;
        });
    } catch (error) {
        console.error('Error fetching contract lines:', error);
        if (error instanceof Error) {
            throw error; // Preserve specific error messages
        }
        throw new Error(`Failed to fetch client contract lines: ${error}`);
    }
});

// New function to get a single contract line by ID
export const getContractLineById = withAuth(async (
    user,
    { tenant },
    planId: string
): Promise<IContractLine | null> => {
    let tenant_copy: string = '';
    try {
        const isBypass = process.env.E2E_AUTH_BYPASS === 'true';

        const { knex } = await createTenantKnex();
        if (!tenant) {
            throw new Error("tenant context not found");
        }
        tenant_copy = tenant;

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!isBypass && !await hasPermission(user, 'billing', 'read')) {
                throw new Error('Permission denied: Cannot read contract lines');
            }

            const templateLine = await trx('contract_template_lines')
                .where({ tenant, template_line_id: planId })
                .first();

            if (templateLine) {
                return {
                    contract_line_id: templateLine.template_line_id,
                    contract_line_name: templateLine.template_line_name,
                    billing_frequency: templateLine.billing_frequency,
                    is_custom: true,
                    contract_id: templateLine.template_id,
                    tenant,
                    display_order: templateLine.display_order ?? 0,
                    custom_rate: templateLine.custom_rate != null ? Number(templateLine.custom_rate) : null,
                    ...normalizeTemplateRecurringStorage({
                        billing_timing: templateLine.billing_timing,
                        cadence_owner: templateLine.cadence_owner,
                    }),
                    contract_line_type: templateLine.line_type ?? 'Fixed',
                    service_category: templateLine.service_category ?? null,
                    is_active: templateLine.is_active ?? true,
                    created_at: templateLine.created_at,
                    updated_at: templateLine.updated_at,
                } as IContractLine;
            }

            // Assuming the ContractLine model has a method like findById
            // This might need adjustment based on the actual model implementation
            // It should ideally fetch the base plan and potentially join/fetch config details
            const plan = await ContractLine.findById(trx, planId);
            if (!plan) {
                return null;
            }

            // billing_timing is stored directly on contract_lines
            return normalizeLiveRecurringStorage(plan);
        });
    } catch (error) {
        console.error(`Error fetching contract line with ID ${planId}:`, error);
        if (error instanceof Error) {
            // Handle specific errors like 'not found' if the model throws them
            if (error.message.includes('not found')) { // Example check
                return null;
            }
            throw error;
        }
        throw new Error(`Failed to fetch contract line ${planId} in tenant ${tenant_copy}: ${error}`);
    }
});

export const createContractLine = withAuth(async (
    user,
    { tenant },
    planData: Omit<IContractLine, 'contract_line_id'>
): Promise<IContractLine> => {
    try {
        const { knex } = await createTenantKnex();
        if (!tenant) {
            throw new Error("tenant context not found");
        }

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!await hasPermission(user, 'billing', 'create')) {
                throw new Error('Permission denied: Cannot create contract lines');
            }

            // Remove tenant field if present in planData to prevent override
            const { tenant: _, ...safePlanData } = planData;
            const recurringAuthoringPolicy = resolveRecurringAuthoringPolicy({
                cadenceOwner: safePlanData.cadence_owner,
                defaultCadenceOwner: DEFAULT_RECURRING_AUTHORING_CADENCE_OWNER,
                billingTiming: safePlanData.billing_timing,
            });
            safePlanData.cadence_owner = recurringAuthoringPolicy.cadenceOwner;
            safePlanData.billing_timing = recurringAuthoringPolicy.billingTiming;
            const plan = await ContractLine.create(trx, safePlanData);
            const enrichedPlan: IContractLine = normalizeLiveRecurringStorage(plan);

            if (enrichedPlan.contract_line_id) {
                await syncRecurringServicePeriodsForContractLine(trx, {
                    tenant,
                    contractLineId: enrichedPlan.contract_line_id,
                    sourceRunPrefix: 'contract_line_create',
                });
            }

            // Track analytics
            const { analytics, AnalyticsEvents } = await getAnalyticsAsync();
            analytics.capture(AnalyticsEvents.BILLING_RULE_CREATED, {
                contract_line_id: enrichedPlan.contract_line_id,
                contract_line_name: enrichedPlan.contract_line_name,
                contract_line_type: enrichedPlan.contract_line_type
            }, user.user_id);

            return enrichedPlan;
        });
    } catch (error) {
        console.error('Error creating contract line:', error);
        if (error instanceof Error) {
            throw error; // Preserve specific error messages
        }
        throw new Error(`Failed to create contract line: ${error}`);
    }
});

export const updateContractLine = withAuth(async (
    user,
    { tenant },
    planId: string,
    updateData: Partial<IContractLine>
): Promise<IContractLine> => {
    try {
        const { knex } = await createTenantKnex();
        if (!tenant) {
            throw new Error("tenant context not found");
        }

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!await hasPermission(user, 'billing', 'update')) {
                throw new Error('Permission denied: Cannot update contract lines');
            }

            // Fetch the existing plan to check its type
            const existingPlan = await ContractLine.findById(trx, planId);
            if (!existingPlan) {
                // Handle case where plan is not found before update attempt
                throw new Error(`Contract Line with ID ${planId} not found.`);
            }
            await assertContractLineIsAuthorable(trx, tenant, planId);

            // Remove tenant field if present in updateData to prevent override
            const { tenant: _, ...safeUpdateData } = updateData;
            const recurringAuthoringPolicy = resolveRecurringAuthoringPolicy({
                cadenceOwner: safeUpdateData.cadence_owner,
                fallbackCadenceOwner: existingPlan.cadence_owner ?? DEFAULT_RECURRING_AUTHORING_CADENCE_OWNER,
                billingTiming: safeUpdateData.billing_timing,
                fallbackBillingTiming: existingPlan.billing_timing,
            });
            safeUpdateData.cadence_owner = recurringAuthoringPolicy.cadenceOwner;
            safeUpdateData.billing_timing = recurringAuthoringPolicy.billingTiming;

            // If the plan is hourly, remove only the per-service hourly_rate field
            // minimum_billable_time and round_up_to_nearest are now contract-line-level
            if (existingPlan.contract_line_type === 'Hourly') {
                delete safeUpdateData.hourly_rate;
            }

            // Proceed with the update using the potentially modified data
            // Ensure ContractLine.update handles empty updateData gracefully if all fields were removed
            const plan = await ContractLine.update(trx, planId, safeUpdateData);
            const enrichedPlan: IContractLine = normalizeLiveRecurringStorage(plan);

            await syncRecurringServicePeriodsForContractLine(trx, {
                tenant,
                contractLineId: planId,
                sourceRunPrefix: 'contract_line_update',
            });

            // Track analytics
            const { analytics, AnalyticsEvents } = await getAnalyticsAsync();
            analytics.capture(AnalyticsEvents.BILLING_RULE_UPDATED, {
                contract_line_id: enrichedPlan.contract_line_id,
                contract_line_name: enrichedPlan.contract_line_name,
                contract_line_type: enrichedPlan.contract_line_type,
                updated_fields: Object.keys(safeUpdateData)
            }, user.user_id);

            return enrichedPlan;
        });
    } catch (error) {
        console.error('Error updating contract line:', error);
        if (error instanceof Error) {
            // Re-throw specific errors like 'not found' if they weren't caught above
            if (error.message.includes('not found')) {
                 throw new Error(`Contract Line with ID ${planId} not found during update.`);
            }
            throw error; // Preserve other specific error messages
        }
        throw new Error(`Failed to update contract line ${planId}: ${error}`);
    }
});

export const upsertContractLineTerms = withAuth(async (
    user,
    { tenant },
    contractLineId: string,
    billingTiming: 'arrears' | 'advance'
): Promise<void> => {
    const { knex } = await createTenantKnex();
    if (!tenant) {
        throw new Error("tenant context not found");
    }

    await withTransaction(knex, async (trx: Knex.Transaction) => {
        if (!await hasPermission(user, 'billing', 'update')) {
            throw new Error('Permission denied: Cannot update contract line terms');
        }

        const contractLine = await ContractLine.findById(trx, contractLineId);
        if (!contractLine) {
            throw new Error(`Contract line ${contractLineId} not found.`);
        }
        await assertContractLineIsAuthorable(trx, tenant, contractLineId);

        if (billingTiming === 'advance' && contractLine.contract_line_type !== 'Fixed') {
            throw new Error('Advance billing is only supported for fixed contract lines.');
        }

        // Update billing_timing directly on contract_lines table
        // (migration 20251025120000 added this column)
        await trx('contract_lines')
            .where({ tenant, contract_line_id: contractLineId })
            .update({
                billing_timing: billingTiming,
                updated_at: trx.fn.now(),
            });

        await syncRecurringServicePeriodsForContractLine(trx, {
            tenant,
            contractLineId,
            sourceRunPrefix: 'contract_line_terms_update',
        });

    });
});

export const deleteContractLine = withAuth(async (
    user,
    { tenant },
    planId: string
): Promise<DeletionValidationResult & { success: boolean; deleted?: boolean }> => {
    try {
        const { knex } = await createTenantKnex();
        if (!tenant) {
            return {
                success: false,
                canDelete: false,
                code: 'VALIDATION_FAILED',
                message: 'Tenant context not found',
                dependencies: [],
                alternatives: []
            };
        }

        const contractsWithClients = await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!await hasPermission(user, 'billing', 'delete')) {
                throw new Error('Permission denied: Cannot delete contract lines');
            }

            return await trx('contract_lines as cl')
                .join('contracts as c', function() {
                    this.on('cl.contract_id', '=', 'c.contract_id')
                        .andOn('cl.tenant', '=', 'c.tenant');
                })
                .leftJoin('client_contracts as cc', function() {
                    this.on('c.contract_id', '=', 'cc.contract_id')
                        .andOn('cc.is_active', '=', trx.raw('?', [true]));
                })
                .leftJoin('clients as cli', function() {
                    this.on('cc.client_id', '=', 'cli.client_id')
                        .andOn('cli.tenant', '=', trx.raw('?', [tenant]));
                })
                .where('cl.contract_line_id', planId)
                .where('cl.tenant', tenant)
                .whereNotNull('cl.contract_id')
                .select('c.contract_id');
        });

        if (contractsWithClients.length > 0) {
            return {
                success: false,
                canDelete: false,
                code: 'DEPENDENCIES_EXIST',
                message: 'Cannot delete contract line: It is currently assigned to one or more clients.',
                dependencies: [{ type: 'contract', count: contractsWithClients.length, label: 'contract' }],
                alternatives: []
            };
        }

        const result = await deleteEntityWithValidation('contract_line', planId, knex, tenant, async (trx) => {
            await ContractLine.delete(trx, planId);
        });

        return {
            ...result,
            success: result.deleted === true,
            deleted: result.deleted
        };
    } catch (error) {
        console.error('Error deleting contract line:', error);
        return {
            success: false,
            canDelete: false,
            code: 'VALIDATION_FAILED',
            message: 'Failed to delete contract line',
            dependencies: [],
            alternatives: []
        };
    }
});

/**
 * Gets the combined fixed plan configuration (plan-level and service-level)
 * Fetches legacy partial-period compatibility settings from contract_line_fixed_config
 * and base_rate from contract_line_service_fixed_config.
 */
export const getCombinedFixedPlanConfiguration = withAuth(async (
    user,
    { tenant },
    planId: string,
    serviceId: string
): Promise<{
    base_rate?: number | null;
    enable_proration: boolean;
    billing_cycle_alignment: 'start' | 'end' | 'prorated';
    config_id?: string; // Service-specific config ID
} | null> => {
    try {
        const { knex } = await createTenantKnex(); // Get knex instance
        if (!tenant) {
            throw new Error("tenant context not found");
        }

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!await hasPermission(user, 'billing', 'read')) {
                throw new Error('Permission denied: Cannot read contract line configurations');
            }

            // --- Fetch Plan-Level Config (Base Rate, Partial-Period Compatibility) ---
            // Use the existing getContractLineFixedConfig action which should now return base_rate
            const planConfig = await getContractLineFixedConfig(planId);

            // Default values if plan-level config doesn't exist
            const contract_line_base_rate = planConfig?.base_rate ?? null; // Get base_rate from plan config
            const enable_proration = planConfig?.enable_proration ?? false;
            const billing_cycle_alignment = resolveBillingCycleAlignmentForCompatibility({
                billingCycleAlignment: planConfig?.billing_cycle_alignment,
                enableProration: planConfig?.enable_proration,
            });

            // --- Fetch Service-Level Config ID (Optional, if needed elsewhere) ---
            // We no longer need service-level config to get the base rate for the combined view.
            // We might still need the config_id if the caller uses it.
            const configService = new ContractLineServiceConfigurationService(trx, tenant);
            const serviceBaseConfig = await configService.getConfigurationForService(planId, serviceId);
            const config_id: string | undefined = serviceBaseConfig?.config_id;

            // Base rate now comes from planConfig fetched above
            const base_rate = contract_line_base_rate;

            // --- Combine Results ---
            // Return null only if BOTH plan and service config are missing? Or just if service config is missing?
            // Current logic: returns combined data even if service config (base_rate) is missing.
            // If serviceBaseConfig is required, uncomment the check below:
            // if (!serviceBaseConfig) {
            //     return null;
            // }

            return {
                base_rate: base_rate,
                enable_proration: enable_proration,
                billing_cycle_alignment: billing_cycle_alignment,
                config_id: config_id
            };
        });

    } catch (error) {
        console.error('Error fetching combined fixed plan configuration:', error);
        if (error instanceof Error) {
            throw error; // Preserve specific error messages
        }
        throw new Error(`Failed to fetch combined fixed plan configuration for plan ${planId}, service ${serviceId}: ${error}`);
    }
});

/**
 * Gets only the plan-level fixed configuration (partial-period compatibility)
 */
export const getContractLineFixedConfig = withAuth(async (
    user,
    { tenant },
    planId: string
): Promise<IContractLineFixedConfig | null> => {
    try {
        const isBypass = process.env.E2E_AUTH_BYPASS === 'true';

        const { knex } = await createTenantKnex();
        if (!tenant) {
            throw new Error("tenant context not found");
        }

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!isBypass && !await hasPermission(user, 'billing', 'read')) {
                throw new Error('Permission denied: Cannot read contract line configurations');
            }

            const model = new ContractLineFixedConfig(trx, tenant);
            return await model.getByPlanId(planId);
        });
    } catch (error) {
        console.error(`Error fetching contract_line_fixed_config for plan ${planId}:`, error);
        if (error instanceof Error) {
            throw error;
        }
        throw new Error(`Failed to fetch contract_line_fixed_config for plan ${planId}: ${error}`);
    }
});

/**
 * Updates the plan-level fixed configuration (partial-period compatibility) in
 * contract_line_fixed_config.
 * Uses upsert logic: creates if not exists, updates if exists.
 */
export const updateContractLineFixedConfig = withAuth(async (
    user,
    { tenant },
    planId: string,
    configData: Partial<Omit<IContractLineFixedConfig, 'contract_line_id' | 'tenant' | 'created_at' | 'updated_at'>>
): Promise<boolean> => {
    try {
        const { knex } = await createTenantKnex();
        if (!tenant) {
            throw new Error("tenant context not found");
        }

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!await hasPermission(user, 'billing', 'update')) {
                throw new Error('Permission denied: Cannot update contract line configurations');
            }

            // Fetch the existing plan to check its type
            const existingPlan = await ContractLine.findById(trx, planId); // Use ContractLine model directly
            if (!existingPlan) {
                throw new Error(`Contract Line with ID ${planId} not found.`);
            }
            await assertContractLineIsAuthorable(trx, tenant, planId);
            if (existingPlan.contract_line_type !== 'Fixed') {
                throw new Error(`Cannot update fixed plan configuration for non-fixed plan type: ${existingPlan.contract_line_type}`);
            }

            const model = new ContractLineFixedConfig(trx, tenant);
            const existingConfig = await model.getByPlanId(planId);

            // Prepare data for upsert, ensuring contract_line_id and tenant are included
            // Prepare data for upsert, ensuring contract_line_id, tenant, and base_rate are included
            const upsertData: Omit<IContractLineFixedConfig, 'created_at' | 'updated_at'> & { base_rate?: number | null } = {
                contract_line_id: planId,
                base_rate: configData.base_rate, // Include base_rate from input
                enable_proration: configData.enable_proration ?? false, // Provide default if undefined
                billing_cycle_alignment: resolveBillingCycleAlignmentForCompatibility({
                    billingCycleAlignment: configData.billing_cycle_alignment,
                    enableProration: configData.enable_proration,
                    fallbackAlignment: existingConfig?.billing_cycle_alignment,
                }),
                tenant: tenant,
            };

            return await model.upsert(upsertData);
        });

    } catch (error) {
        console.error(`Error upserting contract_line_fixed_config for plan ${planId}:`, error);
        if (error instanceof Error) {
            throw error;
        }
        throw new Error(`Failed to upsert contract_line_fixed_config for plan ${planId}: ${error}`);
    }
});


/**
 * Updates only the base_rate for a specific service within a fixed plan.
 * Interacts with contract_line_service_fixed_config.
 * Renamed from updateFixedPlanConfiguration.
 */
export const updatePlanServiceFixedConfigRate = withAuth(async (
    user,
    { tenant },
    planId: string,
    serviceId: string,
    baseRate: number | null // Only accept base_rate
): Promise<boolean> => {
    try {
        const { knex } = await createTenantKnex();
        if (!tenant) {
            throw new Error("tenant context not found");
        }

        return await withTransaction(knex, async (trx: Knex.Transaction) => {
            if (!await hasPermission(user, 'billing', 'update')) {
                throw new Error('Permission denied: Cannot update contract line configurations');
            }

            // Fetch the existing plan to check its type
            const existingPlan = await ContractLine.findById(trx, planId); // Use ContractLine model directly
            if (!existingPlan) {
                throw new Error(`Contract Line with ID ${planId} not found.`);
            }
            await assertContractLineIsAuthorable(trx, tenant, planId);
            if (existingPlan.contract_line_type !== 'Fixed') {
                throw new Error(`Cannot update fixed service config rate for non-fixed plan type: ${existingPlan.contract_line_type}`);
            }

            // Create configuration service
            const configService = new ContractLineServiceConfigurationService(trx, tenant);

            // Get existing configuration for this plan and service
            let config = await configService.getConfigurationForService(planId, serviceId);

            if (!config) {
                // If no configuration exists, create a new one with the provided base_rate
                console.log(`Creating new fixed plan service configuration for plan ${planId}, service ${serviceId}`);

                const configId = await configService.createConfiguration(
                    { // Base config data
                        contract_line_id: planId,
                        service_id: serviceId,
                        configuration_type: 'Fixed',
                        tenant
                    },
                    { // Type config data (only base_rate now)
                        base_rate: baseRate
                    }
                    // No legacy partial-period compatibility data is passed here anymore
                );

                return !!configId;
            } else {
                // Update existing configuration's base_rate
                console.log(`Updating fixed plan service configuration base_rate for plan ${planId}, service ${serviceId}`);

                // Prepare fixed config update data (only base_rate)
                const fixedConfigData: Partial<IContractLineServiceFixedConfig> = {
                     base_rate: baseRate
                };

                // Update the configuration using the service
                return await configService.updateConfiguration(
                    config.config_id,
                    undefined, // No base config updates needed
                    fixedConfigData // Only contains base_rate
                );
            }
        });
    } catch (error) {
        console.error('Error updating fixed plan service config rate:', error);
        if (error instanceof Error) {
            throw error; // Preserve specific error messages
        }
        throw new Error(`Failed to update fixed plan service config rate for plan ${planId}, service ${serviceId}: ${error}`);
    }
});
