import type {
  ContractDocumentArticle,
  ContractDocumentSnapshot,
} from './entities/contract.entity';

/**
 * One recorded change between two document snapshots. A body edit carries the
 * text on both sides so the audit trail shows WHAT was rewritten, not merely
 * that something was — the UI diffs the two strings for display.
 */
export type ContractDocumentChange =
  | { kind: 'ARTICLE_ADDED'; articleId: string; title: string }
  | { kind: 'ARTICLE_REMOVED'; articleId: string; title: string }
  | {
      kind: 'ARTICLE_RENAMED';
      articleId: string;
      title: string;
      fromTitle: string;
    }
  | {
      kind: 'ARTICLE_BODY_CHANGED';
      articleId: string;
      title: string;
      /** Body before / after the edit. Absent on revisions recorded earlier. */
      fromBody?: string;
      toBody?: string;
    }
  | {
      kind: 'ARTICLE_REORDERED';
      articleId: string;
      title: string;
      fromOrder: number;
      toOrder: number;
    }
  | { kind: 'DOCUMENT_TITLE_CHANGED'; title: string; fromTitle: string | null }
  | { kind: 'WHEREAS_CHANGED'; added: number; removed: number }
  /**
   * A contract field (not a document article) changed — the customer editing a
   * DRAFT/CHANGES_REQUESTED contract, e.g. its route, cargo or service type.
   */
  | {
      kind: 'FIELD_CHANGED';
      field: string;
      label: string;
      from: string | null;
      to: string | null;
    };

type SnapshotLike = Pick<
  ContractDocumentSnapshot,
  'documentTitle' | 'whereasClauses' | 'articles'
> | null;

/** Match on id when present, else on normalized title (editors may omit ids). */
function articleKey(article: ContractDocumentArticle): string {
  return article.id || `title:${article.title.trim().toLowerCase()}`;
}

function indexArticles(
  articles: ContractDocumentArticle[] | undefined,
): Map<string, ContractDocumentArticle> {
  const map = new Map<string, ContractDocumentArticle>();
  for (const article of articles ?? []) {
    map.set(articleKey(article), article);
  }
  return map;
}

/**
 * Compare two document snapshots and describe what changed, article by article.
 * Returns an empty array when the snapshots are equivalent, so callers can skip
 * recording a no-op revision.
 */
export function diffSnapshots(
  before: SnapshotLike,
  after: SnapshotLike,
): ContractDocumentChange[] {
  const changes: ContractDocumentChange[] = [];

  const beforeTitle = before?.documentTitle ?? null;
  const afterTitle = after?.documentTitle ?? null;
  if (beforeTitle !== afterTitle && afterTitle !== null) {
    changes.push({
      kind: 'DOCUMENT_TITLE_CHANGED',
      title: afterTitle,
      fromTitle: beforeTitle,
    });
  }

  const beforeWhereas = before?.whereasClauses ?? [];
  const afterWhereas = after?.whereasClauses ?? [];
  const beforeWhereasSet = new Set(beforeWhereas);
  const afterWhereasSet = new Set(afterWhereas);
  const whereasAdded = afterWhereas.filter((c) => !beforeWhereasSet.has(c)).length;
  const whereasRemoved = beforeWhereas.filter((c) => !afterWhereasSet.has(c)).length;
  if (whereasAdded > 0 || whereasRemoved > 0) {
    changes.push({
      kind: 'WHEREAS_CHANGED',
      added: whereasAdded,
      removed: whereasRemoved,
    });
  }

  const beforeArticles = indexArticles(before?.articles);
  const afterArticles = indexArticles(after?.articles);

  for (const [key, article] of afterArticles) {
    const previous = beforeArticles.get(key);
    if (!previous) {
      changes.push({
        kind: 'ARTICLE_ADDED',
        articleId: article.id,
        title: article.title,
      });
      continue;
    }

    if (previous.title !== article.title) {
      changes.push({
        kind: 'ARTICLE_RENAMED',
        articleId: article.id,
        title: article.title,
        fromTitle: previous.title,
      });
    }
    if (previous.body !== article.body) {
      changes.push({
        kind: 'ARTICLE_BODY_CHANGED',
        articleId: article.id,
        title: article.title,
        fromBody: previous.body,
        toBody: article.body,
      });
    }
    if (previous.order !== article.order) {
      changes.push({
        kind: 'ARTICLE_REORDERED',
        articleId: article.id,
        title: article.title,
        fromOrder: previous.order,
        toOrder: article.order,
      });
    }
  }

  for (const [key, article] of beforeArticles) {
    if (afterArticles.has(key)) continue;
    changes.push({
      kind: 'ARTICLE_REMOVED',
      articleId: article.id,
      title: article.title,
    });
  }

  return changes;
}

