import { App, FuzzyMatch, prepareFuzzySearch, setIcon } from "obsidian";
import { LinkSuggestion } from "src/types/types";
import {
  AUDIO_TYPES,
  CODE_TYPES,
  ICON_NAME,
  IMAGE_TYPES,
  REG_LINKINDEX_INVALIDCHARS,
  VIDEO_TYPES,
} from "src/constants/constants";
import ExcalidrawPlugin from "src/core/main";

const isNonNull = <T>(value: T | null | undefined): value is T => value != null;

/**
 * Returns Obsidian link suggestions (files, aliases, unresolved) filtered for invalid characters.
 */
export const getLinkSuggestionsFiltered = (app: App): LinkSuggestion[] => {
  const suggestions = app.metadataCache.getLinkSuggestions?.();
  if (!suggestions) {
    return [];
  }
  const filtered = suggestions.filter(
    (x: LinkSuggestion) => !x.path.match(REG_LINKINDEX_INVALIDCHARS),
  );

  const now = Date.now();
  const weekMs = 7 * 24 * 60 * 60 * 1000;

  const isRecent = (s: LinkSuggestion) =>
    !!s.file && now - s.file.stat.mtime <= weekMs;

  return filtered.sort((a: LinkSuggestion, b: LinkSuggestion) => {
    const aRecent = isRecent(a);
    const bRecent = isRecent(b);

    if (aRecent !== bRecent) {
      return aRecent ? -1 : 1; // recent first
    }

    if (
      aRecent &&
      bRecent &&
      a.file &&
      b.file &&
      a.file.stat.mtime !== b.file.stat.mtime
    ) {
      return b.file.stat.mtime - a.file.stat.mtime; // newer recent first
    }

    const aKey = `${a.path}${a.alias ?? ""}`.toLowerCase();
    const bKey = `${b.path}${b.alias ?? ""}`.toLowerCase();
    return aKey.localeCompare(bKey);
  });
};

/**
 * Returns fuzzy matches sorted by fuzzy score, then recency, then name for stability.
 */
export const getSortedLinkMatches = (
  term: string,
  items: LinkSuggestion[],
): FuzzyMatch<LinkSuggestion>[] => {
  const search = prepareFuzzySearch(term);
  const matches = items
    .map((item) => {
      const target = `${item.path}${item.alias ? `|${item.alias}` : ""}`;
      const matchFull = search(target);

      if (!item.alias) {
        return matchFull ? { item, match: matchFull } : null;
      }

      // Also search alias alone; if better, shift ranges to alias segment in the full string
      const matchAliasOnly = search(item.alias);
      let best = matchFull;

      if (matchAliasOnly && (!best || matchAliasOnly.score > best.score)) {
        const aliasOffset = item.path.length + 1; // position where alias starts in "path|alias"
        const shiftedMatches = matchAliasOnly.matches.map(
          ([from, to]): [number, number] => [
            from + aliasOffset,
            to + aliasOffset,
          ],
        );
        best = {
          ...matchAliasOnly,
          matches: shiftedMatches,
        };
      }

      return best ? { item, match: best } : null;
    })
    .filter(isNonNull);
  const now = Date.now();
  const weekMs = 7 * 24 * 60 * 60 * 1000;
  const isRecent = (s: LinkSuggestion) =>
    !!s.file && now - s.file.stat.mtime <= weekMs;

  return matches.sort((a, b) => {
    const aScore = a?.match?.score ?? 0;
    const bScore = b?.match?.score ?? 0;
    if (aScore !== bScore) {
      return bScore - aScore;
    }

    const aRecent = isRecent(a.item);
    const bRecent = isRecent(b.item);
    if (aRecent !== bRecent) {
      return aRecent ? -1 : 1;
    } // recent first

    if (
      aRecent &&
      bRecent &&
      a.item.file &&
      b.item.file &&
      a.item.file.stat.mtime !== b.item.file.stat.mtime
    ) {
      return b.item.file.stat.mtime - a.item.file.stat.mtime; // newer recent first
    }

    const aKey = `${a.item.path}${a.item.alias ?? ""}`.toLowerCase();
    const bKey = `${b.item.path}${b.item.alias ?? ""}`.toLowerCase();
    return aKey.localeCompare(bKey);
  });
};

const renderTextWithHighlights = (
  titleEl: HTMLElement,
  text: string,
  matches: [number, number][],
) => {
  if (!matches?.length) {
    titleEl.setText(text);
    return;
  }
  const sorted = [...matches].sort((a, b) => a[0] - b[0]);
  for (let i = 0; i < text.length; i++) {
    const match = sorted.find((m) => m[0] === i);
    if (match) {
      const span = createSpan("suggestion-highlight");
      span.appendText(text.substring(match[0], match[1]));
      titleEl.appendChild(span);
      i = match[1] - 1;
      continue;
    }
    titleEl.appendText(text[i]);
  }
};

export const fuzzyMatchTextItems = <T>(
  term: string,
  items: T[],
  getText: (item: T) => string,
): FuzzyMatch<T>[] => {
  const search = prepareFuzzySearch(term ?? "");
  return items
    .map((item) => {
      const match = search(getText(item));
      return match ? { item, match } : null;
    })
    .filter(isNonNull);
};

export const fuzzyMatchParagraphsWithId = <
  T extends { id?: string; text: string },
