import {
  and,
  asc,
  eq,
  gte,
  ilike,
  inArray,
  isNull,
  lte,
  ne,
  or,
  sql,
  type SQL
} from 'drizzle-orm';
import {
  crmCustomers,
  crmOpportunities,
  crmOpportunityCustomers,
  crmQuotationCustomers,
  crmQuotations
} from '@/db/schema';
import { db } from '@/lib/db';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import {
  PROJECT_PARTY_OPTION_CATEGORY,
  resolveProjectPartyRole
} from '@/features/crm/shared/project-party';
import type {
  OutcomeBreakdownRow,
  OutcomeRevenueFilters,
  OutcomeRevenueRecord,
  RevenueAttributionFilters,
  RevenueAttributionSummary
} from './types';

type SupportedRevenueRole = 'end_customer' | 'billing_customer' | 'contractor' | 'consultant';

interface NormalizedProjectParty {
  customerId: string;
  roleCode: string;
}

type ProjectPartyTableAvailability = {
  opportunityCustomers: boolean;
  quotationCustomers: boolean;
};

let projectPartyTableAvailability: ProjectPartyTableAvailability | null = null;

const outcomeOpportunitySelect = {
  id: crmOpportunities.id,
  customerId: crmOpportunities.customerId,
  branchId: crmOpportunities.branchId,
  productType: crmOpportunities.productType,
  assignedToUserId: crmOpportunities.assignedToUserId,
  poAmount: crmOpportunities.poAmount,
  closedWonAt: crmOpportunities.closedWonAt,
  closedLostAt: crmOpportunities.closedLostAt
} as const;

function splitFilterValue(value?: string) {
  return value
    ?.split(',')
    .map((item) => item.trim())
    .filter(Boolean) ?? [];
}

function buildRevenueAttributionFilters(
  organizationId: string,
  filters: RevenueAttributionFilters
): SQL[] {
  const statuses = splitFilterValue(filters.status);
  const quotationTypes = splitFilterValue(filters.quotationType);
  const branches = splitFilterValue(filters.branch);
  const customers = splitFilterValue(filters.customer);
  const opportunities = splitFilterValue(filters.opportunity);

  return [
    eq(crmQuotations.organizationId, organizationId),
    isNull(crmQuotations.deletedAt),
    ...(statuses.length
      ? [inArray(crmQuotations.status, statuses)]
      : filters.includeCancelled
        ? []
        : [ne(crmQuotations.status, 'cancelled')]),
    ...(quotationTypes.length ? [inArray(crmQuotations.quotationType, quotationTypes)] : []),
    ...(branches.length ? [inArray(crmQuotations.branchId, branches)] : []),
    ...(customers.length ? [inArray(crmQuotations.customerId, customers)] : []),
    ...(opportunities.length ? [inArray(crmQuotations.opportunityId, opportunities)] : []),
    ...(filters.hot === 'true' ? [eq(crmQuotations.isHotProject, true)] : []),
    ...(filters.hot === 'false' ? [eq(crmQuotations.isHotProject, false)] : []),
    ...(filters.quotationDateFrom ? [gte(crmQuotations.quotationDate, new Date(filters.quotationDateFrom))] : []),
    ...(filters.quotationDateTo ? [lte(crmQuotations.quotationDate, new Date(filters.quotationDateTo))] : []),
    ...(filters.search
      ? [
          or(
            ilike(crmQuotations.code, `%${filters.search}%`),
            ilike(crmQuotations.projectName, `%${filters.search}%`),
            ilike(crmQuotations.projectLocation, `%${filters.search}%`),
            ilike(crmQuotations.reference, `%${filters.search}%`),
            ilike(crmQuotations.competitor, `%${filters.search}%`)
          )!
        ]
      : [])
  ];
}

function normalizeParties(
  rows: Array<{ customerId: string; role: string }>,
  roleOptions: Awaited<ReturnType<typeof getActiveOptionsByCategory>>
) {
  const normalized: NormalizedProjectParty[] = [];
  const seen = new Set<string>();

  for (const row of rows) {
    const resolvedRole = resolveProjectPartyRole(roleOptions, row.role);

    if (!resolvedRole) {
      continue;
    }

    const key = `${row.customerId}::${resolvedRole.code}`;

    if (seen.has(key)) {
      continue;
    }

    seen.add(key);
    normalized.push({
      customerId: row.customerId,
      roleCode: resolvedRole.code
    });
  }

  return normalized;
}

