import { useEffect, useMemo, useState } from 'react';
import { Empty } from 'antd';
import { CodexDiffPreviewer } from '../previewer';
import type { CodexDiffPreviewerFile, CodexDiffPreviewerFileStatus } from '../previewer';

type RawTextModule = () => Promise<string | { default: string }>;

const repoTextModules = {
  ...import.meta.glob('/src/**/*.{ts,tsx,js,jsx,css,scss,html,json,md,mjs,yml,yaml}', {
    query: '?raw',
    import: 'default',
  }),
  ...import.meta.glob('/docs/**/*.{ts,tsx,js,jsx,css,scss,html,json,md,mjs,yml,yaml}', {
    query: '?raw',
    import: 'default',
  }),
  ...import.meta.glob('/scripts/**/*.{ts,tsx,js,jsx,css,scss,html,json,md,mjs,yml,yaml,sh}', {
    query: '?raw',
    import: 'default',
  }),
  ...import.meta.glob('/etc/**/*.{ts,tsx,js,jsx,css,scss,html,json,md,mjs,yml,yaml,sh}', {
    query: '?raw',
    import: 'default',
  }),
  ...import.meta.glob('/{README.md,package.json,package-lock.json,docker-compose.yml}', {
    query: '?raw',
    import: 'default',
  }),
} as Record<string, RawTextModule>;

const repoImageModules = {
  ...import.meta.glob('/src/**/*.{png,jpg,jpeg,gif,webp,svg,avif}', {
    eager: true,
    import: 'default',
  }),
  ...import.meta.glob('/docs/**/*.{png,jpg,jpeg,gif,webp,svg,avif}', {
    eager: true,
    import: 'default',
  }),
} as Record<string, string>;

type WorklogSourcePreviewProps = {
  sourceSectionContent: string;
  filesSectionContent?: string;
};

type ParsedChangedFile = {
  path: string;
  previousPath: string | null;
  status: CodexDiffPreviewerFileStatus;
};

const BINARY_FILE_PATTERN =
  /\.(?:png|jpe?g|gif|webp|svg|ico|pdf|zip|gz|tar|woff2?|ttf|eot|mp3|mp4|mov|avi|webm)$/i;
const IMAGE_FILE_PATTERN = /\.(?:png|jpe?g|gif|webp|svg|avif)$/i;

function stripSectionHeading(content: string) {
  return content.replace(/^#{2,3}\s+.+\n?/, '').trim();
}

function normalizeRepoPath(path: string) {
  return path.replace(/^`|`$/g, '').replace(/^\/+/, '').trim();
}

function inferLanguageFromPath(path: string) {
  const extension = path.split('.').pop()?.toLowerCase();

  switch (extension) {
    case 'ts':
      return 'ts';
    case 'tsx':
      return 'tsx';
    case 'js':
      return 'js';
    case 'jsx':
      return 'jsx';
    case 'css':
      return 'css';
    case 'scss':
      return 'scss';
    case 'html':
      return 'html';
    case 'json':
      return 'json';
    case 'md':
      return 'md';
    case 'mjs':
      return 'js';
    case 'yml':
    case 'yaml':
      return 'yaml';
    case 'sh':
      return 'bash';
    default:
      return 'text';
  }
}

function parseChangedFiles(filesSectionContent?: string) {
  if (!filesSectionContent?.trim()) {
    return [];
  }

  return stripSectionHeading(filesSectionContent)
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line.startsWith('- '))
    .map((line) => line.slice(2).trim())
    .map<ParsedChangedFile | null>((line) => {
      const renamedMatch = line.match(/^R\s+(.+?)\s+->\s+(.+)$/);
      if (renamedMatch) {
        return {
          path: normalizeRepoPath(renamedMatch[2]),
          previousPath: normalizeRepoPath(renamedMatch[1]),
          status: 'renamed',
        };
      }

      const statusMatch = line.match(/^([A-Z])\s+(.+)$/);
      if (statusMatch) {
        const [, rawStatus, rawPath] = statusMatch;
        const normalizedPath = normalizeRepoPath(rawPath);
        return {
          path: normalizedPath,
          previousPath: null,
          status:
            rawStatus === 'A'
              ? 'added'
              : rawStatus === 'M'
                ? 'modified'
                : rawStatus === 'D'
                  ? 'deleted'
                  : rawStatus === 'R'
                    ? 'renamed'
                    : BINARY_FILE_PATTERN.test(normalizedPath)
                      ? 'binary'
                      : 'unknown',
        };
      }

      if (!line || /^저장소 기준/.test(line)) {
        return null;
      }

      const normalizedPath = normalizeRepoPath(line);
      return normalizedPath
        ? {
            path: normalizedPath,
            previousPath: null,
            status: BINARY_FILE_PATTERN.test(normalizedPath) ? 'binary' : 'unknown',
          }
        : null;
    })
    .filter((file): file is ParsedChangedFile => Boolean(file));
}

