'use server';

/* eslint-disable custom-rules/no-feature-to-feature-imports -- Client portal billing actions intentionally compose billing feature APIs for end-user self-service flows. */

import { getConnection } from '@alga-psa/db';
import { createTenantKnex } from '@alga-psa/db';
import { withTransaction } from '@alga-psa/db';
import { Knex } from 'knex';
import {
  IClientContractLine,
  IBillingResult,
  IBucketUsage,
  IQuote,
  IQuoteItem,
  IService,
  IQuoteWithClient,
  IUserWithRoles
} from '@alga-psa/types';
import {
  fetchInvoicesByClient,
  getInvoiceLineItems,
  getInvoiceForRendering
} from '@alga-psa/billing/actions/invoiceQueries';
import { getInvoiceTemplates } from '@alga-psa/billing/actions/invoiceTemplates';
import { finalizeInvoice, unfinalizeInvoice } from '@alga-psa/billing/actions/invoiceModification';
import { InvoiceViewModel, IInvoiceTemplate } from '@alga-psa/types';
import Invoice from '@alga-psa/billing/models/invoice';
import Quote from '@alga-psa/billing/models/quote';
import QuoteActivity from '@alga-psa/billing/models/quoteActivity';
import { recalculateQuoteFinancials } from '@alga-psa/billing/services';
import { withAuth } from '@alga-psa/auth';
import { scheduleInvoiceEmailAction, scheduleInvoiceZipAction } from '@alga-psa/billing/actions/invoiceJobActions';
import { JobService } from '@alga-psa/jobs';
import { JobStatus } from '@alga-psa/jobs';
import { normalizeLiveRecurringStorage } from '@alga-psa/shared/billingClients/recurrenceStorageModel';

/**
 * Get clientId from user's contact - avoids nested withAuth calls
 */
async function getClientIdFromUser(
  trx: Knex.Transaction,
  user: IUserWithRoles,
  tenant: string
): Promise<string | null> {
  if (!user.contact_id) return null;

  const contact = await trx('contacts')
    .where({
      contact_name_id: user.contact_id,
      tenant
    })
    .select('client_id')
    .first();

  return contact?.client_id || null;
}

/**
 * Check if user has billing read permission - avoids nested withAuth calls
 */
async function hasBillingPermission(
  trx: Knex.Transaction,
  user: IUserWithRoles,
  tenant: string
): Promise<boolean> {
  const permissions = await trx('role_permissions as rp')
    .join('permissions as p', 'rp.permission_id', 'p.permission_id')
    .join('user_roles as ur', function() {
      this.on('rp.role_id', '=', 'ur.role_id')
        .andOn('rp.tenant', '=', 'ur.tenant');
    })
    .where({
      'ur.user_id': user.user_id,
      'ur.tenant': tenant,
      'p.resource': 'billing',
      'p.action': 'read'
    })
    .first();

  return !!permissions;
}

async function getAuthorizedClientQuote(
  trx: Knex.Transaction,
  user: IUserWithRoles,
  tenant: string,
  quoteId: string,
  allowedStatuses?: string[]
): Promise<IQuote> {
  const clientId = await getClientIdFromUser(trx, user, tenant);
  if (!clientId) {
    throw new Error('Unauthorized');
  }

  const hasAccess = await hasBillingPermission(trx, user, tenant);
  if (!hasAccess) {
    throw new Error('Unauthorized to access quote data');
  }

  const quote = await Quote.getById(trx, tenant, quoteId);
  if (!quote || quote.client_id !== clientId || quote.is_template || quote.status === 'draft') {
    throw new Error('Quote not found or access denied');
  }

  if (allowedStatuses?.length && (!quote.status || !allowedStatuses.includes(quote.status))) {
    throw new Error('Quote is not in a valid state for this action');
  }

  return quote;
}

