import {
  type Completion,
  type CompletionContext,
  type CompletionResult,
  type CompletionSource,
} from '@codemirror/autocomplete';
import { StateEffect, StateField, type Extension } from '@codemirror/state';
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';

export type LatexProjectCompletionData = {
  citations?: readonly { key: string; title?: string; detail?: string }[];
  labels?: readonly { label: string; detail?: string }[];
  files?: readonly { path: string; detail?: string }[];
};

export type LatexNavigationTarget =
  | { kind: 'reference'; target: string }
  | { kind: 'citation'; target: string }
  | { kind: 'file'; target: string; command: string };

export type LatexProjectFeatureConfig = {
  getCompletionData?: () => LatexProjectCompletionData | Promise<LatexProjectCompletionData>;
  onNavigate?: (target: LatexNavigationTarget) => void | Promise<void>;
  onPasteFile?: (file: File) => void | Promise<void>;
};

const CITATION_COMMAND = /\\(?:cite|citep|citet|citealp|citeauthor|citeyear|autocite|parencite|textcite|footcite|supercite)\{([^}]*)$/;
const REFERENCE_COMMAND = /\\(?:ref|eqref|pageref|autoref|cref|Cref)\{([^}]*)$/;
const FILE_COMMAND = /\\(input|include|subfile|includegraphics|bibliography|addbibresource)(?:\[[^\]]*\])?\{([^}]*)$/;

function currentArgument(value: string): { partial: string; offset: number } {
  const comma = value.lastIndexOf(',');
  const raw = comma >= 0 ? value.slice(comma + 1) : value;
  const leading = raw.length - raw.trimStart().length;
  return {
    partial: raw.trimStart(),
    offset: (comma >= 0 ? comma + 1 : 0) + leading,
  };
}

function rankedOptions<T extends Completion>(options: T[], partial: string): T[] {
  const needle = partial.toLowerCase();
  return options
    .filter((option) => !needle || option.label.toLowerCase().includes(needle) || option.detail?.toLowerCase().includes(needle))
    .sort((a, b) => {
      const aa = a.label.toLowerCase();
      const bb = b.label.toLowerCase();
      const aPrefix = aa.startsWith(needle);
      const bPrefix = bb.startsWith(needle);
      if (aPrefix !== bPrefix) return aPrefix ? -1 : 1;
      return aa.localeCompare(bb);
    })
    .slice(0, 40);
}

export function createLatexProjectCompletionSource(
  getData: NonNullable<LatexProjectFeatureConfig['getCompletionData']>,
): CompletionSource {
  return async (context: CompletionContext): Promise<CompletionResult | null> => {
    const before = context.state.sliceDoc(Math.max(0, context.pos - 512), context.pos);
    const citation = CITATION_COMMAND.exec(before);
    const reference = REFERENCE_COMMAND.exec(before);
    const file = FILE_COMMAND.exec(before);
    if (!citation && !reference && !file) return null;
    const data = await getData();

    if (citation) {
      const citationArgument = citation[1] ?? '';
      const argument = currentArgument(citationArgument);
      const options = rankedOptions(
        (data.citations ?? []).map((entry) => ({
          label: entry.key,
          type: 'variable',
          detail: entry.title || entry.detail || 'Citation',
        })),
        argument.partial,
      );
      if (!options.length) return null;
      return {
        from: context.pos - citationArgument.length + argument.offset,
        options,
        validFor: /^[^},\s]*$/,
      };
    }

    if (reference) {
      const referenceArgument = reference[1] ?? '';
      const argument = currentArgument(referenceArgument);
      const options = rankedOptions(
        (data.labels ?? []).map((entry) => ({
          label: entry.label,
          type: 'reference',
          detail: entry.detail || 'Label',
        })),
        argument.partial,
      );
      if (!options.length) return null;
      return {
        from: context.pos - referenceArgument.length + argument.offset,
        options,
        validFor: /^[^},\s]*$/,
      };
    }

    if (file) {
      const fileArgument = file[2] ?? '';
      const argument = currentArgument(fileArgument);
      const options = rankedOptions(
        (data.files ?? []).map((entry) => ({
          label: entry.path,
          type: 'file',
          detail: entry.detail || 'Project file',
        })),
        argument.partial,
      );
      if (!options.length) return null;
      return {
        from: context.pos - fileArgument.length + argument.offset,
        options,
        validFor: /^[^},\s]*$/,
      };
    }

    return null;
  };
}