function groupNormalizedParties<TGroupKey extends string>(
  rows: Array<{ groupKey: TGroupKey; customerId: string; role: string }>,
  roleOptions: Awaited<ReturnType<typeof getActiveOptionsByCategory>>
) {
  const grouped = new Map<TGroupKey, Array<{ customerId: string; role: string }>>();

  for (const row of rows) {
    const existing = grouped.get(row.groupKey) ?? [];
    existing.push({
      customerId: row.customerId,
      role: row.role
    });
    grouped.set(row.groupKey, existing);
  }

  return new Map(
    [...grouped.entries()].map(([groupKey, groupRows]) => [
      groupKey,
      normalizeParties(groupRows, roleOptions)
    ])
  );
}

function pickAttributedParties(
  roleCode: SupportedRevenueRole,
  quotationParties: NormalizedProjectParty[],
  opportunityParties: NormalizedProjectParty[]
) {
  const authoritativeParties = quotationParties.length > 0 ? quotationParties : opportunityParties;

  if (roleCode === 'end_customer') {
    const endCustomers = authoritativeParties.filter((party) => party.roleCode === 'end_customer');

    if (endCustomers.length > 0) {
      return endCustomers;
    }

    return authoritativeParties.filter((party) => party.roleCode === 'billing_customer');
  }

  return authoritativeParties.filter((party) => party.roleCode === roleCode);
}

async function getProjectPartyTableAvailability(): Promise<ProjectPartyTableAvailability> {
  if (projectPartyTableAvailability) {
    return projectPartyTableAvailability;
  }

  const rows = await db.execute<{ tableName: string }>(sql`
    select table_name as "tableName"
    from information_schema.tables
    where table_schema = 'public'
      and table_name in ('crm_opportunity_customers', 'crm_quotation_customers')
  `);

  const available = new Set(rows.map((row) => row.tableName));

  projectPartyTableAvailability = {
    opportunityCustomers: available.has('crm_opportunity_customers'),
    quotationCustomers: available.has('crm_quotation_customers')
  };

  return projectPartyTableAvailability;
}

async function getRevenueByRole(
  organizationId: string,
  roleCode: SupportedRevenueRole,
  filters: RevenueAttributionFilters = {}
): Promise<RevenueAttributionSummary[]> {
  const tableAvailability = await getProjectPartyTableAvailability();
  const whereFilters = buildRevenueAttributionFilters(organizationId, filters);
  const where = whereFilters.length === 1 ? whereFilters[0] : and(...whereFilters);

  const quotations = await db
    .select({
      id: crmQuotations.id,
      opportunityId: crmQuotations.opportunityId,
      customerId: crmQuotations.customerId,
      totalAmount: crmQuotations.totalAmount
    })
    .from(crmQuotations)
    .where(where)
    .orderBy(asc(crmQuotations.quotationDate), asc(crmQuotations.code));

  if (quotations.length === 0) {
    return [];
  }

  const quotationIds = quotations.map((quotation) => quotation.id);
  const opportunityIds = [...new Set(quotations.map((quotation) => quotation.opportunityId).filter(Boolean))] as string[];

  const [roleOptions, quotationParties, opportunityParties] = await Promise.all([
    getActiveOptionsByCategory(PROJECT_PARTY_OPTION_CATEGORY, { organizationId }),
    tableAvailability.quotationCustomers
      ? db
          .select({
            quotationId: crmQuotationCustomers.quotationId,
            customerId: crmQuotationCustomers.customerId,
            role: crmQuotationCustomers.role
          })
          .from(crmQuotationCustomers)
          .where(
            and(
              eq(crmQuotationCustomers.organizationId, organizationId),
              inArray(crmQuotationCustomers.quotationId, quotationIds),
              isNull(crmQuotationCustomers.deletedAt)
            )
          )
      : [],
    tableAvailability.opportunityCustomers && opportunityIds.length
      ? db
          .select({
            opportunityId: crmOpportunityCustomers.opportunityId,
            customerId: crmOpportunityCustomers.customerId,
            role: crmOpportunityCustomers.role
          })
          .from(crmOpportunityCustomers)
          .where(
            and(
              eq(crmOpportunityCustomers.organizationId, organizationId),
              inArray(crmOpportunityCustomers.opportunityId, opportunityIds),
              isNull(crmOpportunityCustomers.deletedAt)
            )
          )
      : []
  ]);

  const quotationPartyMap = groupNormalizedParties(
    quotationParties.map((row) => ({
      groupKey: row.quotationId,
      customerId: row.customerId,
      role: row.role
    })),
    roleOptions
  );

  const opportunityPartyMap = groupNormalizedParties(
    opportunityParties.map((row) => ({
      groupKey: row.opportunityId,
      customerId: row.customerId,
      role: row.role
    })),
    roleOptions
  );

  const attributedCustomerIds = new Set<string>();
  const attributionByQuotation = new Map<string, NormalizedProjectParty[]>();

  for (const quotation of quotations) {
    const quotationAttributedParties = quotationPartyMap.get(quotation.id) ?? [];
    const opportunityAttributedParties = quotation.opportunityId
      ? (opportunityPartyMap.get(quotation.opportunityId) ?? [])
      : [];
    const attributedParties =
      quotationAttributedParties.length > 0 || opportunityAttributedParties.length > 0
        ? pickAttributedParties(roleCode, quotationAttributedParties, opportunityAttributedParties)
        : roleCode === 'end_customer' || roleCode === 'billing_customer'
          ? [{ customerId: quotation.customerId, roleCode }]
          : [];

    attributionByQuotation.set(quotation.id, attributedParties);

    for (const party of attributedParties) {
      attributedCustomerIds.add(party.customerId);
    }
  }

  if (attributedCustomerIds.size === 0) {
    return [];
  }

  const customers = await db
    .select({
      id: crmCustomers.id,
      code: crmCustomers.code,
      name: crmCustomers.name
    })
    .from(crmCustomers)
    .where(
      and(
        eq(crmCustomers.organizationId, organizationId),
        inArray(crmCustomers.id, [...attributedCustomerIds]),
        isNull(crmCustomers.deletedAt)
      )
    );

  const customerMap = new Map(customers.map((customer) => [customer.id, customer]));
  const summaryMap = new Map<string, RevenueAttributionSummary>();

  for (const quotation of quotations) {
    const attributedParties = attributionByQuotation.get(quotation.id) ?? [];

    for (const party of attributedParties) {
      const customer = customerMap.get(party.customerId);

      if (!customer) {
        continue;
      }

      const existing = summaryMap.get(customer.id);

      if (existing) {
        existing.revenue += quotation.totalAmount;

        if (!existing.quotationIds.includes(quotation.id)) {
          existing.quotationIds.push(quotation.id);
          existing.quotationCount += 1;
        }

        continue;
      }

      summaryMap.set(customer.id, {
        customerId: customer.id,
        customerCode: customer.code,
        customerName: customer.name,
        roleCode,
        revenue: quotation.totalAmount,
        quotationCount: 1,
        quotationIds: [quotation.id]
      });
    }
  }

  return [...summaryMap.values()].sort((a, b) => {
    if (b.revenue !== a.revenue) {
      return b.revenue - a.revenue;
    }

    return a.customerName.localeCompare(b.customerName);
  });
}