async function persistOptionalQuoteSelections(
  trx: Knex.Transaction,
  tenant: string,
  quoteId: string,
  quoteItems: IQuoteItem[],
  selectedOptionalQuoteItemIds: string[]
): Promise<{ selectedIds: string[]; deselectedIds: string[] }> {
  const optionalItems = quoteItems.filter((item) => item.is_optional);
  const optionalItemIds = new Set(optionalItems.map((item) => item.quote_item_id));
  const selectedIds = selectedOptionalQuoteItemIds.filter((itemId) => optionalItemIds.has(itemId));
  const selectedSet = new Set(selectedIds);

  for (const item of optionalItems) {
    await trx('quote_items')
      .where({ tenant, quote_item_id: item.quote_item_id })
      .update({
        is_selected: selectedSet.has(item.quote_item_id),
        updated_at: trx.fn.now(),
      });
  }

  await recalculateQuoteFinancials(trx, tenant, quoteId);

  return {
    selectedIds,
    deselectedIds: optionalItems
      .map((item) => item.quote_item_id)
      .filter((itemId) => !selectedSet.has(itemId)),
  };
}

export const getClientContractLine = withAuth(async (user, { tenant }): Promise<IClientContractLine | null> => {
  const knex = await getConnection(tenant);

  try {
    const plan = await withTransaction(knex, async (trx: Knex.Transaction) => {
      const clientId = await getClientIdFromUser(trx, user, tenant);
      if (!clientId) {
        throw new Error('Unauthorized');
      }

      // Query via client_contracts -> contracts -> contract_lines
      // (contracts are client-specific via client_contracts)
      return await trx('client_contracts as cc')
        .join('contracts as c', function() {
          this.on('cc.contract_id', '=', 'c.contract_id')
            .andOn('cc.tenant', '=', 'c.tenant');
        })
        .join('contract_lines as cl', function() {
          this.on('c.contract_id', '=', 'cl.contract_id')
            .andOn('c.tenant', '=', 'cl.tenant');
        })
        .leftJoin('service_categories as sc', function() {
          this.on('cl.service_category', '=', 'sc.category_id')
            .andOn('sc.tenant', '=', 'cl.tenant');
        })
        .where({
          'cc.client_id': clientId,
          'cc.tenant': tenant
        })
        .select(
          'cl.contract_line_id',
          'cl.contract_line_name',
          'cl.billing_frequency',
          'cl.billing_timing',
          'cl.cadence_owner',
          'cl.service_category',
          'cl.custom_rate',
          'cl.contract_id',
          'cl.tenant',
          'cc.client_id',
          'cc.start_date',
          'cc.end_date',
          'sc.category_name as service_category_name'
        )
        .first();
    });

    return plan ? normalizeLiveRecurringStorage(plan) : null;
  } catch (error) {
    console.error('Error fetching client contract line:', error);
    throw new Error('Failed to fetch contract line');
  }
});

/**
 * Fetch all invoices for the current client
 */
export const getClientInvoices = withAuth(async (user, { tenant }): Promise<InvoiceViewModel[]> => {
  const knex = await getConnection(tenant);

  try {
    // Get clientId and check permissions in a single transaction
    const clientId = await withTransaction(knex, async (trx: Knex.Transaction) => {
      const id = await getClientIdFromUser(trx, user, tenant);
      if (!id) {
        throw new Error('Unauthorized');
      }

      const hasAccess = await hasBillingPermission(trx, user, tenant);
      if (!hasAccess) {
        throw new Error('Unauthorized to access invoice data');
      }

      return id;
    });

    // Directly fetch only invoices for the current client
    const invoices = await fetchInvoicesByClient(clientId);
    // Filter out draft invoices - only finalized invoices should be visible in client portal
    // An invoice is finalized when finalized_at is set (not null)
    return invoices.filter(invoice => invoice.finalized_at != null);
  } catch (error) {
    console.error('Error fetching client invoices:', error);
    throw new Error('Failed to fetch invoices');
  }
});

