import fs from 'node:fs';
import path from 'node:path';
import type { Plugin } from 'vite';

type PreprocessMode = 'serve' | 'build';

type AnnotationSourceMarkdownNode = {
  type?: unknown;
  markdownPath?: unknown;
  markdown?: unknown;
  children?: unknown;
  [key: string]: unknown;
};

type AnnotationSourceLike = {
  directory?: unknown;
  [key: string]: unknown;
};

export type ResolvedAnnotationMarkdownPath =
  | {
    ok: true;
    absolutePath: string;
    projectRelativePath: string;
    prototypeDir: string;
  }
  | {
    ok: false;
    reason: string;
  };

export interface AnnotationSourceMarkdownPreprocessResult<T extends AnnotationSourceLike = AnnotationSourceLike> {
  source: T;
  watchFiles: string[];
}

export interface AnnotationSourceMarkdownPreprocessOptions<T extends AnnotationSourceLike = AnnotationSourceLike> {
  projectRoot: string;
  sourceFilePath: string;
  source: T;
  mode?: PreprocessMode;
}

function createProjectRelativePath(projectRoot: string, absolutePath: string): string {
  return path.relative(projectRoot, absolutePath).split(path.sep).join('/');
}

function isPathInside(parentPath: string, childPath: string): boolean {
  const relativePath = path.relative(parentPath, childPath);
  return relativePath === '' || (
    !relativePath.startsWith('..')
    && !path.isAbsolute(relativePath)
  );
}

function safeDecodePath(value: string): string | null {
  try {
    return decodeURIComponent(value);
  } catch {
    return null;
  }
}

function isWindowsAbsolutePath(value: string): boolean {
  return /^[a-z]:[\\/]/iu.test(value);
}

function getPrototypeDirFromAnnotationSourcePath(
  projectRoot: string,
  sourceFilePath: string,
): string | null {
  const resolvedProjectRoot = path.resolve(projectRoot);
  const resolvedSourcePath = path.resolve(sourceFilePath);
  const relativeSourcePath = createProjectRelativePath(resolvedProjectRoot, resolvedSourcePath);
  const parts = relativeSourcePath.split('/');
  if (
    parts.length !== 4
    || parts[0] !== 'src'
    || parts[1] !== 'prototypes'
    || !parts[2]
    || parts[2].startsWith('.')
    || parts[2].includes('\0')
    || parts[3] !== 'annotation-source.json'
  ) {
    return null;
  }
  const prototypeDir = path.resolve(resolvedProjectRoot, 'src', 'prototypes', parts[2]);
  return isPathInside(resolvedProjectRoot, prototypeDir) ? prototypeDir : null;
}

export function resolveAnnotationMarkdownPath(
  projectRoot: string,
  sourceFilePath: string,
  markdownPath: unknown,
): ResolvedAnnotationMarkdownPath {
  const prototypeDir = getPrototypeDirFromAnnotationSourcePath(projectRoot, sourceFilePath);
  if (!prototypeDir) {
    return { ok: false, reason: 'annotation-source.json is not under src/prototypes/<id>' };
  }

  const rawPath = String(markdownPath ?? '').trim();
  const decodedPath = safeDecodePath(rawPath);
  if (!rawPath || !decodedPath) {
    return { ok: false, reason: 'markdownPath is empty or malformed' };
  }
  if (
    rawPath.includes('\0')
    || decodedPath.includes('\0')
    || path.isAbsolute(rawPath)
    || path.isAbsolute(decodedPath)
    || isWindowsAbsolutePath(rawPath)
    || isWindowsAbsolutePath(decodedPath)
    || rawPath.includes('\\')
    || decodedPath.includes('\\')
  ) {
    return { ok: false, reason: 'markdownPath must be a relative POSIX path' };
  }

  const rawSegments = rawPath.split('/');
  const decodedSegments = decodedPath.split('/');
  const hasUnsafeSegment = [...rawSegments, ...decodedSegments].some((segment) => (
    segment === ''
    || segment === '.'
    || segment === '..'
  ));
  if (hasUnsafeSegment) {
    return { ok: false, reason: 'markdownPath contains unsafe path segments' };
  }

  const absolutePath = path.resolve(prototypeDir, decodedPath);
  const resolvedProjectRoot = path.resolve(projectRoot);
  if (!isPathInside(prototypeDir, absolutePath) || !isPathInside(resolvedProjectRoot, absolutePath)) {
    return { ok: false, reason: 'markdownPath escapes the prototype directory' };
  }

  return {
    ok: true,
    absolutePath,
    projectRelativePath: createProjectRelativePath(resolvedProjectRoot, absolutePath),
    prototypeDir,
  };
}

