import {
  DEMAND_OPEN_STAGES,
  type AllocationStatus,
  type ContractStatus,
  type DemandStage,
  type ProductLine,
} from './ontology';

export const CUSTOMER_RELATIONSHIP_STATES = [
  'prospect',
  'contracted',
  'deployed',
  'former_customer',
] as const;
export type CustomerRelationshipState = (typeof CUSTOMER_RELATIONSHIP_STATES)[number];

export const GROWTH_FACETS = [
  'expansion_candidate',
  'renewal_due',
  'at_risk',
  'idle_supply_match',
  'coverage_gap',
  'data_stale',
] as const;
export type GrowthFacet = (typeof GROWTH_FACETS)[number];

export const GROWTH_SIGNAL_CATEGORIES = [
  'expansion',
  'renewal',
  'risk',
  'coverage',
  'supply',
] as const;
export type GrowthSignalCategory = (typeof GROWTH_SIGNAL_CATEGORIES)[number];

export const GROWTH_RULESET_VERSION = 'growth-r1-2026-08-13';

export interface LifecycleSourceRef {
  type: 'account' | 'demand_deal' | 'capacity_request' | 'allocation' | 'contract' | 'obligation' | 'activity' | 'commitment';
  id: string;
}

export interface GrowthSignal {
  code: string;
  category: GrowthSignalCategory;
  weight: number;
  explanation: string;
  sourceRefs: LifecycleSourceRef[];
}

export interface LifecycleDealSnapshot {
  id: string;
  stage: DemandStage;
  productLine: ProductLine;
  parentDealId?: string | null;
  msaExecuted: boolean;
  lastActivityAt?: Date | null;
}

export interface LifecycleRequestSnapshot {
  id: string;
  demandDealId: string;
  startsAt?: Date | null;
  endsAt?: Date | null;
  totalGpuHours?: number | null;
}

export interface LifecycleAllocationSnapshot {
  id: string;
  demandDealId?: string | null;
  status: AllocationStatus;
  gpuHours: number;
  startsAt: Date;
  endsAt: Date;
  holdExpiresAt?: Date | null;
}

export interface LifecycleContractSnapshot {
  id: string;
  status: ContractStatus;
  expiresAt?: Date | null;
  isAutoRenew: boolean;
  noticeDays?: number | null;
}

export interface LifecycleObligationSnapshot {
  id: string;
  contractId: string;
  dueAt: Date;
  completedAt?: Date | null;
}

export interface LifecycleIdleMatchSnapshot {
  requestId: string;
  commitmentId: string;
  score: number;
  rationale: string[];
}

export interface CustomerLifecycleInput {
  accountId: string;
  deals: LifecycleDealSnapshot[];
  requests: LifecycleRequestSnapshot[];
  allocations: LifecycleAllocationSnapshot[];
  contracts: LifecycleContractSnapshot[];
  obligations: LifecycleObligationSnapshot[];
  lastActivityAt?: Date | null;
  lastActivityId?: string | null;
  idleMatches?: LifecycleIdleMatchSnapshot[];
}

export interface CustomerLifecycleProjection {
  relationshipState: CustomerRelationshipState;
  facets: GrowthFacet[];
  score: number;
  scoreByCategory: Record<GrowthSignalCategory, number>;
  rulesetVersion: string;
  computedAt: string;
  signals: GrowthSignal[];
  blockers: string[];
  soldCapacityGpuHours: number;
  heldCapacityGpuHours: number;
}

const DAY_MS = 86_400_000;

export function requestHasCoverage(
  request: LifecycleRequestSnapshot,
  allocations: readonly LifecycleAllocationSnapshot[],
  now: Date,
): boolean {
  return allocations.some((allocation) => {
    if (allocation.demandDealId !== request.demandDealId || !reservesCapacity(allocation, now)) {
      return false;
    }
    if (request.startsAt && allocation.startsAt > request.startsAt) return false;
    if (request.endsAt && allocation.endsAt < request.endsAt) return false;
    if (request.totalGpuHours != null && allocation.gpuHours < request.totalGpuHours) return false;
    return true;
  });
}