export const getClientQuotes = withAuth(async (user, { tenant }): Promise<IQuoteWithClient[]> => {
  const knex = await getConnection(tenant);

  try {
    const clientId = await withTransaction(knex, async (trx: Knex.Transaction) => {
      const id = await getClientIdFromUser(trx, user, tenant);
      if (!id) {
        throw new Error('Unauthorized');
      }

      const hasAccess = await hasBillingPermission(trx, user, tenant);
      if (!hasAccess) {
        throw new Error('Unauthorized to access quote data');
      }

      return id;
    });

    const quotes = await Quote.listByClient(knex, tenant, clientId);
    return quotes.filter((quote) => quote.status && quote.status !== 'draft');
  } catch (error) {
    console.error('Error fetching client quotes:', error);
    throw new Error('Failed to fetch quotes');
  }
});

export const getClientQuoteById = withAuth(async (user, { tenant }, quoteId: string): Promise<IQuote> => {
  const knex = await getConnection(tenant);

  try {
    return await withTransaction(knex, async (trx: Knex.Transaction) => {
      const quote = await getAuthorizedClientQuote(trx, user, tenant, quoteId);

      if (!quote.viewed_at) {
        const viewedAt = new Date().toISOString();

        const markedViewed = await trx('quotes')
          .where({ tenant, quote_id: quoteId })
          .whereNull('viewed_at')
          .update({
            viewed_at: viewedAt,
            updated_at: trx.fn.now(),
            updated_by: user.user_id,
          });

        if (markedViewed) {
          await QuoteActivity.create(trx, tenant, {
            quote_id: quoteId,
            activity_type: 'viewed',
            description: 'Quote viewed by client in portal',
            performed_by: user.user_id,
            metadata: {
              viewed_at: viewedAt,
            },
          });
        }
      }

      const updatedQuote = await Quote.getById(trx, tenant, quoteId);
      if (!updatedQuote) {
        throw new Error('Quote not found');
      }

      return updatedQuote;
    });
  } catch (error) {
    console.error('Error fetching client quote details:', error);
    throw new Error('Failed to fetch quote details');
  }
});

export const updateClientQuoteSelections = withAuth(async (
  user,
  { tenant },
  quoteId: string,
  selectedOptionalQuoteItemIds: string[]
): Promise<IQuote> => {
  const knex = await getConnection(tenant);

  try {
    return await withTransaction(knex, async (trx: Knex.Transaction) => {
      const quote = await getAuthorizedClientQuote(trx, user, tenant, quoteId, ['sent']);

      await persistOptionalQuoteSelections(
        trx,
        tenant,
        quoteId,
        quote.quote_items || [],
        selectedOptionalQuoteItemIds
      );

      const updatedQuote = await Quote.getById(trx, tenant, quoteId);
      if (!updatedQuote) {
        throw new Error('Quote not found after updating selections');
      }

      return updatedQuote;
    });
  } catch (error) {
    console.error('Error updating client quote selections:', error);
    throw new Error('Failed to update quote selections');
  }
});

export const acceptClientQuote = withAuth(async (
  user,
  { tenant },
  quoteId: string,
  selectedOptionalQuoteItemIds: string[] = []
): Promise<IQuote> => {
  const knex = await getConnection(tenant);

  try {
    return await withTransaction(knex, async (trx: Knex.Transaction) => {
      const quote = await getAuthorizedClientQuote(trx, user, tenant, quoteId, ['sent']);

      const { selectedIds, deselectedIds } = await persistOptionalQuoteSelections(
        trx,
        tenant,
        quoteId,
        quote.quote_items || [],
        selectedOptionalQuoteItemIds
      );

      const acceptedAt = new Date().toISOString();
      await Quote.update(trx, tenant, quoteId, {
        status: 'accepted',
        accepted_at: acceptedAt,
        accepted_by: user.user_id,
        updated_by: user.user_id,
      });

      await QuoteActivity.create(trx, tenant, {
        quote_id: quoteId,
        activity_type: 'accepted',
        description: 'Quote accepted by client for MSP review',
        performed_by: user.user_id,
        metadata: {
          selected_optional_quote_item_ids: selectedIds,
          deselected_optional_quote_item_ids: deselectedIds,
        },
      });

      const acceptedQuote = await Quote.getById(trx, tenant, quoteId);
      if (!acceptedQuote) {
        throw new Error('Quote not found after acceptance');
      }

      return acceptedQuote;
    });
  } catch (error) {
    console.error('Error accepting client quote:', error);
    throw new Error('Failed to accept quote');
  }
});

