import { randomUUID } from 'node:crypto';
import { createReadStream, type ReadStream } from 'node:fs';
import { accessSync, existsSync } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';

export type ResourceManagerEntryType = 'file' | 'directory';

export type ResourceManagerTreeNode = {
  name: string;
  path: string;
  type: ResourceManagerEntryType;
  extension: string | null;
  size: number | null;
  modifiedAt: string;
  previewUrl: string | null;
  children?: ResourceManagerTreeNode[];
};

export type ResourceManagerTreeRoot = {
  label: string;
  rootPath: string;
  tree: ResourceManagerTreeNode;
};

export type ResourceManagerDirectoryEntry = {
  name: string;
  path: string;
  type: ResourceManagerEntryType;
  extension: string | null;
  size: number | null;
  modifiedAt: string;
  previewUrl: string | null;
};

export type ResourceManagerFileDetail = {
  name: string;
  path: string;
  extension: string | null;
  size: number;
  modifiedAt: string;
  mimeType: string;
  previewUrl: string;
  isTextEditable: boolean;
  content: string | null;
};

export type ResourceManagerPreviewStream = {
  contentType: string;
  size: number;
  createStream: (range?: { start?: number; end?: number }) => ReadStream;
};

class ResourceManagerError extends Error {
  statusCode: number;

  constructor(message: string, statusCode = 400) {
    super(message);
    this.name = 'ResourceManagerError';
    this.statusCode = statusCode;
  }
}

const RESOURCE_MANAGER_ROOT_DIR = 'resource';
const RESOURCE_MANAGER_ROOT_LABEL = 'resource';
const LEGACY_PUBLIC_RESOURCE_ROOT_DIR = path.join('public', 'resource');

const TEXT_FILE_EXTENSIONS = new Set([
  '.txt',
  '.md',
  '.markdown',
  '.json',
  '.yaml',
  '.yml',
  '.js',
  '.jsx',
  '.ts',
  '.tsx',
  '.css',
  '.scss',
  '.sass',
  '.less',
  '.html',
  '.htm',
  '.xml',
  '.svg',
  '.csv',
  '.log',
  '.env',
  '.ini',
  '.sh',
  '.sql',
  '.py',
  '.java',
  '.kt',
  '.go',
  '.rs',
  '.c',
  '.cc',
  '.cpp',
  '.h',
  '.hpp',
  '.diff',
]);

export function resolveStaticContentType(filePath: string) {
  const extension = path.extname(filePath).toLowerCase();

  switch (extension) {
    case '.ts':
    case '.tsx':
    case '.js':
    case '.jsx':
    case '.mjs':
    case '.cjs':
    case '.json':
    case '.css':
    case '.txt':
    case '.diff':
    case '.log':
    case '.csv':
    case '.yaml':
    case '.yml':
    case '.xml':
      return 'text/plain; charset=utf-8';
    case '.html':
    case '.htm':
      return 'text/html; charset=utf-8';
    case '.md':
    case '.markdown':
      return 'text/markdown; charset=utf-8';
    case '.svg':
      return 'image/svg+xml';
    case '.png':
      return 'image/png';
    case '.jpg':
    case '.jpeg':
      return 'image/jpeg';
    case '.gif':
      return 'image/gif';
    case '.webp':
      return 'image/webp';
    case '.wav':
      return 'audio/wav';
    case '.mp3':
      return 'audio/mpeg';
    case '.ogg':
      return 'audio/ogg';
    case '.m4a':
      return 'audio/mp4';
    case '.mp4':
      return 'video/mp4';
    case '.webm':
      return 'video/webm';
    case '.mov':
      return 'video/quicktime';
    case '.m4v':
      return 'video/x-m4v';
    case '.ogv':
      return 'video/ogg';
    case '.pdf':
      return 'application/pdf';
    default:
      return 'application/octet-stream';
  }
}

function isTextEditable(filePath: string) {
  const extension = path.extname(filePath).toLowerCase();
  return TEXT_FILE_EXTENSIONS.has(extension);
}