export function detectLatexNavigationTarget(doc: string, position: number): LatexNavigationTarget | undefined {
  const start = Math.max(0, position - 384);
  const end = Math.min(doc.length, position + 384);
  const text = doc.slice(start, end);
  const patterns: Array<{ regex: RegExp; make: (match: RegExpExecArray) => LatexNavigationTarget; group: number }> = [
    {
      regex: /\\(?:ref|eqref|pageref|autoref|cref|Cref)\{([^}]+)\}/g,
      make: (match) => ({ kind: 'reference', target: (match[1] ?? '').trim() }),
      group: 1,
    },
    {
      regex: /\\(?:cite|citep|citet|citealp|citeauthor|citeyear|autocite|parencite|textcite|footcite|supercite)\{([^}]+)\}/g,
      make: (match) => ({
        kind: 'citation',
        target: ((match[1] ?? '').split(',')[0] ?? '').trim(),
      }),
      group: 1,
    },
    {
      regex: /\\(input|include|subfile|includegraphics|bibliography|addbibresource)(?:\[[^\]]*\])?\{([^}]+)\}/g,
      make: (match) => ({
        kind: 'file',
        command: match[1] ?? '',
        target: (match[2] ?? '').trim(),
      }),
      group: 2,
    },
  ];

  for (const pattern of patterns) {
    while (true) {
      const match = pattern.regex.exec(text);
      if (!match) break;
      const raw = match[pattern.group];
      if (raw === undefined) continue;
      const relative = match[0].indexOf(raw);
      const from = start + match.index + relative;
      const to = from + raw.length;
      if (position >= from && position <= to) return pattern.make(match);
    }
  }
  return undefined;
}

export function createLatexLinkNavigationExtension(
  onNavigate: NonNullable<LatexProjectFeatureConfig['onNavigate']>,
): Extension {
  return EditorView.domEventHandlers({
    mousedown(event, view) {
      if (event.button !== 0 || (!event.metaKey && !event.ctrlKey)) return false;
      const position = view.posAtCoords({ x: event.clientX, y: event.clientY });
      if (position === null) return false;
      const target = detectLatexNavigationTarget(view.state.doc.toString(), position);
      if (!target) return false;
      event.preventDefault();
      void onNavigate(target);
      return true;
    },
  });
}

function clipboardFile(event: ClipboardEvent): File | undefined {
  const direct = Array.from(event.clipboardData?.files ?? []);
  const file = direct.find((candidate) => candidate.type.startsWith('image/') || candidate.type === 'application/pdf');
  if (file) return file;
  for (const item of Array.from(event.clipboardData?.items ?? [])) {
    if (item.kind !== 'file') continue;
    const candidate = item.getAsFile();
    if (candidate && (candidate.type.startsWith('image/') || candidate.type === 'application/pdf')) return candidate;
  }
  return undefined;
}

export function createLatexPasteExtension(
  onPasteFile: NonNullable<LatexProjectFeatureConfig['onPasteFile']>,
): Extension {
  return EditorView.domEventHandlers({
    paste(event) {
      const file = clipboardFile(event);
      if (!file) return false;
      event.preventDefault();
      void onPasteFile(file);
      return true;
    },
  });
}

const searchHighlight = Decoration.mark({ class: 'cm-interactive-search-highlight' });
export const setLatexSearchHighlights = StateEffect.define<{ query: string; caseSensitive?: boolean }>();
export const clearLatexSearchHighlights = StateEffect.define<null>();

