'use server'

import { createTenantKnex } from '@alga-psa/db';
import { withAuth } from '@alga-psa/auth';
import { hasPermission } from '@alga-psa/auth/rbac';
import type { RenewalWorkItemStatus } from '@alga-psa/types';
import { deriveClientContractStatus } from '@alga-psa/shared/billingClients';
import type { Knex } from 'knex';


// Type definitions for reports
export interface ContractRevenue {
  contract_name: string;
  client_name: string;
  monthly_recurring: number;
  total_billed_ytd: number;
  status: 'active' | 'upcoming' | 'expired';
}

export interface ContractExpiration {
  client_contract_id?: string;
  contract_name: string;
  client_name: string;
  end_date: string;
  decision_due_date?: string | null;
  renewal_mode?: 'none' | 'manual' | 'auto' | null;
  queue_status?: RenewalWorkItemStatus | null;
  days_until_expiration: number;
  monthly_value: number;
  auto_renew: boolean;
}

export interface BucketUsage {
  contract_name: string;
  client_name: string;
  total_hours: number;
  used_hours: number;
  remaining_hours: number;
  utilization_percentage: number;
  overage_hours: number;
}

export interface Profitability {
  contract_name: string;
  client_name: string;
  revenue: number;
  cost: number;
  profit: number;
  margin_percentage: number;
}

export interface ContractReportSummary {
  totalMRR: number;
  totalYTD: number;
  activeContractCount: number;
  atRiskDecisionCount: number;
}

type ContractRevenueFactRow = {
  item_id: string;
  client_contract_id: string | null;
  invoice_date: string | Date | null;
  net_amount: string | number | null;
  item_detail_id?: string | null;
  service_period_end?: string | Date | null;
  allocated_amount?: string | number | null;
};

const EXCLUDED_INVOICE_STATUSES = ['draft', 'Draft', 'cancelled', 'Cancelled', 'canceled', 'Canceled'] as const;

function normalizeDateOnly(value: string | Date | null | undefined): string | null {
  if (!value) {
    return null;
  }

  if (value instanceof Date) {
    return value.toISOString().slice(0, 10);
  }

  return value.slice(0, 10);
}

function isDateWithinRange(
  value: string | Date | null | undefined,
  startInclusive: string,
  endExclusive: string
): boolean {
  const normalized = normalizeDateOnly(value);
  if (!normalized) {
    return false;
  }

  return normalized >= startInclusive && normalized < endExclusive;
}

async function getContractRevenueYtdByAssignment(
  knex: Knex,
  tenant: string,
  yearStartDateOnly: string,
  nextYearStartDateOnly: string
): Promise<Map<string, number>> {
  // Contract revenue is the report family that intentionally pivots to
  // canonical recurring service periods when detail rows exist. Expiration and
  // renewal reporting below stay assignment-date based instead.
  const revenueFactRows = await knex('invoice_charges as ic')
    .join('invoices as inv', function joinInvoices() {
      this.on('ic.invoice_id', '=', 'inv.invoice_id').andOn('ic.tenant', '=', 'inv.tenant');
    })
    .leftJoin('invoice_charge_details as iid', function joinChargeDetails() {
      this.on('ic.item_id', '=', 'iid.item_id').andOn('ic.tenant', '=', 'iid.tenant');
    })
    .leftJoin('invoice_charge_fixed_details as iifd', function joinFixedDetails() {
      this.on('iid.item_detail_id', '=', 'iifd.item_detail_id').andOn('iid.tenant', '=', 'iifd.tenant');
    })
    .where({ 'ic.tenant': tenant })
    .whereNotIn('inv.status', EXCLUDED_INVOICE_STATUSES)
    .whereNotNull('ic.client_contract_id')
    .select(
      'ic.item_id',
      'ic.client_contract_id',
      'inv.invoice_date',
      'ic.net_amount',
      'iid.item_detail_id',
      'iid.service_period_end',
      'iifd.allocated_amount'
    ) as ContractRevenueFactRow[];

  const rowsByItemId = new Map<string, ContractRevenueFactRow[]>();
  for (const row of revenueFactRows) {
    const existing = rowsByItemId.get(row.item_id) ?? [];
    existing.push(row);
    rowsByItemId.set(row.item_id, existing);
  }

  const totalsByAssignment = new Map<string, number>();

  for (const itemRows of rowsByItemId.values()) {
    const firstRow = itemRows[0];
    const clientContractId = firstRow?.client_contract_id;
    if (!clientContractId) {
      continue;
    }

    const detailRows = itemRows.filter(
      (row) => typeof row.item_detail_id === 'string' && row.item_detail_id.length > 0
    );
    const hasCanonicalServicePeriods = detailRows.some(
      (row) => typeof normalizeDateOnly(row.service_period_end) === 'string'
    );

    let chargeAmount = 0;

    if (hasCanonicalServicePeriods) {
      const fixedDetailRows = detailRows.filter((row) => row.allocated_amount !== null && row.allocated_amount !== undefined);
      if (fixedDetailRows.length > 0) {
        chargeAmount = fixedDetailRows
          .filter((row) => isDateWithinRange(row.service_period_end, yearStartDateOnly, nextYearStartDateOnly))
          .reduce((sum, row) => sum + (Number(row.allocated_amount ?? 0) || 0), 0);
      } else if (
        detailRows.some((row) => isDateWithinRange(row.service_period_end, yearStartDateOnly, nextYearStartDateOnly))
      ) {
        chargeAmount = Number(firstRow.net_amount ?? 0) || 0;
      }
    } else if (isDateWithinRange(firstRow.invoice_date, yearStartDateOnly, nextYearStartDateOnly)) {
      chargeAmount = Number(firstRow.net_amount ?? 0) || 0;
    }

    if (chargeAmount === 0) {
      continue;
    }

    totalsByAssignment.set(clientContractId, (totalsByAssignment.get(clientContractId) ?? 0) + chargeAmount);
  }

  return totalsByAssignment;
}