export const rejectClientQuote = withAuth(async (
  user,
  { tenant },
  quoteId: string,
  rejectionReason: string
): Promise<IQuote> => {
  const knex = await getConnection(tenant);
  const trimmedReason = rejectionReason.trim();

  if (!trimmedReason) {
    throw new Error('A rejection comment is required');
  }

  try {
    return await withTransaction(knex, async (trx: Knex.Transaction) => {
      await getAuthorizedClientQuote(trx, user, tenant, quoteId, ['sent']);

      const rejectedAt = new Date().toISOString();
      await Quote.update(trx, tenant, quoteId, {
        status: 'rejected',
        rejected_at: rejectedAt,
        rejection_reason: trimmedReason,
        updated_by: user.user_id,
      });

      await QuoteActivity.create(trx, tenant, {
        quote_id: quoteId,
        activity_type: 'rejected',
        description: 'Quote rejected by client',
        performed_by: user.user_id,
        metadata: {
          rejection_reason: trimmedReason,
        },
      });

      const rejectedQuote = await Quote.getById(trx, tenant, quoteId);
      if (!rejectedQuote) {
        throw new Error('Quote not found after rejection');
      }

      return rejectedQuote;
    });
  } catch (error) {
    console.error('Error rejecting client quote:', error);
    throw new Error('Failed to reject quote');
  }
});

/**
 * Get invoice details by ID
 */
export const getClientInvoiceById = withAuth(async (user, { tenant }, invoiceId: string): Promise<InvoiceViewModel> => {
  const knex = await getConnection(tenant);

  try {
    // Get clientId, check permissions, and verify invoice in a single transaction
    await withTransaction(knex, async (trx: Knex.Transaction) => {
      const clientId = await getClientIdFromUser(trx, user, tenant);
      if (!clientId) {
        throw new Error('Unauthorized');
      }

      const hasAccess = await hasBillingPermission(trx, user, tenant);
      if (!hasAccess) {
        throw new Error('Unauthorized to access invoice data');
      }

      // Verify the invoice belongs to the client and is not a draft
      const invoiceCheck = await trx('invoices')
        .where({
          invoice_id: invoiceId,
          client_id: clientId,
          tenant
        })
        .whereNot('status', 'draft')
        .first();

      if (!invoiceCheck) {
        throw new Error('Invoice not found or access denied');
      }
    });

    // Get full invoice details
    return await getInvoiceForRendering(invoiceId);
  } catch (error) {
    console.error('Error fetching client invoice details:', error);
    throw new Error('Failed to fetch invoice details');
  }
});

/**
 * Get invoice line items
 */
export const getClientInvoiceLineItems = withAuth(async (user, { tenant }, invoiceId: string) => {
  const knex = await getConnection(tenant);

  try {
    // Get clientId, check permissions, and verify invoice in a single transaction
    await withTransaction(knex, async (trx: Knex.Transaction) => {
      const clientId = await getClientIdFromUser(trx, user, tenant);
      if (!clientId) {
        throw new Error('Unauthorized');
      }

      const hasAccess = await hasBillingPermission(trx, user, tenant);
      if (!hasAccess) {
        throw new Error('Unauthorized to access invoice data');
      }

      // Verify the invoice belongs to the client and is not a draft
      const invoiceCheck = await trx('invoices')
        .where({
          invoice_id: invoiceId,
          client_id: clientId,
          tenant
        })
        .whereNot('status', 'draft')
        .first();

      if (!invoiceCheck) {
        throw new Error('Invoice not found or access denied');
      }
    });

    // Get invoice items
    return await getInvoiceLineItems(invoiceId);
  } catch (error) {
    console.error('Error fetching client invoice line items:', error);
    throw new Error('Failed to fetch invoice line items');
  }
});