export async function getRevenueByEndCustomer(
  organizationId: string,
  filters: RevenueAttributionFilters = {}
) {
  return getRevenueByRole(organizationId, 'end_customer', filters);
}

export async function getRevenueByBillingCustomer(
  organizationId: string,
  filters: RevenueAttributionFilters = {}
) {
  return getRevenueByRole(organizationId, 'billing_customer', filters);
}

export async function getRevenueByContractor(
  organizationId: string,
  filters: RevenueAttributionFilters = {}
) {
  return getRevenueByRole(organizationId, 'contractor', filters);
}

export async function getRevenueByConsultant(
  organizationId: string,
  filters: RevenueAttributionFilters = {}
) {
  return getRevenueByRole(organizationId, 'consultant', filters);
}

function buildOutcomeRevenueFilters(
  organizationId: string,
  stage: 'closed_won' | 'closed_lost',
  filters: OutcomeRevenueFilters
): SQL[] {
  const dateColumn = stage === 'closed_won' ? crmOpportunities.closedWonAt : crmOpportunities.closedLostAt;

  return [
    eq(crmOpportunities.organizationId, organizationId),
    isNull(crmOpportunities.deletedAt),
    eq(crmOpportunities.pipelineStage, stage),
    ...(filters.branch ? [eq(crmOpportunities.branchId, filters.branch)] : []),
    ...(filters.salesman ? [eq(crmOpportunities.assignedToUserId, filters.salesman)] : []),
    ...(filters.productType ? [eq(crmOpportunities.productType, filters.productType)] : []),
    ...(filters.dateFrom ? [gte(dateColumn, new Date(filters.dateFrom))] : []),
    ...(filters.dateTo ? [lte(dateColumn, new Date(filters.dateTo))] : [])
  ];
}