const mapAssignmentStatusToRevenueStatus = (
  status: ReturnType<typeof deriveClientContractStatus>
): ContractRevenue['status'] | null => {
  if (status === 'draft') {
    return 'upcoming';
  }
  if (status === 'terminated') {
    return null;
  }
  return status;
};

/**
 * Get contract revenue report data
 * Shows monthly recurring revenue and year-to-date billing by contract
 */
export const getContractRevenueReport = withAuth(async (user, { tenant }): Promise<ContractRevenue[]> => {
  if (!await hasPermission(user, 'billing', 'read')) {
    throw new Error('Permission denied: billing read required');
  }
  try {
    const { knex } = await createTenantKnex();

    const today = new Date();
    const yearStartDateOnly = new Date(Date.UTC(today.getUTCFullYear(), 0, 1, 0, 0, 0, 0)).toISOString().slice(0, 10);
    const nextYearStartDateOnly = new Date(Date.UTC(today.getUTCFullYear() + 1, 0, 1, 0, 0, 0, 0)).toISOString().slice(0, 10);
    const invoiceMap = await getContractRevenueYtdByAssignment(
      knex,
      tenant,
      yearStartDateOnly,
      nextYearStartDateOnly
    );

    const data = await knex('client_contracts as cc')
      .join('contracts as c', function joinContracts() {
        this.on('cc.contract_id', '=', 'c.contract_id').andOn('cc.tenant', '=', 'c.tenant');
      })
      .leftJoin('clients as cl', function joinClients() {
        this.on('cc.client_id', '=', 'cl.client_id').andOn('cc.tenant', '=', 'cl.tenant');
      })
      .where({ 'cc.tenant': tenant })
      .andWhere((builder) => builder.whereNull('c.is_template').orWhere('c.is_template', false))
      .whereNotNull('c.owner_client_id')
      .select(
        'cc.client_contract_id',
        'cc.client_id',
        'cc.is_active',
        'cc.start_date',
        'cc.end_date',
        'c.contract_id',
        'c.contract_name',
        'cl.client_name'
      );

    const aggregatedMap = new Map<string, any>();

    for (const row of data) {
      const assignmentStatus = deriveClientContractStatus({
        isActive: Boolean(row.is_active),
        startDate: row.start_date,
        endDate: row.end_date,
        now: today,
      });
      const status = mapAssignmentStatusToRevenueStatus(assignmentStatus);
      if (!status) {
        continue;
      }

      aggregatedMap.set(row.client_contract_id, {
        client_contract_id: row.client_contract_id,
        contract_id: row.contract_id,
        contract_name: row.contract_name,
        client_name: row.client_name || 'Unknown Client',
        monthly_recurring: 0,
        total_billed_ytd: invoiceMap.get(row.client_contract_id) || 0,
        status,
      });
    }

    const contractLines = await knex('contract_lines as cl')
      .where({ 'cl.tenant': tenant })
      .select(
        'cl.contract_id',
        'cl.custom_rate'
      );

    for (const contractLine of contractLines) {
      const rateInCents = Math.round(Number(contractLine.custom_rate) || 0);
      for (const item of aggregatedMap.values()) {
        if (item.contract_id !== contractLine.contract_id) {
          continue;
        }
        item.monthly_recurring += rateInCents;
      }
    }

    return Array.from(aggregatedMap.values()).map(({ client_contract_id, contract_id, ...rest }) => rest);
  } catch (error) {
    console.error('Error fetching contract revenue report:', error);
    if (error instanceof Error) {
      throw error;
    }
    throw new Error(`Failed to fetch contract revenue report: ${error}`);
  }
});

