// Shared helpers for per-QSO award references.
//
// In the UI a QSO's manually-assigned award references are edited as a single
// semicolon-delimited string of "CODE@REF" entries, e.g.
//   "POTA@FR-11553;IOTA@EU-064"
// On save each entry is routed to the QSO field its award actually reads from
// (see internal/award/award.go): POTA/SOTA/IOTA have dedicated columns; WWFF
// and custom awards live in uppercase ADIF extras keys.

// parseAwardRefs turns "POTA@FR-11553;IOTA@EU-064" into
// { POTA: "FR-11553", IOTA: "EU-064" }. Repeated codes join with commas.
export function parseAwardRefs(v: string): Record<string, string> {
  const out: Record<string, string> = {};
  for (const entry of (v ?? '').split(';').filter(Boolean)) {
    const at = entry.indexOf('@');
    if (at <= 0) continue;
    const code = entry.slice(0, at).toUpperCase();
    const ref = entry.slice(at + 1).trim().toUpperCase();
    if (!ref) continue;
    out[code] = out[code] ? `${out[code]},${ref}` : ref;
  }
  return out;
}

// appendTokens adds space-separated tokens (a "A,B" ref string) to a text field,
// skipping any already present, so re-picking is idempotent.
function appendTokens(existing: string | undefined, refs: string): string {
  let out = (existing ?? '').trim();
  for (const tok of refs.split(',').map((s) => s.trim()).filter(Boolean)) {
    const re = new RegExp(`(^|\\s)${tok.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`, 'i');
    if (!re.test(out)) out = out ? `${out} ${tok}` : tok;
  }
  return out;
}