const searchHighlightField = StateField.define<DecorationSet>({
  create: () => Decoration.none,
  update(decorations, transaction) {
    let nextDecorations = decorations.map(transaction.changes);
    for (const effect of transaction.effects) {
      if (effect.is(clearLatexSearchHighlights)) {
        nextDecorations = Decoration.none;
      }
      if (effect.is(setLatexSearchHighlights)) {
        const query = effect.value.query;
        if (!query) {
          nextDecorations = Decoration.none;
          continue;
        }
        const text = transaction.state.doc.toString();
        const haystack = effect.value.caseSensitive ? text : text.toLowerCase();
        const needle = effect.value.caseSensitive ? query : query.toLowerCase();
        const ranges = [] as ReturnType<typeof searchHighlight.range>[];
        let index = haystack.indexOf(needle);
        while (index !== -1) {
          ranges.push(searchHighlight.range(index, index + query.length));
          index += Math.max(1, query.length);
          index = haystack.indexOf(needle, index);
        }
        nextDecorations = Decoration.set(ranges);
      }
    }
    return nextDecorations;
  },
  provide: (field) => EditorView.decorations.from(field),
});

export const latexSearchHighlightExtension: Extension = [
  searchHighlightField,
  EditorView.baseTheme({
    '.cm-interactive-search-highlight': {
      backgroundColor: 'color-mix(in srgb, var(--accent) 45%, transparent)',
      borderRadius: '2px',
    },
  }),
];

export function createLatexProjectFeatureExtensions(config: LatexProjectFeatureConfig): {
  extensions: Extension[];
  completionSource?: CompletionSource;
} {
  const extensions: Extension[] = [latexSearchHighlightExtension];
  if (config.onNavigate) extensions.push(createLatexLinkNavigationExtension(config.onNavigate));
  if (config.onPasteFile) extensions.push(createLatexPasteExtension(config.onPasteFile));
  return {
    extensions,
    ...(config.getCompletionData
      ? { completionSource: createLatexProjectCompletionSource(config.getCompletionData) }
      : {}),
  };
}


export type LatexReviewDecoration = {
  id: string;
  from: number;
  to: number;
  status: 'open' | 'resolved' | 'outdated';
  kind?: 'comment' | 'change';
  changeType?: 'insert' | 'delete' | 'replace';
  originalText?: string;
  proposedText?: string;
  authorName?: string;
  authorColor?: string;
  conflicted?: boolean;
  automatic?: boolean;
};

export const setLatexReviewDecorations = StateEffect.define<readonly LatexReviewDecoration[]>();

class ReviewPointWidget extends WidgetType {
  constructor(private readonly item: LatexReviewDecoration) { super(); }
  toDOM(): HTMLElement {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = `cm-interactive-review-point cm-interactive-review-${this.item.status}${this.item.kind === 'change' ? ` cm-interactive-change-${this.item.changeType ?? 'replace'}` : ''}${this.item.conflicted ? ' cm-interactive-review-conflicted' : ''}${this.item.automatic ? ' cm-interactive-ai-ghost' : ''}`;
    button.dataset.reviewThreadId = this.item.id;
    if (this.item.authorColor) button.style.setProperty('--learnitive-review-color', this.item.authorColor);
    const isChange = this.item.kind === 'change';
    button.setAttribute('aria-label', isChange ? 'Open tracked change' : 'Open review comment');
    const author = this.item.authorName ? ` · ${this.item.authorName}` : '';
    button.title = isChange && this.item.changeType === 'delete' ? `Deleted: ${this.item.originalText ?? ''}${author}` : isChange ? `Tracked change${author}` : `Review comment${author}`;
    button.textContent = isChange && this.item.changeType === 'delete' ? '−' : '●';
    return button;
  }
}