/**
 * Get contract expiration report data
 * Track upcoming contract expirations and renewal opportunities
 */
export const getContractExpirationReport = withAuth(async (user, { tenant }): Promise<ContractExpiration[]> => {
  if (!await hasPermission(user, 'billing', 'read')) {
    throw new Error('Permission denied: billing read required');
  }
  try {
    const { knex } = await createTenantKnex();

    const today = new Date();

    const data = await knex('contracts as c')
      .join('client_contracts as cc', function joinClientContracts() {
        this.on('c.contract_id', '=', 'cc.contract_id').andOn('c.tenant', '=', 'cc.tenant');
      })
      .leftJoin('clients as cl', function joinClients() {
        this.on('cc.client_id', '=', 'cl.client_id').andOn('cc.tenant', '=', 'cl.tenant');
      })
      .leftJoin('contract_lines as cln', function joinLines() {
        this.on('c.contract_id', '=', 'cln.contract_id').andOn('c.tenant', '=', 'cln.tenant');
      })
      .leftJoin('default_billing_settings as dbs', function joinDefaultBillingSettings() {
        this.on('cc.tenant', '=', 'dbs.tenant');
      })
      .where({ 'c.tenant': tenant, 'cc.is_active': true })
      .andWhere((builder) => builder.whereNull('c.is_template').orWhere('c.is_template', false))
      .whereNotNull('c.owner_client_id')
      .whereNotNull('cc.end_date')
      .select(
        'cc.client_contract_id',
        'c.contract_id',
        'c.contract_name',
        'cl.client_name',
        'cc.is_active',
        'cc.start_date',
        'cc.end_date',
        'cc.decision_due_date',
        'cc.renewal_mode',
        'cc.use_tenant_renewal_defaults',
        'dbs.default_renewal_mode as tenant_default_renewal_mode',
        'cc.status as queue_status',
        knex.raw('COALESCE(cln.custom_rate, 0) as monthly_value')
      )
      .orderBy('cc.end_date', 'asc');

    const expirationMap = new Map<string, ContractExpiration>();

    for (const row of data) {
      const assignmentStatus = deriveClientContractStatus({
        isActive: Boolean(row.is_active),
        startDate: row.start_date,
        endDate: row.end_date,
        now: today,
      });
      if (assignmentStatus !== 'active') {
        continue;
      }

      const endDate = new Date(row.end_date);
      const daysUntilExpiration = Math.ceil((endDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
      const contractRenewalMode = row.renewal_mode === 'none' || row.renewal_mode === 'manual' || row.renewal_mode === 'auto'
        ? row.renewal_mode
        : null;
      const tenantDefaultRenewalMode =
        row.tenant_default_renewal_mode === 'none'
        || row.tenant_default_renewal_mode === 'manual'
        || row.tenant_default_renewal_mode === 'auto'
          ? row.tenant_default_renewal_mode
          : null;
      const useTenantRenewalDefaults = row.use_tenant_renewal_defaults !== false;
      const effectiveRenewalMode =
        (useTenantRenewalDefaults
          ? (tenantDefaultRenewalMode ?? contractRenewalMode)
          : (contractRenewalMode ?? tenantDefaultRenewalMode))
        ?? 'manual';

      const key = row.client_contract_id;
      const existing = expirationMap.get(key);
      if (existing) {
        existing.monthly_value += row.monthly_value || 0;
        continue;
      }

      expirationMap.set(key, {
        client_contract_id: row.client_contract_id,
        contract_name: row.contract_name,
        client_name: row.client_name || 'Unknown Client',
        end_date: endDate.toISOString().split('T')[0],
        decision_due_date: row.decision_due_date ? new Date(row.decision_due_date).toISOString().split('T')[0] : null,
        renewal_mode: effectiveRenewalMode,
        queue_status: row.queue_status ?? null,
        days_until_expiration: Math.max(0, daysUntilExpiration),
        monthly_value: row.monthly_value || 0,
        auto_renew: effectiveRenewalMode === 'auto'
      });
    }

    return Array.from(expirationMap.values()).map(({ client_contract_id: _ignored, ...item }) => item);
  } catch (error) {
    console.error('Error fetching contract expiration report:', error);
    if (error instanceof Error) {
      throw error;
    }
    throw new Error(`Failed to fetch contract expiration report: ${error}`);
  }
});

/**
 * Get bucket usage report data
 * Monitor bucket hours usage and identify overage situations
 */
export const getBucketUsageReport = withAuth(async (user, { tenant }): Promise<BucketUsage[]> => {
  if (!await hasPermission(user, 'billing', 'read')) {
    throw new Error('Permission denied: billing read required');
  }
  try {
    const { knex } = await createTenantKnex();

    // Query for bucket-type contract lines and their time tracking
    // Note: We're working with contracts that have bucket-type lines
    // For now, we'll show all bucket contracts without specific hour allocations
    // (as the bucket config is stored separately and not directly linked to contract_line_id)
    const data = await knex('contracts as c')
      .leftJoin('contract_lines as cl_line', function joinLines() {
        this.on('c.contract_id', '=', 'cl_line.contract_id').andOn('c.tenant', '=', 'cl_line.tenant');
      })
      .leftJoin('client_contracts as cc', function joinClientContracts() {
        this.on('c.contract_id', '=', 'cc.contract_id').andOn('c.tenant', '=', 'cc.tenant');
      })
      .leftJoin('clients as cl', function joinClients() {
        this.on('cc.client_id', '=', 'cl.client_id').andOn('cc.tenant', '=', 'cl.tenant');
      })
      .leftJoin('time_entries as te', function joinTimeEntries() {
        this.on('cl_line.contract_line_id', '=', 'te.contract_line_id').andOn('cl_line.tenant', '=', 'te.tenant');
      })
      .where({ 'c.tenant': tenant, 'cl_line.contract_line_type': 'Bucket' })
      .select(
        'c.contract_id',
        'c.contract_name',
        'cl.client_name',
        knex.raw('COALESCE(SUM(te.billable_duration), 0) as used_minutes')
      )
      .groupBy('c.contract_id', 'c.contract_name', 'cl.client_name');

    const bucketUsages: BucketUsage[] = data
      .filter((row: any) => row.contract_name) // Filter out null results
      .map((row: any) => {
        // For bucket contracts, use a default allocation if not specified
        // This is a reasonable default of 40 hours per week
        const totalHours = 40;
        const usedHours = row.used_minutes ? Math.round(row.used_minutes / 60) : 0;
        const remainingHours = Math.max(0, totalHours - usedHours);
        const utilizationPercentage = totalHours > 0 ? Math.round((usedHours / totalHours) * 100) : 0;
        const overageHours = Math.max(0, usedHours - totalHours);

        return {
          contract_name: row.contract_name,
          client_name: row.client_name || 'Unknown Client',
          total_hours: totalHours,
          used_hours: usedHours,
          remaining_hours: remainingHours,
          utilization_percentage: utilizationPercentage,
          overage_hours: overageHours
        };
      });

    return bucketUsages;
  } catch (error) {
    console.error('Error fetching bucket usage report:', error);
    if (error instanceof Error) {
      throw error;
    }
    throw new Error(`Failed to fetch bucket usage report: ${error}`);
  }
});

/**
 * Get profitability report data
 * Basic profit margins and revenue vs. cost analysis by contract
 */
export const getProfitabilityReport = withAuth(async (user, { tenant }): Promise<Profitability[]> => {
  if (!await hasPermission(user, 'billing', 'read')) {
    throw new Error('Permission denied: billing read required');
  }
  try {
    const { knex } = await createTenantKnex();

    const today = new Date();
    const yearStart = new Date(Date.UTC(today.getUTCFullYear(), 0, 1, 0, 0, 0, 0));
    const excludedInvoiceStatuses = ['draft', 'Draft', 'cancelled', 'Cancelled', 'canceled', 'Canceled'];

    // Get total revenue for the year using SQL aggregation to avoid duplication
    const revenueResult = await knex('invoices')
      .where({ tenant })
      .whereNotIn('status', excludedInvoiceStatuses)
      .whereRaw('invoice_date >= ?', [yearStart.toISOString()])
      .select(knex.raw('SUM(total_amount) as total_revenue')) as Array<{ total_revenue: string | number | null }>;

    // SUM(bigint) comes back as a string from pg; coerce to number for consistent math/formatting.
    const totalRevenue = Number(revenueResult[0]?.total_revenue ?? 0) || 0;

    // Get time entries and cost data for the year - simplified approach
    const timeEntries = await knex('time_entries as te')
      .where({ 'te.tenant': tenant })
      .whereRaw('te.start_time >= ?', [yearStart.toISOString()])
      .select(knex.raw('SUM(billable_duration) as total_minutes')) as { total_minutes: number }[];

    const totalMinutes = (timeEntries[0]?.total_minutes as number) || 0;
    const totalHours = totalMinutes / 60;
    const totalCost = totalHours * 5000; // $50/hr = 5000 cents/hr

    const totalProfit = totalRevenue - totalCost;
    const marginPercentage = totalRevenue > 0 ? Math.round((totalProfit / totalRevenue) * 100) : 0;

    console.log('[Profitability Report Debug] Calculation:', {
      totalRevenue,
      totalMinutes,
      totalHours,
      totalCost,
      totalProfit,
      marginPercentage
    });

    // For now, return a single aggregated profitability record
    const profitabilities: Profitability[] = [
      {
        contract_name: 'All Contracts',
        client_name: 'Aggregate',
        revenue: totalRevenue,
        cost: totalCost,
        profit: totalProfit,
        margin_percentage: marginPercentage
      }
    ];

    // For now, we'll just return the aggregated view
    return profitabilities;
  } catch (error) {
    console.error('Error fetching profitability report:', error);
    if (error instanceof Error) {
      throw error;
    }
    throw new Error(`Failed to fetch profitability report: ${error}`);
  }
});

/**
 * Get contract report summary statistics
 */
export const getContractReportSummary = withAuth(async (user, { tenant }): Promise<ContractReportSummary> => {
  if (!await hasPermission(user, 'billing', 'read')) {
    throw new Error('Permission denied: billing read required');
  }
  try {
    const { knex } = await createTenantKnex();

    const revenueData = await getContractRevenueReport();

    const totalMRR = revenueData.reduce((sum, item) => sum + item.monthly_recurring, 0);
    const today = new Date();
    const yearStartDateOnly = new Date(Date.UTC(today.getUTCFullYear(), 0, 1, 0, 0, 0, 0)).toISOString().slice(0, 10);
    const nextYearStartDateOnly = new Date(Date.UTC(today.getUTCFullYear() + 1, 0, 1, 0, 0, 0, 0)).toISOString().slice(0, 10);
    const totalYTD = Array.from(
      (
        await getContractRevenueYtdByAssignment(
          knex,
          tenant,
          yearStartDateOnly,
          nextYearStartDateOnly
        )
      ).values()
    ).reduce((sum, amount) => sum + amount, 0);

    const activeContractCount = revenueData.filter((item) => item.status === 'active').length;
    const summaryTodayDateOnly = today.toISOString().slice(0, 10);
    const inNinetyDays = new Date(today);
    inNinetyDays.setUTCDate(inNinetyDays.getUTCDate() + 90);
    const summaryNinetyDaysDateOnly = inNinetyDays.toISOString().slice(0, 10);

    const atRiskDecisions = await knex('client_contracts as cc')
      .join('contracts as c', function joinContracts() {
        this.on('cc.contract_id', '=', 'c.contract_id').andOn('cc.tenant', '=', 'c.tenant');
      })
      .where({
        'cc.tenant': tenant,
        'cc.is_active': true,
      })
      .andWhere((builder) => builder.whereNull('c.is_template').orWhere('c.is_template', false))
      .whereNotNull('c.owner_client_id')
      .whereNotNull('cc.decision_due_date')
      .andWhere((builder) => {
        builder.whereNull('cc.start_date').orWhere('cc.start_date', '<=', summaryTodayDateOnly);
      })
      .andWhere((builder) => {
        builder.whereNull('cc.end_date').orWhere('cc.end_date', '>=', summaryTodayDateOnly);
      })
      .andWhere('cc.decision_due_date', '>=', summaryTodayDateOnly)
      .andWhere('cc.decision_due_date', '<=', summaryNinetyDaysDateOnly)
      .countDistinct('cc.client_contract_id as count')
      .first() as { count: string } | undefined;
    const atRiskDecisionCount = Number(atRiskDecisions?.count ?? 0);

    return {
      totalMRR,
      totalYTD,
      activeContractCount,
      atRiskDecisionCount
    };
  } catch (error) {
    console.error('Error fetching contract report summary:', error);
    if (error instanceof Error) {
      throw error;
    }
    throw new Error(`Failed to fetch contract report summary: ${error}`);
  }
});