// removeTokens strips space/word-delimited tokens (a "A,B" ref string) from a
// text field — the inverse of appendTokens. Used when a picked reference is
// REMOVED so an in-field award (DDFM finds "D72" in the note) stops matching it;
// otherwise the ref is re-derived from the field and reappears on reopen.
function removeTokens(existing: string | undefined, refs: string): string {
  let out = existing ?? '';
  for (const tok of refs.split(',').map((s) => s.trim()).filter(Boolean)) {
    const re = new RegExp(`\\b${tok.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'ig');
    out = out.replace(re, ' ');
  }
  return out.replace(/\s{2,}/g, ' ').trim();
}

// MANUAL_REFS_KEY mirrors award.ManualRefsKey (Go): the ADIF extras key holding
// the operator's per-QSO award-reference assignments as "CODE@REF;CODE@REF".
// The award engine honours these regardless of how the award matches, so a
// reference can be assigned even for awards that scan a free-text field by
// description/pattern (e.g. WAPC over ADDRESS) where writing a bare code into
// the field wouldn't match.
const MANUAL_REFS_KEY = 'APP_OPSLOG_AWARDREFS';

// applyAwardRefs writes picked references onto a QSO payload. Every assignment
// is recorded under MANUAL_REFS_KEY (authoritative for the engine); in addition,
// awards backed by a standard ADIF column also get that column written so the
// data lives in its conventional place and exports correctly. Awards that match
// a free-text field by description/pattern (address/qth/name/custom) rely solely
// on the override — we don't pollute the text field with a code.
export function applyAwardRefs(payload: any, awardRefs: string, fieldOf: Record<string, string>, prevAwardRefs?: string) {
  const byCode = parseAwardRefs(awardRefs);
  // References the operator REMOVED since the editor opened: for in-field awards
  // (note/comment) strip the token from the field too, so a deleted DDFM/etc.
  // ref doesn't get re-derived from the leftover token and reappear on reopen.
  if (prevAwardRefs != null) {
    const prev = parseAwardRefs(prevAwardRefs);
    for (const [code, prevRef] of Object.entries(prev)) {
      const field = fieldOf[code] || code.toLowerCase();
      if (field !== 'note' && field !== 'notes' && field !== 'comment') continue;
      const now = new Set((byCode[code] ?? '').split(',').map((s) => s.trim().toUpperCase()).filter(Boolean));
      const removed = prevRef.split(',').map((s) => s.trim()).filter(Boolean).filter((r) => !now.has(r.toUpperCase()));
      if (removed.length === 0) continue;
      const rm = removed.join(',');
      if (field === 'comment') payload.comment = removeTokens(payload.comment, rm);
      else payload.notes = removeTokens(payload.notes, rm);
    }
  }
  const extras: Record<string, string> = { ...(payload.extras ?? {}) };
  const overrides: string[] = [];
  for (const [code, ref] of Object.entries(byCode)) {
    for (const r of ref.split(',').map((s) => s.trim()).filter(Boolean)) {
      overrides.push(`${code}@${r}`);
    }
    const field = fieldOf[code] || code.toLowerCase();
    switch (field) {
      case 'iota': payload.iota = ref; break;
      case 'sota_ref': payload.sota_ref = ref; break;
      case 'pota_ref': payload.pota_ref = ref; break;
      // Predefined-list awards on a QSO field (WAS/RAC/WAJA on state, JCC on
      // county): picking a reference writes it straight into that column.
      case 'state': payload.state = ref; break;
      case 'cnty': payload.cnty = ref; break;
      case 'wwff':
        extras['WWFF_REF'] = ref;
        extras['SIG'] = 'WWFF';
        extras['SIG_INFO'] = ref;
        break;
      // QSOFIELDS awards that scan a free-text field for a code (e.g. DDFM finds
      // "D06" in the note): append the code so the in-field matcher finds it too.
      case 'note': case 'notes':
        payload.notes = appendTokens(payload.notes, ref);
        break;
      case 'comment':
        payload.comment = appendTokens(payload.comment, ref);
        break;
      // address / qth / name / custom: the override above is authoritative; do
      // not write a code into a free-text field the award matches by name.
      default:
        break;
    }
  }
  if (overrides.length > 0) extras[MANUAL_REFS_KEY] = overrides.join(';');
  else delete extras[MANUAL_REFS_KEY];
  if (Object.keys(extras).length > 0) payload.extras = extras;
  else if (payload.extras) delete payload.extras;
}

// awardRefValue reads a single award's stored reference from a QSO, inverse of
// applyAwardRefs. Used to seed the editor when opening an existing QSO.
export function awardRefValue(qso: any, code: string, field: string): string {
  switch (field) {
    case 'iota': return (qso.iota ?? '').toUpperCase();
    case 'sota_ref': return (qso.sota_ref ?? '').toUpperCase();
    case 'pota_ref': return (qso.pota_ref ?? '').toUpperCase();
    case 'state': return (qso.state ?? '').toUpperCase();
    case 'cnty': return (qso.cnty ?? '').toUpperCase();
    case 'wwff': {
      const ex = qso.extras ?? {};
      if (ex['WWFF_REF']) return String(ex['WWFF_REF']).toUpperCase();
      if (String(ex['SIG'] ?? '').toUpperCase() === 'WWFF') return String(ex['SIG_INFO'] ?? '').toUpperCase();
      return '';
    }
    default: {
      const ex = qso.extras ?? {};
      return String(ex[field.toUpperCase()] ?? '').toUpperCase();
    }
  }
}

// buildAwardRefs reconstructs the "CODE@REF;…" editor string from a QSO for the
// given pickable awards (code → field). Only awards with a stored value appear.
export function buildAwardRefs(qso: any, pickable: Array<{ code: string; field: string }>): string {
  const out: string[] = [];
  for (const { code, field } of pickable) {
    const v = awardRefValue(qso, code, field);
    // A multi-reference field (n-fer POTA "US-6544,US-0680") becomes one
    // editor entry per reference, so each shows on its own removable line.
    for (const ref of v.split(/[,;]/).map((s) => s.trim()).filter(Boolean)) {
      out.push(`${code.toUpperCase()}@${ref}`);
    }
  }
  return out.join(';');
}

// COMPUTED_AWARD_FIELDS mirrors isComputedAwardField in app.go: awards whose
// reference is derived from the callsign and cty.dat, never assigned by hand.
//
// State and county are deliberately absent, there as here — an operator sets
// those themselves and they drive real predefined-list awards (WAS, WAJA, JCC).
const COMPUTED_AWARD_FIELDS = new Set([
  'dxcc', 'cqz', 'ituz', 'prefix', 'callsign', 'cont', 'country', 'grid', 'grid4',
]);

// spotRefList turns a QSO's code→ref map into the strings a DX-cluster comment
// wants: ["SOTA HA/KM-018"].
//
// COMPUTED awards are dropped. A QSO's materialised references hold both kinds,
// and the derived ones — DXCC, WAC, WAZ, WPX — are exactly what every reader of
// the spot works out from the callsign anyway. Announcing "WAC EU WAZ 15 WPX HA5"
// spends the whole 30-character comment saying nothing, and buries the one
// reference worth chasing. Same rule as the QSO editor, which is why these are
// the references it lists on the LEFT and not in its read-only panel.
//
// fieldOf is award code → scanned field (from GetAwardDefs). A code missing from
// it is dropped rather than guessed: showing a reference on the air matters more
// than showing all of them.
//
// A code carrying several references (a two-fer park) yields one entry each, so
// a spot can take the one that fits instead of emitting a truncated pair.
// Shortest first: when they cannot all fit, more references beat fewer.
export function spotRefList(byCode: Record<string, string>, fieldOf: Record<string, string>): string[] {
  const out: string[] = [];
  for (const [code, refs] of Object.entries(byCode ?? {})) {
    const field = fieldOf?.[code.toUpperCase()];
    if (!field || COMPUTED_AWARD_FIELDS.has(field)) continue;
    for (const ref of String(refs).split(',').map((s) => s.trim()).filter(Boolean)) {
      out.push(`${code.toUpperCase()} ${ref.toUpperCase()}`);
    }
  }
  return out.sort((a, b) => a.length - b.length || a.localeCompare(b));
}

// withIOTARef adds an island reference from the callbook to an entry's award
// references, unless the operator has already set one.
//
// QRZ sends <iota> for an operator on an island and OpsLog used to read past it.
// It matters more here than it would for another award: unlike POTA there is no
// live "who is on an island right now" feed anywhere, so the callbook record is
// the practical source — and it is known before the contact is logged, which is
// when it is useful.
//
// A reference the operator typed or picked wins. A callbook entry can be years
// old, and the operator in front of the radio has just been told where the
// station is.
// withRDARef adds a Russian district from the callbook to an entry's award
// references, on the same terms as the island above.
//
// HamQTH publishes the district a station operates from TODAY. That is the
// right answer for the contact being typed and the wrong one for any other, so
// this is only ever called on the entry — never on a QSO already in the log.
// A station can move district; stamping today's on last year's contact turns a
// correct entry into a false one, and nothing downstream could tell.
export function withRDARef(awardRefs: string, rda: string): string {
  const ref = (rda || '').trim().toUpperCase();
  // Two letters, a hyphen, two digits. Anything else is not a district.
  if (!/^[A-Z]{2}-\d{2}$/.test(ref)) return awardRefs;
  const byCode = parseAwardRefs(awardRefs);
  if ((byCode['RDA'] ?? '').trim() !== '') return awardRefs; // already set: leave it
  const sep = awardRefs.trim() === '' ? '' : ';';
  return awardRefs + sep + 'RDA@' + ref;
}

export function withIOTARef(awardRefs: string, iota: string): string {
  const ref = (iota || '').trim().toUpperCase();
  // EU-048: two letters, a hyphen, three digits. Anything else is not an IOTA
  // reference, and writing it into the award would make a reference that no
  // list contains — which counts for nothing and has to be found by hand later.
  if (!/^[A-Z]{2}-\d{3}$/.test(ref)) return awardRefs;
  const byCode = parseAwardRefs(awardRefs);
  if ((byCode['IOTA'] ?? '').trim() !== '') return awardRefs; // already set: leave it
  const sep = awardRefs.trim() === '' ? '' : ';';
  return awardRefs + sep + 'IOTA@' + ref;
}