function sanitizeEntryName(name: string) {
  const trimmed = name.trim().replace(/[\\/:"*?<>|]+/g, '-').replace(/\s+/g, ' ');

  if (!trimmed || trimmed === '.' || trimmed === '..') {
    throw new ResourceManagerError('이름이 올바르지 않습니다.');
  }

  return trimmed;
}

function normalizeRelativeTarget(relativePath: string | null | undefined) {
  const trimmed = String(relativePath ?? '').trim().replace(/\\/g, '/').replace(/^\/+/, '');

  if (!trimmed) {
    return '';
  }

  const normalized = path.posix.normalize(trimmed);

  if (normalized === '.' || normalized === '') {
    return '';
  }

  if (normalized.startsWith('../') || normalized === '..' || normalized.includes('/../')) {
    throw new ResourceManagerError('허용되지 않은 경로입니다.');
  }

  return normalized.replace(/^\/+/, '');
}

function decodeRepeatedly(value: string, maxIterations = 3) {
  let current = value;

  for (let index = 0; index < maxIterations; index += 1) {
    try {
      const decoded = decodeURIComponent(current);

      if (!decoded || decoded === current) {
        break;
      }

      current = decoded;
    } catch {
      break;
    }
  }

  return current;
}

function normalizePreviewTargetPath(targetPath: string) {
  const normalized = normalizeRelativeTarget(targetPath);

  if (!normalized) {
    return normalized;
  }

  const segments = normalized.split('/');
  const lastSegment = segments.at(-1) ?? '';
  const decodedLastSegment = decodeRepeatedly(lastSegment);
  const hashIndex = decodedLastSegment.lastIndexOf('#');

  if (hashIndex <= 0) {
    return normalized;
  }

  const fileName = decodedLastSegment.slice(0, hashIndex).trim();

  if (!fileName || !/\.[a-z0-9]{1,16}$/i.test(fileName)) {
    return normalized;
  }

  segments[segments.length - 1] = fileName;
  return normalizeRelativeTarget(segments.join('/'));
}

function resolveRepoRoot(candidateRootPath: string) {
  const candidates = [
    candidateRootPath,
    path.resolve(process.cwd(), '../../..'),
  ];

  for (const candidate of candidates) {
    try {
      accessSync(candidate);
      return candidate;
    } catch {
      continue;
    }
  }

  return candidates[0];
}

export function resolveResourceManagerRoot(repoRootPath: string) {
  return path.join(resolveRepoRoot(repoRootPath), RESOURCE_MANAGER_ROOT_DIR);
}

function resolveLegacyPublicResourceRoot(repoRootPath: string) {
  return path.join(resolveRepoRoot(repoRootPath), LEGACY_PUBLIC_RESOURCE_ROOT_DIR);
}

function resolveLegacyPublicResourcePreviewPath(repoRootPath: string, targetPath: string) {
  const normalizedRelativePath = normalizeRelativeTarget(targetPath);

  if (!normalizedRelativePath) {
    throw new ResourceManagerError('리소스를 찾을 수 없습니다.', 404);
  }

  const rootPath = resolveLegacyPublicResourceRoot(repoRootPath);
  const absolutePath = path.resolve(rootPath, normalizedRelativePath);

  if (absolutePath !== rootPath && !absolutePath.startsWith(`${rootPath}${path.sep}`)) {
    throw new ResourceManagerError('허용되지 않은 경로입니다.', 403);
  }

  return {
    absolutePath,
    relativePath: normalizedRelativePath,
  };
}

function resolveResourceManagerTargetPath(repoRootPath: string, relativePath: string) {
  const rootPath = resolveResourceManagerRoot(repoRootPath);
  const normalizedRelativePath = normalizeRelativeTarget(relativePath);
  const absolutePath = path.resolve(rootPath, normalizedRelativePath);

  if (absolutePath !== rootPath && !absolutePath.startsWith(`${rootPath}${path.sep}`)) {
    throw new ResourceManagerError('허용되지 않은 경로입니다.');
  }

  return {
    rootPath,
    absolutePath,
    relativePath: normalizedRelativePath,
  };
}

function toResourceManagerError(error: unknown) {
  if (error instanceof ResourceManagerError) {
    return error;
  }

  if (error && typeof error === 'object' && 'code' in error) {
    const code = String((error as { code?: string }).code ?? '');

    switch (code) {
      case 'ENOENT':
        return new ResourceManagerError('항목을 찾을 수 없습니다.', 404);
      case 'EEXIST':
        return new ResourceManagerError('같은 이름의 항목이 이미 존재합니다.', 409);
      case 'ENOTDIR':
        return new ResourceManagerError('디렉터리가 아닙니다.', 400);
      case 'EISDIR':
        return new ResourceManagerError('파일이 아닙니다.', 400);
      case 'ENOTEMPTY':
        return new ResourceManagerError('비어 있지 않은 폴더입니다.', 409);
      case 'EACCES':
      case 'EPERM':
        return new ResourceManagerError('항목에 접근할 권한이 없습니다.', 403);
      default:
        break;
    }
  }

  if (error instanceof Error) {
    return new ResourceManagerError(error.message || '리소스 작업에 실패했습니다.', 500);
  }

  return new ResourceManagerError('리소스 작업에 실패했습니다.', 500);
}

async function withResourceManagerError<T>(callback: () => Promise<T>) {
  try {
    return await callback();
  } catch (error) {
    throw toResourceManagerError(error);
  }
}

function buildPreviewUrl(relativePath: string) {
  const encodedPath = normalizeRelativeTarget(relativePath)
    .split('/')
    .filter(Boolean)
    .map((segment) => encodeURIComponent(segment))
    .join('/');

  return `/api/resource-manager/preview/${encodedPath}`;
}

function resolveLatestModifiedAt(currentModifiedAt: string, childModifiedAts: string[]) {
  let latestTime = Date.parse(currentModifiedAt);
  let latestModifiedAt = currentModifiedAt;

  for (const childModifiedAt of childModifiedAts) {
    const childTime = Date.parse(childModifiedAt);

    if (!Number.isFinite(childTime)) {
      continue;
    }

    if (!Number.isFinite(latestTime) || childTime > latestTime) {
      latestTime = childTime;
      latestModifiedAt = childModifiedAt;
    }
  }

  return latestModifiedAt;
}

async function resolveDirectoryLatestModifiedAt(absolutePath: string, stats?: Awaited<ReturnType<typeof fs.stat>>) {
  const currentStats = stats ?? (await fs.stat(absolutePath));
  let latestModifiedAt = currentStats.mtime.toISOString();
  const entries = await fs.readdir(absolutePath, { withFileTypes: true });

  for (const entry of entries) {
    const entryAbsolutePath = path.join(absolutePath, entry.name);
    const entryStats = await fs.stat(entryAbsolutePath);
    const entryModifiedAt = entry.isDirectory()
      ? await resolveDirectoryLatestModifiedAt(entryAbsolutePath, entryStats)
      : entryStats.mtime.toISOString();

    latestModifiedAt = resolveLatestModifiedAt(latestModifiedAt, [entryModifiedAt]);
  }

  return latestModifiedAt;
}

async function buildTreeNode(absolutePath: string, relativePath: string): Promise<ResourceManagerTreeNode> {
  const stats = await fs.stat(absolutePath);
  const type: ResourceManagerEntryType = stats.isDirectory() ? 'directory' : 'file';
  const extension = type === 'file' ? path.extname(absolutePath).toLowerCase() || null : null;

  const node: ResourceManagerTreeNode = {
    name: relativePath ? path.basename(relativePath) : RESOURCE_MANAGER_ROOT_LABEL,
    path: normalizeRelativeTarget(relativePath),
    type,
    extension,
    size: type === 'file' ? stats.size : null,
    modifiedAt: stats.mtime.toISOString(),
    previewUrl: type === 'file' ? buildPreviewUrl(relativePath) : null,
  };

  if (type === 'directory') {
    const entries = await fs.readdir(absolutePath, { withFileTypes: true });
    const children = await Promise.all(
      entries
        .sort((left, right) => {
          if (left.isDirectory() && !right.isDirectory()) {
            return -1;
          }

          if (!left.isDirectory() && right.isDirectory()) {
            return 1;
          }

          return left.name.localeCompare(right.name, 'ko');
        })
        .map((entry) =>
          buildTreeNode(path.join(absolutePath, entry.name), path.posix.join(relativePath, entry.name)),
        ),
    );

    node.children = children;
    node.modifiedAt = resolveLatestModifiedAt(
      node.modifiedAt,
      children.map((child) => child.modifiedAt),
    );
  }

  return node;
}

export async function ensureResourceManagerRoot(repoRootPath: string) {
  return withResourceManagerError(async () => {
    const rootPath = resolveResourceManagerRoot(repoRootPath);
    await fs.mkdir(rootPath, { recursive: true });
  });
}

export async function getResourceManagerTree(repoRootPath: string) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);

    return {
      label: RESOURCE_MANAGER_ROOT_LABEL,
      rootPath: RESOURCE_MANAGER_ROOT_DIR,
      tree: await buildTreeNode(resolveResourceManagerRoot(repoRootPath), ''),
    } satisfies ResourceManagerTreeRoot;
  });
}