async function getOutcomeRevenueRecords(
  organizationId: string,
  stage: 'closed_won' | 'closed_lost',
  filters: OutcomeRevenueFilters = {}
): Promise<OutcomeRevenueRecord[]> {
  const whereFilters = buildOutcomeRevenueFilters(organizationId, stage, filters);
  const where = whereFilters.length === 1 ? whereFilters[0] : and(...whereFilters);
  const opportunities = await db.select(outcomeOpportunitySelect).from(crmOpportunities).where(where);

  if (!opportunities.length) {
    return [];
  }

  const opportunityIds = opportunities.map((row) => row.id);
  const quotationRows = await db
    .select({
      opportunityId: crmQuotations.opportunityId,
      totalAmount: crmQuotations.totalAmount
    })
    .from(crmQuotations)
    .where(
      and(
        eq(crmQuotations.organizationId, organizationId),
        inArray(crmQuotations.opportunityId, opportunityIds),
        isNull(crmQuotations.deletedAt)
      )
    );
  const quotationTotals = new Map<string, number>();

  for (const row of quotationRows) {
    if (!row.opportunityId) {
      continue;
    }

    quotationTotals.set(row.opportunityId, (quotationTotals.get(row.opportunityId) ?? 0) + row.totalAmount);
  }

  return opportunities.map((row) => ({
    opportunityId: row.id,
    customerId: row.customerId,
    branchId: row.branchId,
    productType: row.productType,
    assignedToUserId: row.assignedToUserId,
    revenue: stage === 'closed_won' ? row.poAmount ?? quotationTotals.get(row.id) ?? 0 : quotationTotals.get(row.id) ?? 0,
    closedAt:
      (stage === 'closed_won' ? row.closedWonAt : row.closedLostAt)?.toISOString() ?? null
  }));
}

export async function getWonRevenue(
  organizationId: string,
  filters: OutcomeRevenueFilters = {}
) {
  return getOutcomeRevenueRecords(organizationId, 'closed_won', filters);
}

export async function getLostRevenue(
  organizationId: string,
  filters: OutcomeRevenueFilters = {}
) {
  return getOutcomeRevenueRecords(organizationId, 'closed_lost', filters);
}

export async function getLostByReason(
  organizationId: string,
  filters: OutcomeRevenueFilters = {}
): Promise<OutcomeBreakdownRow[]> {
  const records = await getLostRevenue(organizationId, filters);
  const opportunityIds = records.map((row) => row.opportunityId);

  if (!opportunityIds.length) {
    return [];
  }

  const [opportunities, lostReasonOptions] = await Promise.all([
    db
      .select({ id: crmOpportunities.id, lostReason: crmOpportunities.lostReason })
      .from(crmOpportunities)
      .where(inArray(crmOpportunities.id, opportunityIds)),
    getActiveOptionsByCategory('crm_lost_reason', { organizationId })
  ]);
  const lostReasonMap = new Map(
    lostReasonOptions.flatMap((item) => [
      [item.id, item.label] as const,
      [item.code, item.label] as const
    ])
  );
  const grouped = new Map<string, OutcomeBreakdownRow>();

  for (const row of records) {
    const opportunity = opportunities.find((item) => item.id === row.opportunityId);
    const key = opportunity?.lostReason ?? 'unknown';
    const current = grouped.get(key) ?? {
      key,
      label: lostReasonMap.get(key) ?? key,
      count: 0,
      revenue: 0
    };
    current.count += 1;
    current.revenue += row.revenue;
    grouped.set(key, current);
  }

  return [...grouped.values()].sort((a, b) => b.count - a.count || b.revenue - a.revenue);
}

export async function getLostByCompetitor(
  organizationId: string,
  filters: OutcomeRevenueFilters = {}
): Promise<OutcomeBreakdownRow[]> {
  const records = await getLostRevenue(organizationId, filters);
  const opportunityIds = records.map((row) => row.opportunityId);

  if (!opportunityIds.length) {
    return [];
  }

  const opportunities = await db
    .select({ id: crmOpportunities.id, lostCompetitor: crmOpportunities.lostCompetitor })
    .from(crmOpportunities)
    .where(inArray(crmOpportunities.id, opportunityIds));
  const grouped = new Map<string, OutcomeBreakdownRow>();

  for (const row of records) {
    const opportunity = opportunities.find((item) => item.id === row.opportunityId);
    const key = opportunity?.lostCompetitor?.trim() || 'unknown';
    const current = grouped.get(key) ?? { key, label: key, count: 0, revenue: 0 };
    current.count += 1;
    current.revenue += row.revenue;
    grouped.set(key, current);
  }

  return [...grouped.values()].sort((a, b) => b.count - a.count || b.revenue - a.revenue);
}

export async function getWinRate(
  organizationId: string,
  filters: OutcomeRevenueFilters = {}
) {
  const [wonRows, lostRows] = await Promise.all([
    getWonRevenue(organizationId, filters),
    getLostRevenue(organizationId, filters)
  ]);
  const denominator = wonRows.length + lostRows.length;

  if (denominator === 0) {
    return 0;
  }

  return Math.round((wonRows.length / denominator) * 10000) / 100;
}