function parseSourcePaths(sourceSectionContent: string) {
  const body = stripSectionHeading(sourceSectionContent);
  const headingPaths = body
    .split('\n')
    .map((line) => line.trim())
    .map((line) => line.match(/^###\s+파일\s+\d+:\s+`([^`]+)`$/)?.[1] ?? null)
    .filter((path): path is string => Boolean(path));
  const inlinePaths = Array.from(
    body.matchAll(/`((?:src|docs|scripts|etc|public|\.github)\/[^`\n]+?\.[a-z0-9]+)`/gi),
  ).map((match) => match[1]);
  const diffPaths = Array.from(body.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)).flatMap((match) => [
    match[1],
    match[2],
  ]);

  return Array.from(new Set([...headingPaths, ...inlinePaths, ...diffPaths])).map((path) => ({
    path: normalizeRepoPath(path),
    previousPath: null,
    status: 'unknown' as const,
  }));
}

function parseRawDiffText(sourceSectionContent: string) {
  const matches = Array.from(sourceSectionContent.matchAll(/```diff\s*\n([\s\S]*?)\n```/g));
  return matches.map((match) => match[1].trimEnd()).filter(Boolean).join('\n\n');
}

function normalizeRawTextModuleValue(value: string | { default: string }) {
  return typeof value === 'string' ? value : value.default;
}

function buildPreviewFiles(
  entries: ParsedChangedFile[],
  loadedTextByPath: Record<string, string | undefined>,
): CodexDiffPreviewerFile[] {
  return entries.map((entry) => {
    const normalizedPath = `/${entry.path}`;
    const isPublicFile = entry.path.startsWith('public/');
    const publicAssetUrl = isPublicFile ? `/${entry.path.slice('public/'.length)}` : null;
    const canLoadRawContent = Boolean(repoTextModules[normalizedPath]);
    const rawContent = loadedTextByPath[normalizedPath];
    const imageUrl = repoImageModules[normalizedPath] ?? (IMAGE_FILE_PATTERN.test(entry.path) ? publicAssetUrl : null);
    const isBinary = entry.status === 'binary' || BINARY_FILE_PATTERN.test(entry.path);
    const isImage = IMAGE_FILE_PATTERN.test(entry.path);

    return {
      path: entry.path,
      previousPath: entry.previousPath,
      status: isBinary ? 'binary' : entry.status,
      previewType:
        isImage && imageUrl
          ? 'image'
          : entry.status === 'deleted'
            ? 'empty'
            : 'code',
      language: inferLanguageFromPath(entry.path),
      content: isImage && imageUrl
        ? imageUrl
        : isBinary
          ? '바이너리 파일은 전체 소스를 표시하지 않습니다.'
        : isPublicFile
          ? 'public 디렉터리 파일은 번들 import 없이 정적 URL로만 제공되어 전체 소스 미리보기를 표시하지 않습니다.'
        : rawContent ??
            (canLoadRawContent
              ? '파일 내용을 불러오는 중입니다.'
              : entry.status === 'deleted'
                ? '삭제된 파일이라 현재 저장소에서 전체 소스를 불러올 수 없습니다.'
                : isImage
                  ? '이미지 파일 URL을 현재 저장소에서 찾지 못했습니다.'
                  : '현재 저장소에서 파일 내용을 불러올 수 없습니다.'),
    };
  });
}

function mergeSourceEntries(changedFiles: ParsedChangedFile[], sourcePaths: ParsedChangedFile[]) {
  const merged = new Map<string, ParsedChangedFile>();

  for (const entry of sourcePaths) {
    merged.set(entry.path, entry);
  }

  for (const entry of changedFiles) {
    merged.set(entry.path, entry);
  }

  return Array.from(merged.values());
}

export function WorklogSourcePreview({
  sourceSectionContent,
  filesSectionContent,
}: WorklogSourcePreviewProps) {
  const diffText = useMemo(() => parseRawDiffText(sourceSectionContent), [sourceSectionContent]);
  const sourcePaths = useMemo(() => {
    const changedFiles = parseChangedFiles(filesSectionContent);
    return mergeSourceEntries(changedFiles, parseSourcePaths(sourceSectionContent));
  }, [filesSectionContent, sourceSectionContent]);
  const [loadedTextByPath, setLoadedTextByPath] = useState<Record<string, string | undefined>>({});
  const files = useMemo(() => buildPreviewFiles(sourcePaths, loadedTextByPath), [loadedTextByPath, sourcePaths]);
  const description = diffText
    ? '변경 파일 기준 전체 소스와 raw diff를 Codex preview 스타일로 전환해 표시합니다.'
    : '변경 파일 기준 전체 소스를 Codex preview 스타일로 표시합니다. raw diff는 작업일지 `## 소스` 섹션의 diff 코드블록을 그대로 사용합니다.';

  useEffect(() => {
    let isMounted = true;
    const textModuleEntries = sourcePaths
      .map((entry) => `/${entry.path}`)
      .filter((path) => repoTextModules[path] && loadedTextByPath[path] === undefined);

    if (!textModuleEntries.length) {
      return () => {
        isMounted = false;
      };
    }

    Promise.all(
      textModuleEntries.map(async (path) => {
        const value = await repoTextModules[path]();
        return [path, normalizeRawTextModuleValue(value)] as const;
      }),
    )
      .then((loadedEntries) => {
        if (!isMounted) {
          return;
        }

        setLoadedTextByPath((current) => ({
          ...current,
          ...Object.fromEntries(loadedEntries),
        }));
      })
      .catch(() => {
        if (!isMounted) {
          return;
        }

        setLoadedTextByPath((current) => ({
          ...current,
          ...Object.fromEntries(textModuleEntries.map((path) => [path, '현재 저장소에서 파일 내용을 불러올 수 없습니다.'])),
        }));
      });

    return () => {
      isMounted = false;
    };
  }, [loadedTextByPath, sourcePaths]);

  if (!files.length && !diffText) {
    return <Empty description="기록된 소스 미리보기가 없습니다." />;
  }

  return (
    <CodexDiffPreviewer
      title="작업 소스"
      description={description}
      files={files}
      diffText={diffText || null}
      height="auto"
    />
  );
}
