/**
 * Stable source anchors shared by search, diagnostics, review, AI and future
 * Paper Memory integrations. Anchors never silently jump to ambiguous text.
 */
export interface SourceAnchor {
  filePath: string;
  from: number;
  to: number;
  selectedText?: string;
  prefixContext?: string;
  suffixContext?: string;
  sourceRevision?: number;
  semanticId?: string;
  semanticKind?: string;
}

export type SourceAnchorRecoveryStrategy =
  | 'exact'
  | 'selected-text'
  | 'context'
  | 'semantic'
  | 'outdated';

export type ResolvedSourceAnchor = {
  anchor: SourceAnchor;
  from: number;
  to: number;
  strategy: Exclude<SourceAnchorRecoveryStrategy, 'outdated'>;
};

export type SourceAnchorResolution =
  | { status: 'resolved'; value: ResolvedSourceAnchor }
  | { status: 'outdated'; strategy: 'outdated'; reason: string };

export type CaptureSourceAnchorOptions = {
  filePath: string;
  source: string;
  from: number;
  to: number;
  sourceRevision?: number;
  semanticId?: string;
  semanticKind?: string;
  contextLength?: number;
};

export type ResolveSourceAnchorOptions = {
  source: string;
  sourceRevision?: number;
  semanticRange?: (anchor: SourceAnchor) => { from: number; to: number } | undefined;
};

function validRange(source: string, from: number, to: number): boolean {
  return Number.isInteger(from) && Number.isInteger(to) && from >= 0 && to >= from && to <= source.length;
}

function allOccurrences(source: string, needle: string): number[] {
  if (!needle) return [];
  const matches: number[] = [];
  let offset = 0;
  while (offset <= source.length - needle.length) {
    const index = source.indexOf(needle, offset);
    if (index < 0) break;
    matches.push(index);
    offset = index + Math.max(1, needle.length);
  }
  return matches;
}

export function captureSourceAnchor(options: CaptureSourceAnchorOptions): SourceAnchor {
  const { source, from, to } = options;
  if (!validRange(source, from, to)) {
    throw new RangeError(`Invalid source anchor range ${from}..${to} for ${source.length} characters`);
  }
  const contextLength = Math.max(0, Math.trunc(options.contextLength ?? 64));
  const selectedText = source.slice(from, to);
  const prefixContext = source.slice(Math.max(0, from - contextLength), from);
  const suffixContext = source.slice(to, Math.min(source.length, to + contextLength));
  return {
    filePath: options.filePath,
    from,
    to,
    ...(selectedText ? { selectedText } : {}),
    ...(prefixContext ? { prefixContext } : {}),
    ...(suffixContext ? { suffixContext } : {}),
    ...(options.sourceRevision === undefined ? {} : { sourceRevision: options.sourceRevision }),
    ...(options.semanticId ? { semanticId: options.semanticId } : {}),
    ...(options.semanticKind ? { semanticKind: options.semanticKind } : {}),
  };
}

export function resolveSourceAnchor(
  anchor: SourceAnchor,
  options: ResolveSourceAnchorOptions,
): SourceAnchorResolution {
  const { source } = options;
  const exactRevision =
    anchor.sourceRevision === undefined ||
    options.sourceRevision === undefined ||
    anchor.sourceRevision === options.sourceRevision;
  if (exactRevision && validRange(source, anchor.from, anchor.to)) {
    const matchesSelection = anchor.selectedText === undefined || source.slice(anchor.from, anchor.to) === anchor.selectedText;
    if (matchesSelection) {
      return { status: 'resolved', value: { anchor, from: anchor.from, to: anchor.to, strategy: 'exact' } };
    }
  }

  if (anchor.selectedText) {
    const matches = allOccurrences(source, anchor.selectedText);
    if (matches.length === 1) {
      const from = matches[0]!;
      return {
        status: 'resolved',
        value: { anchor, from, to: from + anchor.selectedText.length, strategy: 'selected-text' },
      };
    }
  }

  const prefix = anchor.prefixContext ?? '';
  const suffix = anchor.suffixContext ?? '';
  if (prefix || suffix) {
    const candidates: Array<{ from: number; to: number }> = [];
    if (anchor.selectedText) {
      for (const from of allOccurrences(source, anchor.selectedText)) {
        const to = from + anchor.selectedText.length;
        const prefixMatches = !prefix || source.slice(Math.max(0, from - prefix.length), from) === prefix;
        const suffixMatches = !suffix || source.slice(to, to + suffix.length) === suffix;
        if (prefixMatches && suffixMatches) candidates.push({ from, to });
      }
    } else if (prefix && suffix) {
      let offset = 0;
      while (offset <= source.length) {
        const prefixIndex = source.indexOf(prefix, offset);
        if (prefixIndex < 0) break;
        const from = prefixIndex + prefix.length;
        const suffixIndex = source.indexOf(suffix, from);
        if (suffixIndex >= 0) candidates.push({ from, to: suffixIndex });
        offset = prefixIndex + Math.max(1, prefix.length);
      }
    } else if (prefix) {
      for (const index of allOccurrences(source, prefix)) candidates.push({ from: index + prefix.length, to: index + prefix.length });
    } else if (suffix) {
      for (const index of allOccurrences(source, suffix)) candidates.push({ from: index, to: index });
    }
    if (candidates.length === 1) {
      return { status: 'resolved', value: { anchor, ...candidates[0]!, strategy: 'context' } };
    }
  }

  if (anchor.semanticId && options.semanticRange) {
    const semantic = options.semanticRange(anchor);
    if (semantic && validRange(source, semantic.from, semantic.to)) {
      return { status: 'resolved', value: { anchor, ...semantic, strategy: 'semantic' } };
    }
  }

  return {
    status: 'outdated',
    strategy: 'outdated',
    reason: 'Source anchor could not be recovered unambiguously from the current document.',
  };
}
