import type {
  BreakdownItem,
  CertificateKpis,
  ReportGranularity,
  VesselReportQuery,
} from '@ema-platform/api';

/** Nothing measurable is not zero — an em dash says so without lying. */
export const DASH = '—';

/**
 * A figure the API may legitimately have no answer for.
 *
 * `avgGrossTonnage` is null on an empty register and `approvalRatePct` is null
 * until something has been decided; rendering either as 0 would report a fleet
 * that weighs nothing and a service that approves nobody.
 */
export function formatNumber(
  value: number | null | undefined,
  options: { decimals?: number; suffix?: string } = {},
): string {
  if (value === null || value === undefined || Number.isNaN(value)) return DASH;
  const text = value.toLocaleString(undefined, {
    minimumFractionDigits: options.decimals ?? 0,
    maximumFractionDigits: options.decimals ?? 0,
  });
  return options.suffix ? `${text}${options.suffix}` : text;
}

export function formatPercent(value: number | null | undefined): string {
  return value === null || value === undefined
    ? DASH
    : `${formatNumber(value, { decimals: 1 })}%`;
}

export function formatMoney(value: number, currency: string): string {
  return `${formatNumber(value, { decimals: 2 })} ${currency}`;
}

/** A signed delta for the change-vs-previous chip. */
export function formatDelta(value: number | null): string {
  if (value === null) return DASH;
  const sign = value > 0 ? '+' : '';
  return `${sign}${formatNumber(value, { decimals: 1 })}%`;
}

export function deltaColor(value: number | null): string {
  if (value === null || value === 0) return 'gray';
  return value > 0 ? 'teal' : 'red';
}

/**
 * The API's expiry counts are cumulative — a certificate due in eleven days is
 * inside the 30-, 60- and 90-day figures, which is how a renewals desk reads
 * them. Stacked side by side in a chart that reads as three separate groups,
 * so they are differenced into disjoint bands first.
 */
export function expiryBands(
  certificates: CertificateKpis,
): Array<{ label: string; count: number }> {
  const { expiringIn30, expiringIn60, expiringIn90 } = certificates;
  return [
    { label: 'Within 30 days', count: expiringIn30 },
    // Math.max guards against a server that ever answers non-monotonically —
    // a negative bar is worse than a zero one.
    { label: '31–60 days', count: Math.max(0, expiringIn60 - expiringIn30) },
    { label: '61–90 days', count: Math.max(0, expiringIn90 - expiringIn60) },
  ];
}

/** Red inside a week, orange inside a month, otherwise unremarkable. */
export function expiryUrgency(daysToExpiry: number): string {
  if (daysToExpiry <= 7) return 'red';
  if (daysToExpiry <= 30) return 'orange';
  return 'gray';
}

/**
 * Officer ids are IAM uuids, which make useless axis labels. Until the
 * dashboard has a name lookup, shorten them and keep "UNASSIGNED" readable.
 */
export function officerLabel(key: string): string {
  if (key === 'UNASSIGNED') return 'Unassigned';
  return key.length > 8 ? `${key.slice(0, 8)}…` : key;
}

/**
 * Chart colours, assigned by position so a slice keeps its colour between
 * renders. Mantine's palette rather than invented hex codes, so the charts
 * follow the theme the rest of the app is built on.
 */
const PALETTE = [
  'var(--mantine-color-blue-6)',
  'var(--mantine-color-teal-6)',
  'var(--mantine-color-orange-6)',
  'var(--mantine-color-grape-6)',
  'var(--mantine-color-cyan-6)',
  'var(--mantine-color-lime-7)',
  'var(--mantine-color-pink-6)',
  'var(--mantine-color-indigo-6)',
];

const MUTED = 'var(--mantine-color-gray-5)';

/**
 * "Unknown" and "Other" are bookkeeping slices rather than findings, so they
 * always take the muted colour instead of competing with the real categories
 * for one of the bright ones.
 */
export function sliceColor(item: BreakdownItem, index: number): string {
  if (item.key === 'OTHER' || item.key === 'Unknown') return MUTED;
  return PALETTE[index % PALETTE.length];
}

/** Bucket keys are ISO dates; the axis wants something a human reads. */
export function formatBucket(
  bucket: string,
  granularity: ReportGranularity,
): string {
  const date = new Date(bucket);
  if (Number.isNaN(date.getTime())) return bucket;
  if (granularity === 'MONTH') {
    return date.toLocaleDateString(undefined, {
      month: 'short',
      year: 'numeric',
      timeZone: 'UTC',
    });
  }
  return date.toLocaleDateString(undefined, {
    day: 'numeric',
    month: 'short',
    timeZone: 'UTC',
  });
}

/** The default window the API applies when none is given: the last 12 months. */
export function defaultRange(now: Date): [Date, Date] {
  const from = new Date(
    Date.UTC(now.getUTCFullYear() - 1, now.getUTCMonth(), now.getUTCDate()),
  );
  return [from, now];
}

export const ISO_DAY_LENGTH = 10;

export const toIsoDay = (date: Date): string =>
  date.toISOString().slice(0, ISO_DAY_LENGTH);

/**
 * The filter state as URL search params, so a filtered dashboard is a
 * shareable link rather than something the next person has to rebuild.
 *
 * Empty arrays and blank strings are dropped rather than serialised, which
 * keeps an untouched dashboard's URL clean and lets the API apply its own
 * defaults instead of being handed an empty filter to honour.
 */
export function queryToSearchParams(query: VesselReportQuery): URLSearchParams {
  const params = new URLSearchParams();
  for (const [key, value] of Object.entries(query)) {
    if (value === undefined || value === null || value === '') continue;
    if (Array.isArray(value)) {
      if (value.length === 0) continue;
      params.set(key, value.join(','));
    } else {
      params.set(key, String(value));
    }
  }
  return params;
}

const ARRAY_KEYS = [
  'category',
  'status',
  'flagState',
  'portOfRegistry',
  'vesselType',
] as const;

const NUMBER_KEYS = ['expiringWithinDays', 'topN', 'tableLimit'] as const;

/** The inverse, for restoring state from a shared link. */
export function searchParamsToQuery(
  params: URLSearchParams,
): VesselReportQuery {
  const query: Record<string, unknown> = {};
  for (const key of ARRAY_KEYS) {
    const raw = params.get(key);
    if (raw) query[key] = raw.split(',').filter(Boolean);
  }
  for (const key of NUMBER_KEYS) {
    const raw = params.get(key);
    // An unparseable number in a hand-edited URL is ignored rather than sent
    // on to fail the API's validation pipe.
    if (raw !== null && raw !== '' && Number.isFinite(Number(raw))) {
      query[key] = Number(raw);
    }
  }
  for (const key of ['from', 'to', 'search'] as const) {
    const raw = params.get(key);
    if (raw) query[key] = raw;
  }
  const granularity = params.get('granularity');
  if (granularity === 'DAY' || granularity === 'WEEK' || granularity === 'MONTH') {
    query.granularity = granularity;
  }
  return query as VesselReportQuery;
}

/**
 * The multi-select options a filter offers, taken from the breakdown the last
 * response carried — there is no lookup endpoint for flag states or ports, and
 * the register is the only place that knows which ones are in use.
 *
 * "Unknown" is dropped: it stands for a missing value, and there is nothing to
 * filter the register down to.
 */
export function optionsFrom(items: BreakdownItem[] | undefined): string[] {
  return (items ?? [])
    .filter((item) => item.key !== 'Unknown' && item.key !== 'OTHER')
    .map((item) => item.key);
}