export async function listResourceManagerDirectory(repoRootPath: string, directoryPath = '') {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const { absolutePath, relativePath } = resolveResourceManagerTargetPath(repoRootPath, directoryPath);
    const stats = await fs.stat(absolutePath);

    if (!stats.isDirectory()) {
      throw new ResourceManagerError('디렉터리가 아닙니다.');
    }

    const entries = await fs.readdir(absolutePath, { withFileTypes: true });
    const items: ResourceManagerDirectoryEntry[] = await Promise.all(
      entries
        .sort((left, right) => {
          if (left.isDirectory() && !right.isDirectory()) {
            return -1;
          }

          if (!left.isDirectory() && right.isDirectory()) {
            return 1;
          }

          return left.name.localeCompare(right.name, 'ko');
        })
        .map(async (entry) => {
          const entryRelativePath = path.posix.join(relativePath, entry.name);
          const entryAbsolutePath = path.join(absolutePath, entry.name);
          const entryStats = await fs.stat(entryAbsolutePath);
          const type: ResourceManagerEntryType = entry.isDirectory() ? 'directory' : 'file';

          return {
            name: entry.name,
            path: entryRelativePath,
            type,
            extension: type === 'file' ? path.extname(entry.name).toLowerCase() || null : null,
            size: type === 'file' ? entryStats.size : null,
            modifiedAt:
              type === 'directory'
                ? await resolveDirectoryLatestModifiedAt(entryAbsolutePath, entryStats)
                : entryStats.mtime.toISOString(),
            previewUrl: type === 'file' ? buildPreviewUrl(entryRelativePath) : null,
          };
        }),
    );

    return {
      path: relativePath,
      items,
    };
  });
}

