export type BibSourceRange = { from: number; to: number };

export type BibField = {
  name: string;
  value: string;
  rawValue: string;
  range: BibSourceRange;
  bodyRange: BibSourceRange;
};

export type BibEntry = {
  id: string;
  type: string;
  key: string;
  line: number;
  range: BibSourceRange;
  keyRange: BibSourceRange;
  bodyRange: BibSourceRange;
  fields: readonly BibField[];
};

function sourceLineAt(source: string, offset: number): number {
  let line = 1;
  for (let index = 0; index < offset; index += 1) {
    if (source.charCodeAt(index) === 10) line += 1;
  }
  return line;
}

export type BibDiagnostic = {
  kind: 'duplicate-key' | 'malformed-entry';
  message: string;
  offset?: number;
  key?: string;
};

export type BibFile = {
  entries: readonly BibEntry[];
  diagnostics: readonly BibDiagnostic[];
};

export type BibTextEdit = { from: number; to: number; text: string };

function isEscaped(source: string, offset: number): boolean {
  let slashes = 0;
  for (let index = offset - 1; index >= 0 && source[index] === '\\'; index -= 1) slashes += 1;
  return slashes % 2 === 1;
}

function skipWhitespaceAndComments(source: string, offset: number, limit = source.length): number {
  let index = offset;
  while (index < limit) {
    if (/\s/.test(source[index] ?? '')) {
      index += 1;
      continue;
    }
    if (source[index] === '%' && !isEscaped(source, index)) {
      while (index < limit && source[index] !== '\n') index += 1;
      continue;
    }
    break;
  }
  return index;
}

function findBalancedEnd(
  source: string,
  start: number,
  open: '{' | '(',
  close: '}' | ')',
): number | undefined {
  let depth = 0;
  let quoted = false;
  for (let index = start; index < source.length; index += 1) {
    const char = source[index];
    if (char === '"' && !isEscaped(source, index)) quoted = !quoted;
    if (quoted) continue;
    if (char === open && !isEscaped(source, index)) depth += 1;
    if (char === close && !isEscaped(source, index)) {
      depth -= 1;
      if (depth === 0) return index;
    }
  }
  return undefined;
}

function findTopLevelComma(source: string, from: number, to: number): number | undefined {
  let braces = 0;
  let quoted = false;
  for (let index = from; index < to; index += 1) {
    const char = source[index];
    if (char === '"' && !isEscaped(source, index)) quoted = !quoted;
    if (quoted) continue;
    if (char === '{' && !isEscaped(source, index)) braces += 1;
    else if (char === '}' && !isEscaped(source, index)) braces = Math.max(0, braces - 1);
    else if (char === ',' && braces === 0) return index;
  }
  return undefined;
}

function readFieldValue(
  source: string,
  start: number,
  limit: number,
): { rawValue: string; value: string; range: BibSourceRange; bodyRange: BibSourceRange; to: number } | undefined {
  const valueStart = skipWhitespaceAndComments(source, start, limit);
  if (valueStart >= limit) return undefined;
  const opener = source[valueStart];
  if (opener === '{') {
    const end = findBalancedEnd(source, valueStart, '{', '}');
    if (end === undefined || end >= limit) return undefined;
    return {
      rawValue: source.slice(valueStart, end + 1),
      value: source.slice(valueStart + 1, end),
      range: { from: valueStart, to: end + 1 },
      bodyRange: { from: valueStart + 1, to: end },
      to: end + 1,
    };
  }
  if (opener === '"') {
    for (let index = valueStart + 1; index < limit; index += 1) {
      if (source[index] === '"' && !isEscaped(source, index)) {
        return {
          rawValue: source.slice(valueStart, index + 1),
          value: source.slice(valueStart + 1, index),
          range: { from: valueStart, to: index + 1 },
          bodyRange: { from: valueStart + 1, to: index },
          to: index + 1,
        };
      }
    }
    return undefined;
  }

  let index = valueStart;
  while (index < limit && source[index] !== ',') index += 1;
  const rawEnd = index;
  let bodyEnd = rawEnd;
  while (bodyEnd > valueStart && /\s/.test(source[bodyEnd - 1] ?? '')) bodyEnd -= 1;
  return {
    rawValue: source.slice(valueStart, bodyEnd),
    value: source.slice(valueStart, bodyEnd),
    range: { from: valueStart, to: bodyEnd },
    bodyRange: { from: valueStart, to: bodyEnd },
    to: bodyEnd,
  };
}

function parseFields(source: string, from: number, to: number): BibField[] {
  const fields: BibField[] = [];
  let cursor = from;
  while (cursor < to) {
    cursor = skipWhitespaceAndComments(source, cursor, to);
    while (source[cursor] === ',') cursor = skipWhitespaceAndComments(source, cursor + 1, to);
    if (cursor >= to) break;

    const nameMatch = /^[A-Za-z][A-Za-z0-9_:\-.]*/.exec(source.slice(cursor, to));
    if (!nameMatch) break;
    const nameFrom = cursor;
    const name = nameMatch[0];
    cursor += name.length;
    cursor = skipWhitespaceAndComments(source, cursor, to);
    if (source[cursor] !== '=') break;
    cursor += 1;
    const value = readFieldValue(source, cursor, to);
    if (!value) break;
    fields.push({
      name: name.toLowerCase(),
      value: value.value,
      rawValue: value.rawValue,
      range: { from: nameFrom, to: value.to },
      bodyRange: value.bodyRange,
    });
    cursor = value.to;
    cursor = skipWhitespaceAndComments(source, cursor, to);
    if (source[cursor] === ',') cursor += 1;
  }
  return fields;
}