const reviewDecorationField = StateField.define<DecorationSet>({
  create: () => Decoration.none,
  update(decorations, transaction) {
    let next = decorations.map(transaction.changes);
    for (const effect of transaction.effects) {
      if (!effect.is(setLatexReviewDecorations)) continue;
      const ranges = effect.value.flatMap((item) => {
        const from = Math.max(0, Math.min(item.from, transaction.state.doc.length));
        const to = Math.max(from, Math.min(item.to, transaction.state.doc.length));
        if (from === to) {
          return [Decoration.widget({ widget: new ReviewPointWidget(item), side: 1 }).range(from)];
        }
        return [Decoration.mark({
          class: `cm-interactive-review-anchor cm-interactive-review-${item.status}${item.kind === 'change' ? ` cm-interactive-change-${item.changeType ?? 'replace'}` : ''}${item.conflicted ? ' cm-interactive-review-conflicted' : ''}${item.automatic ? ' cm-interactive-ai-ghost' : ''}`,
          attributes: {
            'data-review-thread-id': item.id,
            'aria-label': item.kind === 'change' ? 'Tracked change' : 'Review comment',
            ...(item.authorName ? { title: `${item.kind === 'change' ? 'Tracked change' : 'Review comment'} · ${item.authorName}` } : {}),
            ...(item.authorColor ? { style: `--learnitive-review-color:${item.authorColor}` } : {}),
          },
        }).range(from, to)];
      });
      next = Decoration.set(ranges, true);
    }
    return next;
  },
  provide: (field) => EditorView.decorations.from(field),
});

export function createLatexReviewExtension(onOpen: (threadId: string) => void): Extension {
  return [
    reviewDecorationField,
    EditorView.domEventHandlers({
      click(event) {
        const element = event.target instanceof Element ? event.target.closest<HTMLElement>('[data-review-thread-id]') : null;
        const id = element?.dataset.reviewThreadId;
        if (!id) return false;
        event.preventDefault();
        onOpen(id);
        return true;
      },
    }),
    EditorView.baseTheme({
      '.cm-interactive-review-anchor': {
        borderBottom: '2px solid color-mix(in srgb, var(--learnitive-review-color, var(--primary)) 72%, transparent)',
        backgroundColor: 'color-mix(in srgb, var(--learnitive-review-color, var(--primary)) 9%, transparent)',
        borderRadius: '2px',
        cursor: 'pointer',
      },
      '.cm-interactive-review-resolved': { opacity: '0.55' },
      '.cm-interactive-change-insert': { borderBottomColor: 'color-mix(in srgb, var(--learnitive-review-color, #15803d) 78%, transparent)', backgroundColor: 'color-mix(in srgb, var(--learnitive-review-color, #22c55e) 14%, transparent)' },
      '.cm-interactive-change-replace': { borderBottomColor: 'color-mix(in srgb, var(--learnitive-review-color, #b45309) 78%, transparent)', backgroundColor: 'color-mix(in srgb, var(--learnitive-review-color, #f59e0b) 14%, transparent)' },
      '.cm-interactive-change-delete': { color: 'var(--destructive)', fontWeight: '700' },
      '.cm-interactive-ai-ghost': {
        opacity: '0.52',
        fontStyle: 'italic',
        borderBottomStyle: 'dotted',
        backgroundColor: 'color-mix(in srgb, var(--muted-foreground) 7%, transparent)',
      },
      '.cm-interactive-review-outdated': {
        borderBottomStyle: 'dashed',
        borderBottomColor: 'color-mix(in srgb, var(--destructive) 75%, transparent)',
      },
      '.cm-interactive-review-conflicted': {
        borderBottomStyle: 'dashed',
        borderBottomColor: 'color-mix(in srgb, var(--destructive) 88%, transparent)',
        backgroundColor: 'color-mix(in srgb, var(--destructive) 10%, transparent)',
        boxShadow: 'inset 0 -1px 0 color-mix(in srgb, var(--destructive) 45%, transparent)',
      },
      '.cm-interactive-review-point': {
        border: '0', background: 'transparent', color: 'var(--learnitive-review-color, var(--primary))', padding: '0 2px', cursor: 'pointer', fontSize: '9px', verticalAlign: 'middle'
      },
    }),
  ];
}
