// Row colouring for the log grid, by QSL status.
//
// Four categories, each scoped to the channels the operator cares about —
// paper QSL, LoTW, eQSL, QRZ.com. The rules are ORDERED and the first match
// wins, because a contact is usually several of them at once.

export type RowColorRule = {
  id: string;
  color: string;
  enabled: boolean;
  channels?: string[]; // empty = every channel
};
export type RowColorSettings = {
  enabled: boolean;
  style?: 'bar' | 'tint' | 'both';
  intensity?: number;
  bandmap_lotw?: boolean;
  rules: RowColorRule[];
};

export const CHANNELS = ['qsl', 'lotw', 'eqsl', 'qrz'] as const;

// The two QSO fields behind each channel. QRZ.com and Club Log call theirs an
// "upload status" rather than a QSL flag, but they carry the same Y / R letters.
const FIELDS: Record<string, { sent: string; rcvd: string }> = {
  qsl: { sent: 'qsl_sent', rcvd: 'qsl_rcvd' },
  lotw: { sent: 'lotw_sent', rcvd: 'lotw_rcvd' },
  eqsl: { sent: 'eqsl_sent', rcvd: 'eqsl_rcvd' },
  qrz: { sent: 'qrzcom_qso_upload_status', rcvd: 'qrzcom_qso_download_status' },
};

// ADIF QSL fields are single letters. Y is the only one that means "yes";
// R (requested) and Q (queued) mean it has not gone out yet — a different state,
// and the one an operator looks for when deciding what to send.
const yes = (v: any) => String(v ?? '').trim().toUpperCase() === 'Y';
const owed = (v: any) => {
  const s = String(v ?? '').trim().toUpperCase();
  return s === 'R' || s === 'Q';
};

const chansOf = (r: RowColorRule): readonly string[] =>
  r.channels && r.channels.length ? r.channels : CHANNELS;

function ruleMatches(q: any, r: RowColorRule): boolean {
  const cs = chansOf(r);
  switch (r.id) {
    case 'confirmed':
      return cs.some((c) => yes(q[FIELDS[c]?.rcvd]));
    case 'sent':
      return cs.some((c) => yes(q[FIELDS[c]?.sent]));
    case 'to_send':
      return cs.some((c) => owed(q[FIELDS[c]?.sent]));
    case 'worked':
      // The catch-all: nothing sent, nothing asked for, nothing back.
      return !CHANNELS.some((c) => yes(q[FIELDS[c].rcvd]) || yes(q[FIELDS[c].sent]) || owed(q[FIELDS[c].sent]));
    default:
      return false;
  }
}

// Walks the rules IN ORDER — the order is the priority, and the settings panel
// shows it numbered so it can be read rather than guessed.
export function matchRowRule(q: any, cfg: RowColorSettings | null): RowColorRule | null {
  if (!q || !cfg?.rules) return null;
  for (const r of cfg.rules) {
    if (r.enabled && ruleMatches(q, r)) return r;
  }
  return null;
}

// The colour is never a fill by default.
//
// A log where nearly every contact has SOME QSL state ends up with every row
// painted, and colour that is always present stops being information. The
// default is a stripe down the left edge; a tint is offered at a chosen strength.
export function rowStyleFor(q: any, cfg: RowColorSettings | null): Record<string, string> | undefined {
  if (!cfg?.enabled) return undefined;
  const rule = matchRowRule(q, cfg);
  if (!rule?.color) return undefined;

  const style = cfg.style ?? 'bar';
  const pct = Math.max(5, Math.min(45, cfg.intensity ?? 12));
  const out: Record<string, string> = {};
  if (style === 'tint' || style === 'both') {
    out.backgroundColor = `color-mix(in srgb, ${rule.color} ${pct}%, transparent)`;
  }
  if (style === 'bar' || style === 'both') {
    // inset shadow rather than a border: a border would shift the cells three
    // pixels on coloured rows only, and the columns would stop lining up.
    out.boxShadow = `inset 3px 0 0 ${rule.color}`;
  }
  return out;
}