export function evaluateCustomerLifecycle(
  input: CustomerLifecycleInput,
  now = new Date(),
): CustomerLifecycleProjection {
  const signals: GrowthSignal[] = [];
  const blockers: string[] = [];
  const dealById = new Map(input.deals.map((deal) => [deal.id, deal]));
  const activeAllocations = input.allocations.filter(
    (allocation) => allocation.status === 'active' && overlaps(allocation, now),
  );
  const liveContracts = input.contracts.filter(
    (contract) =>
      contract.status === 'executed' && (!contract.expiresAt || contract.expiresAt > now),
  );
  const futureCommitted = input.allocations.some(
    (allocation) => allocation.status === 'committed' && allocation.endsAt > now,
  );
  const wasCustomer =
    input.allocations.some((allocation) => allocation.status === 'completed' || allocation.endsAt <= now)
    || input.contracts.some((contract) =>
      contract.status === 'expired' || contract.status === 'terminated' || Boolean(contract.expiresAt && contract.expiresAt <= now),
    )
    || input.deals.some((deal) => deal.stage === 'closed_won');

  const relationshipState: CustomerRelationshipState = activeAllocations.length
    ? 'deployed'
    : liveContracts.length || futureCommitted
      ? 'contracted'
      : wasCustomer
        ? 'former_customer'
        : 'prospect';

  const openExpansionDeals = input.deals.filter(
    (deal) => deal.stage === 'expansion' && isOpenStage(deal.stage),
  );
  for (const deal of openExpansionDeals) {
    addSignal(signals, 'open_expansion_deal', 'expansion', 35, 'An explicit expansion opportunity is open.', [
      { type: 'demand_deal', id: deal.id },
    ]);
  }

  const uncoveredRequests = input.requests.filter(
    (request) => !requestHasCoverage(request, input.allocations, now),
  );
  for (const request of uncoveredRequests) {
    addSignal(signals, 'uncovered_capacity_request', 'coverage', 30, 'A recorded capacity requirement has no sold or reserved capacity covering its requested shape and window.', [
      { type: 'capacity_request', id: request.id },
      { type: 'demand_deal', id: request.demandDealId },
    ]);
  }

  const expansionAnchored = openExpansionDeals.length > 0 || uncoveredRequests.length > 0;
  const lastActivityAt = latestDate([
    input.lastActivityAt,
    ...input.deals.map((deal) => deal.lastActivityAt),
  ]);
  if (
    expansionAnchored
    && (relationshipState === 'deployed' || relationshipState === 'contracted')
    && lastActivityAt
    && now.getTime() - lastActivityAt.getTime() <= 30 * DAY_MS
  ) {
    addSignal(signals, 'recent_customer_activity', 'expansion', 15, 'Recent recorded customer activity strengthens an already-evidenced expansion or coverage opportunity.', [
      { type: input.lastActivityId ? 'activity' : 'account', id: input.lastActivityId ?? input.accountId },
    ]);
  }

  for (const match of input.idleMatches ?? []) {
    addSignal(signals, 'idle_supply_match', 'supply', 10, `Authoritative capacity matching found idle supply: ${match.rationale.join(' ')}`, [
      { type: 'capacity_request', id: match.requestId },
      { type: 'commitment', id: match.commitmentId },
    ]);
  }
  if (uncoveredRequests.length && !(input.idleMatches?.length)) {
    blockers.push('Customer-to-capacity matching is withheld until an allocation-level compliance decision is available.');
  }

  for (const contract of input.contracts) {
    if (!contract.expiresAt) continue;
    const daysToExpiry = Math.ceil((contract.expiresAt.getTime() - now.getTime()) / DAY_MS);
    const noticeAt = contract.noticeDays == null
      ? null
      : new Date(contract.expiresAt.getTime() - contract.noticeDays * DAY_MS);
    if (contract.isAutoRenew && noticeAt && noticeAt <= now && contract.expiresAt > now) {
      addSignal(signals, 'renewal_notice_due', 'renewal', 50, 'The contractual notice window is open now; renewal terms need a decision.', [
        { type: 'contract', id: contract.id },
      ]);
    } else if (contract.isAutoRenew && noticeAt && noticeAt.getTime() - now.getTime() <= 30 * DAY_MS && noticeAt > now) {
      addSignal(signals, 'renewal_notice_soon', 'renewal', 35, 'The contractual renewal notice window opens within 30 days.', [
        { type: 'contract', id: contract.id },
      ]);
    }
    if (daysToExpiry >= 0 && daysToExpiry <= 120) {
      addSignal(signals, 'contract_expiring', 'renewal', 25, `Executed customer paper expires in ${daysToExpiry} days.`, [
        { type: 'contract', id: contract.id },
      ]);
    }
  }

  for (const allocation of activeAllocations) {
    const futureCoverage = input.allocations.some((candidate) =>
      candidate.id !== allocation.id
      && candidate.demandDealId === allocation.demandDealId
      && (candidate.status === 'planned' || candidate.status === 'committed' || candidate.status === 'active')
      && reservesCapacity(candidate, now)
      && candidate.endsAt > allocation.endsAt
      && candidate.startsAt.getTime() <= allocation.endsAt.getTime() + 7 * DAY_MS,
    );
    const daysToEnd = Math.ceil((allocation.endsAt.getTime() - now.getTime()) / DAY_MS);
    if (daysToEnd >= 0 && daysToEnd <= 30 && !futureCoverage) {
      addSignal(signals, 'allocation_ending_uncovered', 'risk', 30, `Active sold capacity ends in ${daysToEnd} days and no future reservation is recorded.`, [
        { type: 'allocation', id: allocation.id },
      ]);
    }
    const deal = allocation.demandDealId ? dealById.get(allocation.demandDealId) : undefined;
    if (deal && !deal.msaExecuted) {
      addSignal(signals, 'msa_missing_for_active_capacity', 'risk', 25, 'Active sold capacity is linked to a deal whose MSA evidence is not marked executed.', [
        { type: 'allocation', id: allocation.id },
        { type: 'demand_deal', id: deal.id },
      ]);
    }
  }
  if (activeAllocations.length && !liveContracts.length) {
    addSignal(signals, 'active_capacity_without_live_contract', 'risk', 40, 'Active sold capacity has no currently executed demand-side contract on the account.', activeAllocations.map((allocation) => ({
      type: 'allocation' as const,
      id: allocation.id,
    })));
    blockers.push('Confirm governing customer paper before changing or extending sold capacity.');
  }

  for (const obligation of input.obligations) {
    if (!obligation.completedAt && obligation.dueAt < now) {
      addSignal(signals, 'overdue_contract_obligation', 'risk', 25, 'A customer contract obligation is overdue.', [
        { type: 'obligation', id: obligation.id },
        { type: 'contract', id: obligation.contractId },
      ]);
    }
  }

  const staleDays = lastActivityAt
    ? Math.floor((now.getTime() - lastActivityAt.getTime()) / DAY_MS)
    : null;
  if (staleDays == null || staleDays >= 90) {
    addSignal(signals, 'crm_evidence_stale_90', 'risk', 25, staleDays == null
      ? 'CRM evidence is stale: no customer activity is recorded. This does not establish customer disengagement.'
      : `CRM evidence is stale: no customer activity is recorded in ${staleDays} days. This does not establish customer disengagement.`, [
      { type: input.lastActivityId ? 'activity' : 'account', id: input.lastActivityId ?? input.accountId },
    ]);
  } else if (staleDays >= 60) {
    addSignal(signals, 'crm_evidence_stale_60', 'risk', 15, `CRM evidence is stale: no customer activity is recorded in ${staleDays} days. This does not establish customer disengagement.`, [
      { type: input.lastActivityId ? 'activity' : 'account', id: input.lastActivityId ?? input.accountId },
    ]);
  }

  signals.sort((left, right) =>
    right.weight - left.weight
    || left.code.localeCompare(right.code)
    || sourceKey(left).localeCompare(sourceKey(right)),
  );
  blockers.sort();
  const scoreByCategory = Object.fromEntries(
    GROWTH_SIGNAL_CATEGORIES.map((category) => [
      category,
      Math.min(100, signals.filter((signal) => signal.category === category).reduce((sum, signal) => sum + signal.weight, 0)),
    ]),
  ) as Record<GrowthSignalCategory, number>;
  const facets: GrowthFacet[] = [];
  if (scoreByCategory.expansion >= 30 || scoreByCategory.coverage >= 30) facets.push('expansion_candidate');
  if (signals.some((signal) => signal.category === 'renewal')) facets.push('renewal_due');
  if (signals.some((signal) => signal.category === 'risk' && !signal.code.startsWith('crm_evidence_stale'))) facets.push('at_risk');
  if (signals.some((signal) => signal.category === 'supply')) facets.push('idle_supply_match');
  if (uncoveredRequests.length) facets.push('coverage_gap');
  if (signals.some((signal) => signal.code.startsWith('crm_evidence_stale'))) facets.push('data_stale');

  return {
    relationshipState,
    facets,
    score: Math.min(100, signals.reduce((sum, signal) => sum + signal.weight, 0)),
    scoreByCategory,
    rulesetVersion: GROWTH_RULESET_VERSION,
    computedAt: now.toISOString(),
    signals,
    blockers,
    soldCapacityGpuHours: input.allocations
      .filter((allocation) => ['committed', 'active', 'completed'].includes(allocation.status))
      .reduce((sum, allocation) => sum + allocation.gpuHours, 0),
    heldCapacityGpuHours: input.allocations
      .filter((allocation) => allocation.status === 'planned' && reservesCapacity(allocation, now))
      .reduce((sum, allocation) => sum + allocation.gpuHours, 0),
  };
}

function reservesCapacity(allocation: LifecycleAllocationSnapshot, now: Date): boolean {
  if (allocation.status === 'released' || allocation.status === 'completed') return false;
  return allocation.status !== 'planned' || !allocation.holdExpiresAt || allocation.holdExpiresAt > now;
}

function overlaps(allocation: LifecycleAllocationSnapshot, now: Date): boolean {
  return allocation.startsAt <= now && allocation.endsAt > now;
}

function isOpenStage(stage: DemandStage): boolean {
  return (DEMAND_OPEN_STAGES as readonly DemandStage[]).includes(stage);
}

function latestDate(values: readonly (Date | null | undefined)[]): Date | null {
  return values.reduce<Date | null>((latest, value) =>
    value && (!latest || value > latest) ? value : latest, null);
}

function addSignal(
  target: GrowthSignal[],
  code: string,
  category: GrowthSignalCategory,
  weight: number,
  explanation: string,
  sourceRefs: LifecycleSourceRef[],
): void {
  target.push({ code, category, weight, explanation, sourceRefs });
}

function sourceKey(signal: GrowthSignal): string {
  return signal.sourceRefs.map((ref) => `${ref.type}:${ref.id}`).join('|');
}