export async function readResourceManagerFile(repoRootPath: string, filePath: string) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const { absolutePath, relativePath } = resolveResourceManagerTargetPath(repoRootPath, filePath);
    const stats = await fs.stat(absolutePath);

    if (!stats.isFile()) {
      throw new ResourceManagerError('파일이 아닙니다.');
    }

    const textEditable = isTextEditable(absolutePath);

    return {
      name: path.basename(relativePath),
      path: relativePath,
      extension: path.extname(absolutePath).toLowerCase() || null,
      size: stats.size,
      modifiedAt: stats.mtime.toISOString(),
      mimeType: resolveStaticContentType(absolutePath),
      previewUrl: buildPreviewUrl(relativePath),
      isTextEditable: textEditable,
      content: textEditable ? await fs.readFile(absolutePath, 'utf8') : null,
    } satisfies ResourceManagerFileDetail;
  });
}

export async function createResourceManagerDirectory(repoRootPath: string, parentPath: string, name: string) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const safeName = sanitizeEntryName(name);
    const { absolutePath } = resolveResourceManagerTargetPath(repoRootPath, path.posix.join(parentPath, safeName));
    await fs.mkdir(absolutePath, { recursive: false });
  });
}

export async function createResourceManagerFile(repoRootPath: string, parentPath: string, name: string, content = '') {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const safeName = sanitizeEntryName(name);
    const { absolutePath } = resolveResourceManagerTargetPath(repoRootPath, path.posix.join(parentPath, safeName));
    await fs.mkdir(path.dirname(absolutePath), { recursive: true });
    await fs.writeFile(absolutePath, content, 'utf8');
  });
}

export async function saveResourceManagerFile(repoRootPath: string, filePath: string, content: string) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const { absolutePath } = resolveResourceManagerTargetPath(repoRootPath, filePath);
    await fs.writeFile(absolutePath, content, 'utf8');
  });
}