/**
 * Get invoice templates
 */
export const getClientInvoiceTemplates = withAuth(async (user, { tenant }): Promise<IInvoiceTemplate[]> => {
  try {
    // Get all templates (both standard and tenant-specific)
    return await getInvoiceTemplates();
  } catch (error) {
    console.error('Error fetching invoice templates:', error);
    throw new Error('Failed to fetch invoice templates');
  }
});

/**
 * Download invoice PDF response
 */
export interface DownloadPdfResult {
  success: boolean;
  fileId?: string;
  error?: string;
}

/**
 * Download invoice PDF - schedules job, waits for completion, returns file ID
 */
export const downloadClientInvoicePdf = withAuth(async (user, { tenant }, invoiceId: string): Promise<DownloadPdfResult> => {
  const knex = await getConnection(tenant);

  try {
    // Get clientId, check permissions, and verify invoice in a single transaction
    await withTransaction(knex, async (trx: Knex.Transaction) => {
      const clientId = await getClientIdFromUser(trx, user, tenant);
      if (!clientId) {
        throw new Error('Unauthorized');
      }

      const hasAccess = await hasBillingPermission(trx, user, tenant);
      if (!hasAccess) {
        throw new Error('Unauthorized to access invoice data');
      }

      // Verify the invoice belongs to the client and is not a draft
      const invoiceCheck = await trx('invoices')
        .where({
          invoice_id: invoiceId,
          client_id: clientId,
          tenant
        })
        .whereNot('status', 'draft')
        .first();

      if (!invoiceCheck) {
        throw new Error('Invoice not found or access denied');
      }
    });

    // Schedule PDF generation
    const result = await scheduleInvoiceZipAction([invoiceId]);

    if (!result?.jobId) {
      return { success: false, error: 'Failed to start PDF generation' };
    }

    // Poll until job completes
    const status = await pollJobUntilComplete(result.jobId, tenant);

    if (status.status === 'completed' && status.fileId) {
      return { success: true, fileId: status.fileId };
    } else {
      return { success: false, error: status.error || 'PDF generation failed' };
    }
  } catch (error) {
    console.error('Error downloading invoice PDF:', error);
    throw new Error('Failed to download invoice PDF');
  }
});

/**
 * Send invoice email response
 */
export interface SendEmailResult {
  success: boolean;
  error?: string;
}

/**
 * Send invoice email - schedules job, waits for completion
 */
export const sendClientInvoiceEmail = withAuth(async (user, { tenant }, invoiceId: string): Promise<SendEmailResult> => {
  const knex = await getConnection(tenant);

  try {
    // Get clientId, check permissions, and verify invoice in a single transaction
    await withTransaction(knex, async (trx: Knex.Transaction) => {
      const clientId = await getClientIdFromUser(trx, user, tenant);
      if (!clientId) {
        throw new Error('Unauthorized');
      }

      const hasAccess = await hasBillingPermission(trx, user, tenant);
      if (!hasAccess) {
        throw new Error('Unauthorized to access invoice data');
      }

      // Verify the invoice belongs to the client and is not a draft
      const invoiceCheck = await trx('invoices')
        .where({
          invoice_id: invoiceId,
          client_id: clientId,
          tenant
        })
        .whereNot('status', 'draft')
        .first();

      if (!invoiceCheck) {
        throw new Error('Invoice not found or access denied');
      }
    });

    // Schedule email sending
    const result = await scheduleInvoiceEmailAction([invoiceId]);

    if (!result?.jobId) {
      return { success: false, error: 'Failed to start email sending' };
    }

    // Poll until job completes
    const status = await pollJobUntilComplete(result.jobId, tenant);

    if (status.status === 'completed') {
      return { success: true };
    } else {
      return { success: false, error: status.error || 'Email sending failed' };
    }
  } catch (error) {
    console.error('Error sending invoice email:', error);
    throw new Error('Failed to send invoice email');
  }
});