>(
  term: string,
  items: T[],
): FuzzyMatch<T>[] => {
  const search = prepareFuzzySearch(term ?? "");
  const needle = term?.trim().replace(/^\^/, "").toLowerCase();
  return items
    .map((item) => {
      const display = `${item.id ? `^${item.id} ` : ""}${item.text}`;
      const match = search(display);
      if (!match) {
        return null;
      }
      if (item.id && needle) {
        const idLower = item.id.toLowerCase();
        if (idLower === needle) {
          match.score = Number.MAX_SAFE_INTEGER; // strongest priority for exact id
        } else if (idLower.includes(needle)) {
          match.score = match.score + 1000; // strong priority if id contains term
        }
      }
      return { item, match };
    })
    .filter(isNonNull)
    .sort((a, b) => (b.match?.score ?? 0) - (a.match?.score ?? 0));
};

export const renderHeadingSuggestionRow = (
  result: FuzzyMatch<{ heading: string; level?: number }>,
  note: string,
  itemEl: HTMLElement,
) => {
  const { item, match } = result || {};
  itemEl.addClass("suggestion-item");
  itemEl.addClass("mod-complex");
  const contentEl = itemEl.createDiv("suggestion-content");
  const titleEl = contentEl.createDiv("suggestion-title");
  if (!item) {
    titleEl.setText("No match");
    itemEl.addClass("is-selected");
    return;
  }
  renderTextWithHighlights(titleEl, item.heading, match?.matches ?? []);
  const noteEl = contentEl.createDiv("suggestion-note");
  noteEl.setText(note ?? "");
  const auxEl = itemEl.createDiv("suggestion-aux");
  const flair = auxEl.createSpan("suggestion-flair");
  flair.setText(`H${item.level ?? 1}`);
};

export const renderParagraphSuggestionRow = (
  result: FuzzyMatch<{ text: string; id?: string }>,
  note: string,
  itemEl: HTMLElement,
) => {
  const { item, match } = result || {};
  itemEl.addClass("suggestion-item");
  itemEl.addClass("mod-complex");
  const contentEl = itemEl.createDiv("suggestion-content");
  const titleEl = contentEl.createDiv("suggestion-title");
  if (!item) {
    titleEl.setText("No match");
    itemEl.addClass("is-selected");
    return;
  }
  const display = `${item.id ? `^${item.id} ` : ""}${item.text}`;
  renderTextWithHighlights(titleEl, display, match?.matches ?? []);
  const noteEl = contentEl.createDiv("suggestion-note");
  noteEl.setText(note ?? "");
  if (item.id) {
    const auxEl = itemEl.createDiv("suggestion-aux");
    const flair = auxEl.createSpan("suggestion-flair");
    flair.setText(`^${item.id}`);
  }
};

/**
 * Standard rendering for link suggestions with icons and highlights.
 */
export const renderLinkSuggestion = (
  plugin: ExcalidrawPlugin,
  result: FuzzyMatch<LinkSuggestion>,
  itemEl: HTMLElement,
  emptyStateText: string,
) => {
  const { item, match: matches } = result || {};
  itemEl.addClass("mod-complex");
  const contentEl = itemEl.createDiv("suggestion-content");
  const auxEl = itemEl.createDiv("suggestion-aux");
  const titleEl = contentEl.createDiv("suggestion-title");
  const noteEl = contentEl.createDiv("suggestion-note");

  if (!item) {
    titleEl.setText(emptyStateText);
    itemEl.addClass("is-selected");
    return;
  }

  const path = item.file?.path ?? item.path;
  // For unresolved (ghost) files, highlight the full path. For resolved files, skip folder prefixes.
  const pathLength = item.file
    ? path.length - (item.file?.name.length ?? 0)
    : 0;
  const matchElements = matches.matches.map(() =>
    createSpan("suggestion-highlight"),
  );
  const itemText = item.path + (item.alias ? `|${item.alias}` : "");
  for (let i = pathLength; i < itemText.length; i++) {
    const match = matches.matches.find((m) => m[0] === i);
    if (match) {
      const element = matchElements[matches.matches.indexOf(match)];
      titleEl.appendChild(element);
      element.appendText(itemText.substring(match[0], match[1]));
      i += match[1] - match[0] - 1;
      continue;
    }
    titleEl.appendText(itemText[i]);
  }
  noteEl.setText(path);

  if (!item.file) {
    setIcon(auxEl, "ghost");
  } else if (plugin.isExcalidrawFile(item.file)) {
    setIcon(auxEl, ICON_NAME);
  } else if (item.file.extension === "md") {
    setIcon(auxEl, "square-pen");
  } else if (IMAGE_TYPES.includes(item.file.extension)) {
    setIcon(auxEl, "image");
  } else if (VIDEO_TYPES.includes(item.file.extension)) {
    setIcon(auxEl, "monitor-play");
  } else if (AUDIO_TYPES.includes(item.file.extension)) {
    setIcon(auxEl, "file-audio");
  } else if (CODE_TYPES.includes(item.file.extension)) {
    setIcon(auxEl, "file-code");
  } else if (item.file.extension === "canvas") {
    setIcon(auxEl, "layout-dashboard");
  } else if (item.file.extension === "pdf") {
    setIcon(auxEl, "book-open-text");
  } else {
    auxEl.setText(item.file.extension);
  }
};