function preprocessDirectoryNode(
  node: unknown,
  options: {
    projectRoot: string;
    sourceFilePath: string;
    mode: PreprocessMode;
    watchFiles: Set<string>;
  },
): unknown {
  if (!node || typeof node !== 'object' || Array.isArray(node)) {
    return node;
  }

  const record = node as AnnotationSourceMarkdownNode;
  if (record.type === 'folder') {
    return {
      ...record,
      children: Array.isArray(record.children)
        ? record.children.map((child) => preprocessDirectoryNode(child, options))
        : record.children,
    };
  }

  if (record.type !== 'markdown') {
    return record;
  }

  const nextNode: AnnotationSourceMarkdownNode = { ...record };
  delete nextNode.markdownEditUrl;

  if (typeof record.markdownPath !== 'string' || !record.markdownPath.trim()) {
    return nextNode;
  }

  const resolved = resolveAnnotationMarkdownPath(
    options.projectRoot,
    options.sourceFilePath,
    record.markdownPath,
  );
  if (resolved.ok === false) {
    if (options.mode === 'serve') {
      console.warn(`[annotation-source] Ignored unsafe markdownPath "${record.markdownPath}": ${resolved.reason}`);
    }
    return nextNode;
  }

  options.watchFiles.add(resolved.absolutePath);
  if (fs.existsSync(resolved.absolutePath)) {
    nextNode.markdown = fs.readFileSync(resolved.absolutePath, 'utf8');
  } else if (options.mode === 'serve') {
    console.warn(`[annotation-source] Markdown file not found for markdownPath "${record.markdownPath}": ${resolved.projectRelativePath}`);
  }

  return nextNode;
}

export function preprocessAnnotationSourceMarkdown<T extends AnnotationSourceLike>(
  options: AnnotationSourceMarkdownPreprocessOptions<T>,
): AnnotationSourceMarkdownPreprocessResult<T> {
  const mode = options.mode ?? 'build';
  const source = options.source;
  const directoryRecord = source?.directory && typeof source.directory === 'object' && !Array.isArray(source.directory)
    ? source.directory as Record<string, unknown>
    : null;
  const nodes = directoryRecord?.nodes;
  if (!Array.isArray(nodes) || !getPrototypeDirFromAnnotationSourcePath(options.projectRoot, options.sourceFilePath)) {
    return { source, watchFiles: [] };
  }

  const watchFiles = new Set<string>();
  const nextSource = {
    ...source,
    directory: {
      ...directoryRecord,
      nodes: nodes.map((node) => preprocessDirectoryNode(node, {
        projectRoot: options.projectRoot,
        sourceFilePath: options.sourceFilePath,
        mode,
        watchFiles,
      })),
    },
  } as T;

  return {
    source: nextSource,
    watchFiles: Array.from(watchFiles),
  };
}

function cleanModuleId(id: string): string {
  return id.split(/[?#]/u)[0] || id;
}

export function createAnnotationSourceMarkdownPlugin(
  projectRoot = process.cwd(),
  options: { mode?: PreprocessMode } = {},
): Plugin {
  const mode = options.mode ?? 'build';
  return {
    name: 'axhub-annotation-source-markdown',
    enforce: 'pre',
    transform(code, id) {
      const filePath = cleanModuleId(id);
      if (path.basename(filePath) !== 'annotation-source.json') {
        return null;
      }
      if (!getPrototypeDirFromAnnotationSourcePath(projectRoot, filePath)) {
        return null;
      }

      const source = JSON.parse(code) as AnnotationSourceLike;
      const result = preprocessAnnotationSourceMarkdown({
        projectRoot,
        sourceFilePath: filePath,
        source,
        mode,
      });
      for (const watchFile of result.watchFiles) {
        this.addWatchFile(watchFile);
      }
      return `${JSON.stringify(result.source, null, 2)}\n`;
    },
  };
}