/**
 * Job status response for client portal
 */
export interface ClientJobStatus {
  status: 'pending' | 'processing' | 'completed' | 'failed';
  fileId?: string;
  error?: string;
}

/**
 * Get job status - internal helper for polling
 */
async function getJobStatus(jobId: string, tenant: string): Promise<ClientJobStatus> {
  const jobService = await JobService.create();
  const { knex } = await createTenantKnex(tenant);

  // Get job record
  const job = await knex('jobs')
    .where({ job_id: jobId, tenant })
    .first();

  if (!job) {
    throw new Error('Job not found');
  }

  // Map job status
  let status: ClientJobStatus['status'] = 'pending';
  if (job.status === JobStatus.Processing || job.status === JobStatus.Active) {
    status = 'processing';
  } else if (job.status === JobStatus.Completed) {
    status = 'completed';
  } else if (job.status === JobStatus.Failed) {
    status = 'failed';
  }

  // If completed, get the file_id from job details
  let fileId: string | undefined;
  if (status === 'completed') {
    const details = await jobService.getJobDetails(jobId);
    // Look for file_id in the metadata of completed steps
    for (const detail of details) {
      const metadata = detail.metadata as Record<string, unknown> | undefined;
      if (metadata?.file_id && typeof metadata.file_id === 'string') {
        fileId = metadata.file_id;
        break;
      }
    }
  }

  // If failed, get error message
  let error: string | undefined;
  if (status === 'failed' && job.metadata?.error) {
    error = job.metadata.error;
  }

  return { status, fileId, error };
}

/**
 * Poll job until completion or failure
 * Returns the final status with fileId if successful
 */
async function pollJobUntilComplete(
  jobId: string,
  tenant: string,
  maxAttempts: number = 30,
  intervalMs: number = 2000
): Promise<ClientJobStatus> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const status = await getJobStatus(jobId, tenant);

    if (status.status === 'completed' || status.status === 'failed') {
      return status;
    }

    // Wait before next poll
    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }

  // Timeout - job took too long
  return {
    status: 'failed',
    error: 'Job timed out. Please try again.'
  };
}

/**
 * Get job status for polling - used to check if PDF generation is complete
 */
export const getClientJobStatus = withAuth(async (user, { tenant }, jobId: string): Promise<ClientJobStatus> => {
  try {
    return await getJobStatus(jobId, tenant);
  } catch (error) {
    console.error('Error getting job status:', error);
    throw new Error('Failed to get job status');
  }
});

export const getCurrentUsage = withAuth(async (user, { tenant }): Promise<{
  bucketUsage: IBucketUsage | null;
  services: IService[];
}> => {
  const knex = await getConnection(tenant);

  try {
    const result = await withTransaction(knex, async (trx: Knex.Transaction) => {
      const clientId = await getClientIdFromUser(trx, user, tenant);
      if (!clientId) {
        throw new Error('Unauthorized');
      }

      const currentDate = new Date().toISOString().slice(0, 10);

      // Get current bucket usage if any
      const bucketUsage = await trx('bucket_usage')
        .select('*')
        .where({
          client_id: clientId,
          tenant
        })
        .andWhere('period_start', '<=', currentDate)
        .andWhere('period_end', '>', currentDate)
        .orderBy('period_start', 'desc')
        .first();

      // Get all services associated with the client's plan
      const services = await trx('service_catalog')
        .select('service_catalog.*')
        .join('contract_line_services', function() {
          this.on('service_catalog.service_id', '=', 'contract_line_services.service_id')
            .andOn('service_catalog.tenant', '=', 'contract_line_services.tenant')
        })
        .join('contract_lines as cl', function() {
          this.on('contract_line_services.contract_line_id', '=', 'cl.contract_line_id')
            .andOn('contract_line_services.tenant', '=', 'cl.tenant')
        })
        .join('client_contracts as cc', function() {
          this.on('cl.contract_id', '=', 'cc.contract_id')
            .andOn('cl.tenant', '=', 'cc.tenant')
        })
        .where({
          'cc.client_id': clientId,
          'cc.is_active': true,
          'service_catalog.tenant': tenant,
          'contract_line_services.tenant': tenant,
          'cl.tenant': tenant,
          'cc.tenant': tenant
        });

      return {
        bucketUsage,
        services
      };
    });

    return result;
  } catch (error) {
    console.error('Error fetching current usage:', error);
    throw new Error('Failed to fetch current usage');
  }
});