export function parseBibTeX(source: string): BibFile {
  const entries: BibEntry[] = [];
  const diagnostics: BibDiagnostic[] = [];
  const entryPattern = /@([A-Za-z]+)\s*([\{(])/g;
  while (true) {
    const match = entryPattern.exec(source);
    if (!match) break;
    const type = (match[1] ?? '').toLowerCase();
    const open = match[2] as '{' | '(';
    const close = open === '{' ? '}' : ')';
    const openAt = match.index + match[0].lastIndexOf(open);
    const end = findBalancedEnd(source, openAt, open, close);
    if (end === undefined) {
      diagnostics.push({ kind: 'malformed-entry', message: `Unterminated @${type} entry.`, offset: match.index });
      break;
    }
    entryPattern.lastIndex = end + 1;
    if (type === 'comment' || type === 'preamble' || type === 'string') continue;

    const bodyFrom = openAt + 1;
    const bodyTo = end;
    const comma = findTopLevelComma(source, bodyFrom, bodyTo);
    if (comma === undefined) {
      diagnostics.push({ kind: 'malformed-entry', message: `@${type} entry has no citation key separator.`, offset: match.index });
      continue;
    }
    const keyRaw = source.slice(bodyFrom, comma);
    const leading = keyRaw.match(/^\s*/)?.[0].length ?? 0;
    const trailing = keyRaw.match(/\s*$/)?.[0].length ?? 0;
    const keyFrom = bodyFrom + leading;
    const keyTo = comma - trailing;
    const key = source.slice(keyFrom, keyTo).trim();
    if (!key) {
      diagnostics.push({ kind: 'malformed-entry', message: `@${type} entry has an empty citation key.`, offset: match.index });
      continue;
    }
    entries.push({
      id: `${key}:${match.index}:${end + 1}`,
      type,
      key,
      line: sourceLineAt(source, match.index),
      range: { from: match.index, to: end + 1 },
      keyRange: { from: keyFrom, to: keyTo },
      bodyRange: { from: bodyFrom, to: bodyTo },
      fields: parseFields(source, comma + 1, bodyTo),
    });
  }

  const byKey = new Map<string, number>();
  for (const entry of entries) byKey.set(entry.key, (byKey.get(entry.key) ?? 0) + 1);
  for (const [key, count] of byKey) {
    if (count > 1) diagnostics.push({ kind: 'duplicate-key', key, message: `Duplicate bibliography key: ${key}` });
  }
  return { entries, diagnostics };
}

export function getBibField(entry: BibEntry, name: string): BibField | undefined {
  const normalized = name.toLowerCase();
  return entry.fields.find((field) => field.name === normalized);
}

export function createBibKeyEdit(entry: BibEntry, key: string): BibTextEdit {
  const normalized = key.trim();
  if (!normalized || /[\s,{}()]/.test(normalized)) throw new Error('Invalid bibliography key.');
  return { from: entry.keyRange.from, to: entry.keyRange.to, text: normalized };
}

export function createBibFieldEdit(
  source: string,
  entry: BibEntry,
  name: string,
  value: string,
): BibTextEdit {
  const normalizedName = name.trim().toLowerCase();
  if (!/^[A-Za-z][A-Za-z0-9_:\-.]*$/.test(normalizedName)) throw new Error('Invalid bibliography field name.');
  const existing = getBibField(entry, normalizedName);
  if (existing) {
    return { from: existing.bodyRange.from, to: existing.bodyRange.to, text: value };
  }

  const closing = entry.range.to - 1;
  const beforeClosing = source.slice(entry.bodyRange.from, closing).trimEnd();
  const needsComma = beforeClosing.length > 0 && !beforeClosing.endsWith(',');
  return {
    from: closing,
    to: closing,
    text: `${needsComma ? ',' : ''}\n  ${normalizedName} = {${value}}\n`,
  };
}

export function bibEntryLabel(entry: BibEntry): string {
  const title = getBibField(entry, 'title')?.value.trim();
  const author = getBibField(entry, 'author')?.value.trim();
  const year = getBibField(entry, 'year')?.value.trim();
  if (title) return title;
  if (author && year) return `${author} (${year})`;
  return author || entry.key;
}

export function searchBibEntries<T extends BibEntry>(
  entries: readonly T[],
  query: string,
): T[] {
  const needle = query.trim().toLowerCase();
  if (!needle) return [...entries];
  return entries.filter((entry) => {
    const haystack = [
      entry.key,
      entry.type,
      ...entry.fields.flatMap((field) => [field.name, field.value]),
    ].join('\n').toLowerCase();
    return haystack.includes(needle);
  });
}

export type {
  ReferenceCollection,
  ReferenceFetch,
  ReferenceMatch,
  ReferenceProvider,
  ReferenceProviderId,
  ReferenceRecord,
  ReferenceSearchFilters,
  ReferenceSearchInput,
  ReferenceSearchResult,
  ReferenceSort,
} from './reference-provider';
export {
  createBibTeXEntry,
  createCrossrefReferenceProvider,
  createLocalBibliographyReferenceProvider,
  createOpenAlexReferenceProvider,
  createSemanticScholarReferenceProvider,
  createZoteroReferenceProvider,
  dedupeReferenceRecords,
  matchReferenceToBibliography,
  suggestCitationKey,
} from './reference-provider';

export type {
  LiteratureShortlistItem,
  LiteratureShortlistState,
  ProbableDoiMatch,
  ReferenceResearchIntent,
  ReferenceResearchSuggestion,
} from './research-assistance';
export {
  buildReferenceResearchQuery,
  createShortlistItem,
  findProbableMissingDoi,
  migrateShortlistRecords,
  rankReferenceResearchCandidates,
  researchFiltersForIntent,
} from './research-assistance';