/** Human label per audited contract field, in the order they read on the form. */
export const CONTRACT_FIELD_LABELS: Record<string, string> = {
  contractKind: 'Contract kind',
  tradeDirection: 'Trade direction',
  freightType: 'Freight type',
  serviceType: 'Service type',
  paymentCurrency: 'Payment currency',
  contractType: 'Contract type',
  isHazardous: 'Hazardous',
  hazardClass: 'Hazard class',
  unNumber: 'UN number',
  isReefer: 'Reefer',
  equipmentReturn: 'Equipment return',
  customsClearingAgent: 'Customs clearing agent',
  firstMilePickupAddress: 'First-mile pickup address',
  lastMileDeliveryAddress: 'Last-mile delivery address',
  routes: 'Routes',
  cargoScope: 'Cargo scope',
};

/** Render a field value for the audit trail — never "[object Object]". */
function displayValue(value: unknown): string | null {
  if (value === null || value === undefined || value === '') return null;
  if (typeof value === 'boolean') return value ? 'Yes' : 'No';
  return String(value);
}

/**
 * Compare two flat maps of contract fields and report what changed. Only keys
 * present in `after` are considered, so a partial update never reports the
 * fields it did not touch.
 */
export function diffContractFields(
  before: Record<string, unknown>,
  after: Record<string, unknown>,
): ContractDocumentChange[] {
  const changes: ContractDocumentChange[] = [];

  for (const [field, nextRaw] of Object.entries(after)) {
    const next = displayValue(nextRaw);
    const previous = displayValue(before[field]);
    if (next === previous) continue;
    changes.push({
      kind: 'FIELD_CHANGED',
      field,
      label: CONTRACT_FIELD_LABELS[field] ?? field,
      from: previous,
      to: next,
    });
  }

  return changes;
}

/** Short human summary of a change set, e.g. "2 articles edited, 1 article added". */
export function summarizeChanges(changes: ContractDocumentChange[]): string {
  if (changes.length === 0) return 'No changes';

  const articleVerbs: Record<string, string> = {
    ARTICLE_ADDED: 'added',
    ARTICLE_REMOVED: 'removed',
    ARTICLE_RENAMED: 'renamed',
    ARTICLE_BODY_CHANGED: 'edited',
    ARTICLE_REORDERED: 'reordered',
  };

  const counts = new Map<string, number>();
  const parts: string[] = [];
  const fields: string[] = [];

  for (const change of changes) {
    const verb = articleVerbs[change.kind];
    if (verb) {
      counts.set(verb, (counts.get(verb) ?? 0) + 1);
    } else if (change.kind === 'DOCUMENT_TITLE_CHANGED') {
      parts.push('document title changed');
    } else if (change.kind === 'WHEREAS_CHANGED') {
      parts.push('recitals changed');
    } else if (change.kind === 'FIELD_CHANGED') {
      fields.push(change.label.toLowerCase());
    }
  }

  if (fields.length > 0) {
    parts.push(
      fields.length <= 3
        ? `${fields.join(', ')} changed`
        : `${fields.length} contract fields changed`,
    );
  }

  const articleParts = [...counts.entries()].map(
    ([verb, count]) => `${count} article${count === 1 ? '' : 's'} ${verb}`,
  );

  return [...articleParts, ...parts].join(', ');
}