/**
 * Download quote PDF - looks up the stored PDF file_id for the quote.
 * If no PDF exists yet (quote was created before PDF storage was added),
 * generates and stores one on the fly.
 */
export const downloadClientQuotePdf = withAuth(async (
  user,
  { tenant },
  quoteId: string
): Promise<DownloadPdfResult> => {
  const knex = await getConnection(tenant);

  try {
    const quote = await withTransaction(knex, async (trx: Knex.Transaction) => {
      return getAuthorizedClientQuote(trx, user, tenant, quoteId);
    });

    // Look for an existing stored PDF document
    const doc = await knex('document_associations as da')
      .join('documents as d', function () {
        this.on('da.document_id', 'd.document_id')
          .andOn('da.tenant', 'd.tenant');
      })
      .where({
        'da.entity_id': quoteId,
        'da.entity_type': 'quote',
        'da.tenant': tenant,
      })
      .whereNotNull('d.file_id')
      .orderBy('da.created_at', 'desc')
      .select('d.file_id')
      .first<{ file_id: string } | undefined>();

    if (doc?.file_id) {
      return { success: true, fileId: doc.file_id };
    }

    // No stored PDF yet — generate one on the fly
    const { createPDFGenerationService } = await import('@alga-psa/billing/services');
    const pdfService = createPDFGenerationService(tenant);
    const fileRecord = await pdfService.generateAndStore({
      quoteId: quote.quote_id,
      quoteNumber: quote.quote_number ?? undefined,
      userId: user.user_id,
    });

    return { success: true, fileId: fileRecord.file_id };
  } catch (error) {
    console.error('Error downloading quote PDF:', error);
    return { success: false, error: 'Failed to download quote PDF' };
  }
});

/**
 * Client-portal location summary used for rendering location-grouped
 * quote/invoice detail pages. Returns only the locations referenced by the
 * given quote, scoped to the authenticated client user's own client_id.
 */
export interface ClientPortalLocationSummary {
  location_id: string;
  location_name?: string | null;
  address_line1?: string | null;
  address_line2?: string | null;
  address_line3?: string | null;
  city?: string | null;
  state_province?: string | null;
  postal_code?: string | null;
  country_code?: string | null;
  country_name?: string | null;
  region_code?: string | null;
}

export const getLocationsForClientQuote = withAuth(async (
  user,
  { tenant },
  quoteId: string,
): Promise<ClientPortalLocationSummary[]> => {
  const knex = await getConnection(tenant);

  return withTransaction(knex, async (trx: Knex.Transaction) => {
    // Authorizes + confirms the quote belongs to this portal user's client.
    const quote = await getAuthorizedClientQuote(trx, user, tenant, quoteId);
    if (!quote.client_id) return [];

    return trx('client_locations')
      .select<ClientPortalLocationSummary[]>(
        'location_id',
        'location_name',
        'address_line1',
        'address_line2',
        'address_line3',
        'city',
        'state_province',
        'postal_code',
        'country_code',
        'country_name',
        'region_code',
      )
      .where({ tenant, client_id: quote.client_id, is_active: true })
      .orderBy('is_default', 'desc')
      .orderBy('location_name', 'asc');
  });
});