export async function uploadResourceManagerFile(
  repoRootPath: string,
  parentPath: string,
  fileName: string,
  contentBase64: string,
) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const safeFileName = sanitizeEntryName(fileName);
    const { absolutePath, relativePath } = resolveResourceManagerTargetPath(repoRootPath, path.posix.join(parentPath, safeFileName));
    const buffer = Buffer.from(contentBase64, 'base64');

    if (!buffer.byteLength) {
      throw new ResourceManagerError('업로드할 파일 내용을 찾지 못했습니다.');
    }

    await fs.mkdir(path.dirname(absolutePath), { recursive: true });
    await fs.writeFile(absolutePath, buffer);

    return {
      id: randomUUID(),
      name: safeFileName,
      path: relativePath,
      previewUrl: buildPreviewUrl(relativePath),
      size: buffer.byteLength,
    };
  });
}

async function resolveCopyMoveTarget(
  repoRootPath: string,
  sourcePath: string,
  targetDirectoryPath: string,
  nextName?: string | null,
) {
  const sourceTarget = resolveResourceManagerTargetPath(repoRootPath, sourcePath);
  const sourceStats = await fs.stat(sourceTarget.absolutePath);
  const resolvedName = sanitizeEntryName(nextName?.trim() || path.basename(sourceTarget.relativePath));
  const targetTarget = resolveResourceManagerTargetPath(repoRootPath, path.posix.join(targetDirectoryPath, resolvedName));

  if (sourceTarget.relativePath === targetTarget.relativePath) {
    throw new ResourceManagerError('같은 위치로는 복사하거나 이동할 수 없습니다.');
  }

  return {
    sourceAbsolutePath: sourceTarget.absolutePath,
    sourceRelativePath: sourceTarget.relativePath,
    sourceStats,
    targetAbsolutePath: targetTarget.absolutePath,
    targetRelativePath: targetTarget.relativePath,
  };
}

export async function copyResourceManagerItem(
  repoRootPath: string,
  sourcePath: string,
  targetDirectoryPath: string,
  nextName?: string | null,
) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const target = await resolveCopyMoveTarget(repoRootPath, sourcePath, targetDirectoryPath, nextName);
    await fs.mkdir(path.dirname(target.targetAbsolutePath), { recursive: true });
    await fs.cp(target.sourceAbsolutePath, target.targetAbsolutePath, {
      recursive: target.sourceStats.isDirectory(),
      force: false,
      errorOnExist: true,
    });

    return {
      path: target.targetRelativePath,
    };
  });
}

export async function moveResourceManagerItem(
  repoRootPath: string,
  sourcePath: string,
  targetDirectoryPath: string,
  nextName?: string | null,
) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const target = await resolveCopyMoveTarget(repoRootPath, sourcePath, targetDirectoryPath, nextName);
    await fs.mkdir(path.dirname(target.targetAbsolutePath), { recursive: true });
    await fs.rename(target.sourceAbsolutePath, target.targetAbsolutePath);

    return {
      path: target.targetRelativePath,
    };
  });
}

export async function deleteResourceManagerItem(repoRootPath: string, targetPath: string) {
  return withResourceManagerError(async () => {
    await ensureResourceManagerRoot(repoRootPath);
    const { absolutePath, relativePath } = resolveResourceManagerTargetPath(repoRootPath, targetPath);

    if (!relativePath) {
      throw new ResourceManagerError('루트 폴더는 삭제할 수 없습니다.');
    }

    await fs.rm(absolutePath, { recursive: true, force: false });
  });
}

export async function openResourceManagerPreviewStream(repoRootPath: string, targetPath: string) {
  return withResourceManagerError(async () => {
    const normalizedTargetPath = normalizePreviewTargetPath(targetPath);
    const primaryTarget = resolveResourceManagerTargetPath(repoRootPath, normalizedTargetPath);
    const legacyTarget = resolveLegacyPublicResourcePreviewPath(repoRootPath, normalizedTargetPath);
    const absolutePath = existsSync(primaryTarget.absolutePath)
      ? primaryTarget.absolutePath
      : legacyTarget.absolutePath;

    if (!existsSync(absolutePath)) {
      throw new ResourceManagerError('리소스를 찾을 수 없습니다.', 404);
    }

    const stats = await fs.stat(absolutePath);

    if (!stats.isFile()) {
      throw new ResourceManagerError('파일만 미리보기할 수 있습니다.');
    }

    return {
      contentType: resolveStaticContentType(absolutePath),
      size: stats.size,
      createStream: (range?: { start?: number; end?: number }) => createReadStream(absolutePath, range),
    } satisfies ResourceManagerPreviewStream;
  });
}
