import { VALID_MARKDOWN_EXTENSIONS_SET } from './constants';
import { type WsFilePath, WsPath } from './ws-path';

function decodeSegment(value: string): string | undefined {
  try {
    const decoded = decodeURIComponent(value);
    return decoded && !decoded.includes('/') && !decoded.includes('\\')
      ? decoded
      : undefined;
  } catch {
    return undefined;
  }
}

function hasUnsafeEncoding(value: string): boolean {
  if (!value.includes('%')) {
    return false;
  }
  try {
    const decoded = decodeURIComponent(value);
    return decoded.includes('/') || decoded.includes('\\');
  } catch {
    return true;
  }
}

function normalizeWikiLinkTarget(target: string): string | undefined {
  const normalizedTarget = target.trim();
  return normalizedTarget &&
    !normalizedTarget.includes('\\') &&
    !normalizedTarget.includes(':') &&
    !hasUnsafeEncoding(normalizedTarget)
    ? normalizedTarget
    : undefined;
}

function toExplicitWikiLinkBase(target: string): string {
  return /^\.\.?\//.test(target) ? target : `/${target.replace(/^\//, '')}`;
}

/** Resolves a URL-style path without allowing workspace escape or separators hidden by encoding. */
export function resolveInternalWsPath(
  currentWsPath: string | WsFilePath,
  target: string,
): WsFilePath | undefined {
  const current = WsPath.safeParse(currentWsPath).data?.asFile();
  if (!current || target.includes('//') || target.includes('?')) {
    return undefined;
  }

  const path = target.split('#', 1)[0] ?? '';
  if (!path) {
    return current;
  }

  const segments = path.startsWith('/')
    ? []
    : (current.getParent()?.path.split('/').filter(Boolean) ?? []);
  for (const encoded of path.replace(/^\//, '').split('/')) {
    if (!encoded || encoded === '.') {
      continue;
    }
    const segment = decodeSegment(encoded);
    if (!segment) {
      return undefined;
    }
    if (segment === '.') {
      continue;
    }
    if (segment === '..') {
      if (!segments.pop()) {
        return undefined;
      }
    } else {
      segments.push(segment);
    }
  }

  const result = WsPath.safeFromParts(current.wsName, segments.join('/')).data;
  const file = result?.asFile();
  return file?.extension &&
    VALID_MARKDOWN_EXTENSIONS_SET.has(file.extension.toLocaleLowerCase())
    ? file
    : undefined;
}

function withoutMarkdownExtension(path: string): string {
  return path.replace(/\.(?:md|markdown)$/i, '');
}

function hasMarkdownExtension(path: string): boolean {
  const extension = `.${path.split('.').at(-1)?.toLocaleLowerCase() ?? ''}`;
  return VALID_MARKDOWN_EXTENSIONS_SET.has(extension);
}

export type WikiLinkIndex = {
  wsName: string;
  notes: readonly WsFilePath[];
  searchRecords: readonly {
    wsPath: WsFilePath;
    searchText: string;
    target: string;
  }[];
  byWsPath: ReadonlyMap<string, WsFilePath>;
  byStem: ReadonlyMap<string, readonly WsFilePath[]>;
  byLowerStem: ReadonlyMap<string, readonly WsFilePath[]>;
};

function createWikiLinkTargetFromIndex(
  path: WsFilePath,
  index: Pick<WikiLinkIndex, 'byLowerStem'>,
): string {
  const stem = path.fileNameWithoutExtension;
  const matches = index.byLowerStem.get(stem.toLocaleLowerCase()) ?? [];
  return matches.length === 1
    ? stem
    : `/${withoutMarkdownExtension(path.filePath)}`;
}

function appendMapValue(
  map: Map<string, WsFilePath[]>,
  key: string,
  value: WsFilePath,
) {
  const existing = map.get(key);
  if (existing) {
    existing.push(value);
  } else {
    map.set(key, [value]);
  }
}

export function createWikiLinkIndex(
  wsPaths: readonly WsFilePath[],
  wsName: string,
): WikiLinkIndex {
  const notes = wsPaths.filter(
    (path) => path.wsName === wsName && path.isMarkdown(),
  );
  const byWsPath = new Map<string, WsFilePath>();
  const byStem = new Map<string, WsFilePath[]>();
  const byLowerStem = new Map<string, WsFilePath[]>();

  for (const note of notes) {
    byWsPath.set(note.wsPath, note);
    appendMapValue(byStem, note.fileNameWithoutExtension, note);
    appendMapValue(
      byLowerStem,
      note.fileNameWithoutExtension.toLocaleLowerCase(),
      note,
    );
  }

  const index = { byLowerStem };

  return {
    wsName,
    notes,
    searchRecords: notes.map((path) => ({
      wsPath: path,
      searchText: `${path.fileNameWithoutExtension} ${path.filePath}`,
      target: createWikiLinkTargetFromIndex(path, index),
    })),
    byWsPath,
    byStem,
    byLowerStem,
  };
}

function dirPrefix(path: WsFilePath): string {
  return path.filePath.slice(0, path.filePath.lastIndexOf('/') + 1);
}

/**
 * Picks the closest note when a bare wiki target matches several files:
 * same directory as the current note, then fewest path segments, then
 * lexicographic file path so resolution stays deterministic.
 */
function pickClosestWikiLinkMatch(
  current: WsFilePath,
  matches: readonly WsFilePath[],
): WsFilePath | undefined {
  const currentDir = dirPrefix(current);
  const compare = (a: WsFilePath, b: WsFilePath): number =>
    Number(dirPrefix(a) !== currentDir) - Number(dirPrefix(b) !== currentDir) ||
    a.filePath.split('/').length - b.filePath.split('/').length ||
    (a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0);
  return matches.reduce<WsFilePath | undefined>(
    (best, match) => (!best || compare(match, best) < 0 ? match : best),
    undefined,
  );
}

/** Resolves legacy and relative wiki targets against the known notes in one workspace. */
export function resolveWikiLinkTarget(
  currentWsPath: string | WsFilePath,
  target: string,
  index: WikiLinkIndex,
): WsFilePath | undefined {
  const current = WsPath.safeParse(currentWsPath).data?.asFile();
  const normalizedTarget = normalizeWikiLinkTarget(target);
  if (!current || !normalizedTarget) {
    return undefined;
  }
  if (index.wsName !== current.wsName) {
    return undefined;
  }
  const explicitPath = normalizedTarget.includes('/');

  if (explicitPath) {
    const base = toExplicitWikiLinkBase(normalizedTarget);
    for (const extension of ['', '.md', '.markdown']) {
      const resolved = resolveInternalWsPath(current, `${base}${extension}`);
      if (resolved) {
        const match = index.byWsPath.get(resolved.wsPath);
        if (match) return match;
      }
    }
    return undefined;
  }

  const stem = withoutMarkdownExtension(normalizedTarget);
  const exact = index.byStem.get(stem) ?? [];
  const matches = exact.length
    ? exact
    : (index.byLowerStem.get(stem.toLocaleLowerCase()) ?? []);
  return pickClosestWikiLinkMatch(current, matches);
}

/** Resolves where an unresolved wiki target should be created. */
export function createMissingWikiLinkTarget(
  currentWsPath: string | WsFilePath,
  target: string,
): WsFilePath | undefined {
  const current = WsPath.safeParse(currentWsPath).data?.asFile();
  const normalizedTarget = normalizeWikiLinkTarget(target);
  if (
    !current ||
    !normalizedTarget ||
    normalizedTarget.includes('#') ||
    normalizedTarget.includes('?')
  ) {
    return undefined;
  }

  const targetWithExtension = hasMarkdownExtension(normalizedTarget)
    ? normalizedTarget
    : `${normalizedTarget}${WsPath.DEFAULT_NOTE_EXTENSION}`;
  const explicitPath = targetWithExtension.includes('/');

  if (explicitPath) {
    return resolveInternalWsPath(
      current,
      toExplicitWikiLinkBase(targetWithExtension),
    );
  }

  return WsPath.safeFromParts(
    current.wsName,
    targetWithExtension,
  ).data?.asFile();
}
