import fs from "fs";
import path from "path";
import { config } from "../config/env";
import { localDateStr, localTimeStr } from "../utils/date";

// file-type v22 is ESM-only. The server compiles to CommonJS, where a plain
// `require("file-type")` fails (you can't require an ESM-only package) and tsc
// would down-level a normal dynamic import() back to require(). The Function(...)
// indirection keeps it a genuine runtime ESM import — the same trick server.ts
// uses to load Vite from the CJS bundle. Cached so we import the module once.
type FileTypeModule = {
  fileTypeFromBuffer: (
    buffer: Uint8Array,
  ) => Promise<{ ext: string; mime: string } | undefined>;
};
let fileTypeModulePromise: Promise<FileTypeModule> | undefined;
function loadFileType(): Promise<FileTypeModule> {
  if (!fileTypeModulePromise) {
    fileTypeModulePromise = Function(
      'return import("file-type")',
    )() as Promise<FileTypeModule>;
  }
  return fileTypeModulePromise;
}

const BLOCKED_EXTENSIONS = new Set([
  "exe",
  "bat",
  "sh",
  "php",
  "htaccess",
  "env",
  "cmd",
  "com",
  "msi",
  "ps1",
  "vbs",
  "vbe",
  "js",
  "ws",
  "wsf",
  "scr",
  "pif",
  "jar",
  "reg",
]);

const SUSPICIOUS_MIMES = [
  "application/x-executable",
  "application/x-msdos-program",
  "application/x-dosexec",
  "application/x-msdownload",
];

const MIME_MAP: Record<string, string> = {
  pdf: "application/pdf",
  doc: "application/msword",
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  xls: "application/vnd.ms-excel",
  xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  ppt: "application/vnd.ms-powerpoint",
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  zip: "application/zip",
  rar: "application/x-rar-compressed",
  "7z": "application/x-7z-compressed",
  tar: "application/x-tar",
  gz: "application/gzip",
  png: "image/png",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  gif: "image/gif",
  bmp: "image/bmp",
  svg: "image/svg+xml",
  webp: "image/webp",
  ico: "image/x-icon",
  tif: "image/tiff",
  tiff: "image/tiff",
  mp3: "audio/mpeg",
  wav: "audio/wav",
  mp4: "video/mp4",
  avi: "video/x-msvideo",
  mkv: "video/x-matroska",
  mov: "video/quicktime",
  txt: "text/plain",
  csv: "text/csv",
  html: "text/html",
  htm: "text/html",
  xml: "application/xml",
  json: "application/json",
  dwg: "application/acad",
  dxf: "application/dxf",
  step: "application/step",
  stp: "application/step",
  iges: "application/iges",
  igs: "application/iges",
};

interface FileItem {
  name: string;
  type: "file" | "folder";
  modified: string;
  is_symlink: boolean;
  link_target?: string;
  size?: number;
  size_formatted?: string;
  extension?: string;
  item_count?: number;
}

interface ListFilesResult {
  path: string;
  items: FileItem[];
  breadcrumb: string[];
  full_path: string;
}

interface DownloadResult {
  filePath: string;
  fileName: string;
  mime: string;
}

export class NasFileManager {
  private readonly basePath: string;

  constructor(basePath?: string) {
    // basePath is an injection seam for tests; production callers pass nothing
    // and get the configured NAS_PATH.
    this.basePath = path
      .resolve(basePath ?? config.nas.path)
      .replace(/\\/g, "/");
  }

  public isConfigured(): boolean {
    if (!this.basePath) return false;
    try {
      return (
        fs.existsSync(this.basePath) && fs.statSync(this.basePath).isDirectory()
      );
    } catch (err) {
      console.error(
        "[nas-file-manager] stat failed for basePath:",
        this.basePath,
        err,
      );
      return false;
    }
  }

  public createProjectFolder(
    projectNumber: string,
    projectName: string,
  ): boolean {
    if (!this.isConfigured()) return false;

    const folderName = this.buildFolderName(projectNumber, projectName);
    const fullPath = this.basePath + "/" + folderName;

    if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
      return true;
    }

    try {
      fs.mkdirSync(fullPath, { recursive: true, mode: 0o775 });
      return true;
    } catch (err) {
      console.error("[nas-file-manager] mkdir failed for path:", fullPath, err);
      return false;
    }
  }

  public async deleteProjectFolder(projectNumber: string): Promise<boolean> {
    if (!this.isConfigured()) return false;

    const folderPath = this.findProjectFolder(projectNumber);
    if (folderPath === null) return true;

    try {
      await fs.promises.rm(folderPath, { recursive: true, force: true });
      return true;
    } catch (err) {
      console.error(
        "[nas-file-manager] delete failed for path:",
        folderPath,
        err,
      );
      return false;
    }
  }

  public projectFolderExists(projectNumber: string): boolean {
    return this.findProjectFolder(projectNumber) !== null;
  }

  public renameProjectFolder(projectNumber: string, newName: string): boolean {
    if (!this.isConfigured()) return false;

    const currentPath = this.findProjectFolder(projectNumber);
    if (currentPath === null) return false;

    const newFolderName = this.buildFolderName(projectNumber, newName);
    const newPath = this.basePath + "/" + newFolderName;

    if (currentPath === newPath) return true;

    try {
      fs.renameSync(currentPath, newPath);
      return true;
    } catch (err) {
      console.error(
        "[nas-file-manager] rename failed:",
        currentPath,
        "→",
        newPath,
        err,
      );
      return false;
    }
  }

  public listFiles(
    projectNumber: string,
    subPath: string = "",
  ): ListFilesResult | null {
    const dirPath = this.resolveProjectPath(projectNumber, subPath);
    if (dirPath === null) return null;

    try {
      const stat = fs.lstatSync(dirPath);
      if (!stat.isDirectory()) return null;
    } catch (err) {
      console.error("[nas-file-manager] lstat failed for path:", dirPath, err);
      return null;
    }

    let entries: string[];
    try {
      entries = fs.readdirSync(dirPath);
    } catch (err) {
      console.error(
        "[nas-file-manager] readdir failed for path:",
        dirPath,
        err,
      );
      return null;
    }

    const items: FileItem[] = [];
    for (const entry of entries) {
      const fullPath = dirPath + "/" + entry;

      let lstat: fs.Stats;
      try {
        lstat = fs.lstatSync(fullPath);
      } catch (err) {
        console.error(
          "[nas-file-manager] lstat failed for entry:",
          fullPath,
          err,
        );
        continue;
      }

      const isLink = lstat.isSymbolicLink();
      // For symlinks, we need to check if target is dir
      let isDir: boolean;
      if (isLink) {
        try {
          isDir = fs.statSync(fullPath).isDirectory();
        } catch (err) {
          console.error(
            "[nas-file-manager] stat failed for symlink target:",
            fullPath,
            err,
          );
          isDir = false;
        }
      } else {
        isDir = lstat.isDirectory();
      }

      const modified = lstat.mtime;
      const modifiedStr = `${localDateStr(modified)} ${localTimeStr(modified)}`;

      const item: FileItem = {
        name: entry,
        type: isDir ? "folder" : "file",
        modified: modifiedStr,
        is_symlink: isLink,
      };

      if (isLink) {
        try {
          const target = fs.readlinkSync(fullPath);
          item.link_target = target;
        } catch (err) {
          console.error(
            "[nas-file-manager] readlink failed for:",
            fullPath,
            err,
          );
        }
      }

      if (isDir) {
        item.item_count = this.countItems(fullPath);
      } else {
        const size = lstat.size;
        item.size = size;
        item.size_formatted = this.formatFileSize(size);
        item.extension = path.extname(entry).slice(1).toLowerCase();
      }

      items.push(item);
    }

    // Sort: folders first, then files, both alphabetically (natural sort)
    items.sort((a, b) => {
      if (a.type !== b.type) {
        return a.type === "folder" ? -1 : 1;
      }
      return a.name.localeCompare(b.name, undefined, {
        numeric: true,
        sensitivity: "base",
      });
    });

    const breadcrumb: string[] = [""];
    if (subPath !== "") {
      const parts = subPath
        .replace(/\\/g, "/")
        .replace(/^\/+|\/+$/g, "")
        .split("/");
      for (const part of parts) {
        breadcrumb.push(part);
      }
    }

    let realDirPath: string;
    try {
      realDirPath = fs.realpathSync(dirPath);
    } catch (err) {
      console.error(
        "[nas-file-manager] realpath failed for path:",
        dirPath,
        err,
      );
      realDirPath = dirPath;
    }

    return {
      path: subPath,
      items,
      breadcrumb,
      full_path: realDirPath,
    };
  }

  public async uploadFile(
    projectNumber: string,
    subPath: string,
    fileBuffer: Buffer,
    fileName: string,
  ): Promise<string | null> {
    const dirPath = this.resolveProjectPath(projectNumber, subPath);
    if (dirPath === null) {
      return "Cílová složka neexistuje";
    }

    try {
      await fs.promises.mkdir(dirPath, { recursive: true, mode: 0o775 });
    } catch (err) {
      console.error("[nas-file-manager] mkdir failed for path:", dirPath, err);
      return "Cílová složka neexistuje";
    }

    let stat: fs.Stats;
    try {
      stat = await fs.promises.stat(dirPath);
    } catch (err) {
      console.error("[nas-file-manager] stat failed for path:", dirPath, err);
      return "Cílová složka neexistuje";
    }
    if (!stat.isDirectory()) {
      return "Cílová složka neexistuje";
    }

    const originalName = path.basename(fileName);
    let safeName = this.sanitizeFilename(originalName);
    if (safeName === "") {
      return "Neplatný název souboru";
    }

    const ext = path.extname(safeName).slice(1).toLowerCase();
    if (BLOCKED_EXTENSIONS.has(ext)) {
      return "Tento typ souboru není povolen";
    }

    // MIME validation (file-type v22: named `fileTypeFromBuffer` export)
    const { fileTypeFromBuffer } = await loadFileType();
    const typeResult = await fileTypeFromBuffer(fileBuffer);
    if (typeResult) {
      if (this.isSuspiciousMime(typeResult.mime)) {
        return "Obsah souboru neodpovídá jeho příponě";
      }
      const expectedMime = ext ? MIME_MAP[ext] : null;
      if (expectedMime && typeResult.mime !== expectedMime) {
        return "Obsah souboru neodpovídá jeho příponě";
      }
    }

    let destPath = dirPath + "/" + safeName;

    // If destination exists, append counter
    let attempts = 0;
    const maxAttempts = 1000;
    while (attempts < maxAttempts) {
      try {
        await fs.promises.writeFile(destPath, fileBuffer, { flag: "wx" });
        return null;
      } catch (err) {
        const e = err as NodeJS.ErrnoException;
        if (e.code === "EEXIST") {
          const base = path.basename(safeName, ext ? "." + ext : "");
          attempts++;
          safeName = base + "_" + attempts + (ext ? "." + ext : "");
          destPath = dirPath + "/" + safeName;
        } else {
          return `Nepodařilo se uložit soubor (${e?.message || String(e)})`;
        }
      }
    }

    return "Nepodařilo se uložit soubor";
  }

  public downloadFile(
    projectNumber: string,
    filePath: string,
  ): DownloadResult | null {
    const fullPath = this.resolveProjectPath(projectNumber, filePath);
    if (fullPath === null) return null;

    try {
      const stat = fs.lstatSync(fullPath);
      if (!stat.isFile()) return null;
    } catch (err) {
      console.error("[nas-file-manager] lstat failed for path:", fullPath, err);
      return null;
    }

    const fileName = path.basename(fullPath);
    const ext = path.extname(fileName).slice(1).toLowerCase();
    const mime = MIME_MAP[ext] || "application/octet-stream";

    return {
      filePath: fullPath,
      fileName,
      mime,
    };
  }

  public async deleteItem(
    projectNumber: string,
    filePath: string,
  ): Promise<string | null> {
    if (
      filePath === "" ||
      filePath === "/" ||
      filePath === "." ||
      filePath === "./"
    ) {
      return "Nelze smazat kořenovou složku projektu";
    }

    const fullPath = this.resolveProjectPath(projectNumber, filePath);
    if (fullPath === null) {
      return "Neplatná cesta";
    }

    let isDir: boolean;
    try {
      const stat = await fs.promises.lstat(fullPath);
      isDir = stat.isDirectory();
    } catch (err) {
      console.error("[nas-file-manager] lstat failed for path:", fullPath, err);
      return "Soubor nebo složka neexistuje";
    }

    try {
      if (isDir) {
        await fs.promises.rm(fullPath, { recursive: true, force: true });
      } else {
        await fs.promises.unlink(fullPath);
      }
    } catch (err) {
      console.error(
        "[nas-file-manager] delete failed for path:",
        fullPath,
        err,
      );
      return isDir
        ? "Nepodařilo se smazat složku"
        : "Nepodařilo se smazat soubor";
    }

    return null;
  }

  public async moveItem(
    projectNumber: string,
    fromPath: string,
    toPath: string,
  ): Promise<string | null> {
    if (
      fromPath === "" ||
      fromPath === "/" ||
      fromPath === "." ||
      fromPath === "./"
    ) {
      return "Nelze přesunout kořenovou složku";
    }

    const fullFrom = this.resolveProjectPath(projectNumber, fromPath);
    const fullTo = this.resolveProjectPath(projectNumber, toPath);

    if (fullFrom === null || fullTo === null) {
      return "Neplatná cesta";
    }

    try {
      await fs.promises.stat(fullFrom);
    } catch (err) {
      console.error(
        "[nas-file-manager] stat failed for source path:",
        fullFrom,
        err,
      );
      return "Zdrojový soubor neexistuje";
    }

    // Case-insensitive FS (Windows) — allow case-only rename
    const sameFile =
      fullFrom.replace(/\\/g, "/").toLowerCase() ===
      fullTo.replace(/\\/g, "/").toLowerCase();

    if (!sameFile) {
      try {
        await fs.promises.stat(fullTo);
        return "Cílový soubor již existuje";
      } catch {
        // target does not exist, continue
      }
    }

    const targetName = path.basename(toPath);
    if (this.sanitizeFilename(targetName) !== targetName) {
      return "Neplatný cílový název";
    }

    try {
      await fs.promises.rename(fullFrom, fullTo);
    } catch (err: unknown) {
      if (
        err instanceof Error &&
        "code" in err &&
        (err as NodeJS.ErrnoException).code === "EXDEV"
      ) {
        return "Přesun mezi různými disky není podporován";
      }
      return "Nepodařilo se přesunout soubor";
    }

    return null;
  }

  public async createFolder(
    projectNumber: string,
    subPath: string,
    folderName: string,
  ): Promise<string | null> {
    const dirPath = this.resolveProjectPath(projectNumber, subPath);
    if (dirPath === null) {
      return "Nadřazená složka neexistuje";
    }

    try {
      const stat = await fs.promises.lstat(dirPath);
      if (stat.isSymbolicLink() || !stat.isDirectory()) {
        return "Nadřazená složka neexistuje";
      }
    } catch (err) {
      console.error("[nas-file-manager] lstat failed for path:", dirPath, err);
      return "Nadřazená složka neexistuje";
    }

    const safeName = this.sanitizeFilename(folderName);
    if (safeName === "") {
      return "Neplatný název složky";
    }

    const newPath = dirPath + "/" + safeName;
    try {
      await fs.promises.stat(newPath);
      return "Složka s tímto názvem již existuje";
    } catch {
      // does not exist, continue
    }

    try {
      await fs.promises.mkdir(newPath, { mode: 0o775 });
    } catch (err) {
      console.error("[nas-file-manager] mkdir failed for path:", newPath, err);
      return "Nepodařilo se vytvořit složku";
    }

    return null;
  }

  public sanitizeFilename(name: string): string {
    let safe = path.basename(name);
    safe = safe.replace(/[\x00-\x1f\x7f<>:"/\\|?*]/g, "");
    safe = safe.replace(/^[. ]+|[. ]+$/g, "");

    if ([...safe].length > 255) {
      const ext = path.extname(safe).slice(1);
      const base = path.basename(safe, ext ? "." + ext : "");
      const maxBase = 250 - [...ext].length;
      const trimmedBase = [...base].slice(0, maxBase).join("");
      safe = ext ? trimmedBase + "." + ext : trimmedBase;
    }

    return safe;
  }

  // --- Private helpers ---

  private findProjectFolder(projectNumber: string): string | null {
    if (!this.isConfigured()) return null;

    let entries: string[];
    try {
      entries = fs.readdirSync(this.basePath);
    } catch (err) {
      console.error(
        "[nas-file-manager] readdir failed for basePath:",
        this.basePath,
        err,
      );
      return null;
    }

    const prefix = projectNumber + "_";
    for (const entry of entries) {
      if (entry.startsWith(prefix)) {
        const fullPath = this.basePath + "/" + entry;
        try {
          if (fs.statSync(fullPath).isDirectory()) {
            return fullPath;
          }
        } catch (err) {
          console.error(
            "[nas-file-manager] stat failed for entry:",
            fullPath,
            err,
          );
          continue;
        }
      }
    }

    return null;
  }

  private buildFolderName(projectNumber: string, projectName: string): string {
    let safeNum = projectNumber.replace(/[^\p{L}\p{N}_\-.]/gu, "");
    safeNum = safeNum.replace(/^\.+|\.+$/g, "").trim();
    let safe = projectName.replace(/[^\p{L}\p{N}_\-. ]/gu, "");
    safe = safe.trim().replace(/ /g, "_").replace(/_+/g, "_");
    if ([...safe].length > 200) {
      safe = [...safe].slice(0, 200).join("");
    }
    return safeNum + "_" + safe;
  }

  private resolveProjectPath(
    projectNumber: string,
    subPath: string,
  ): string | null {
    const folderPath = this.findProjectFolder(projectNumber);
    if (folderPath === null) return null;

    if (subPath === "" || subPath === "/") {
      return folderPath;
    }

    // Basic path traversal protection.
    // NOTE (audit finding, accepted): ".." is matched as a LITERAL substring,
    // which would also reject a legitimate filename containing ".." — that's an
    // acceptable conservative trade-off and the real traversal is contained by
    // the `candidate.startsWith(normalFolder + "/")` prefix check below plus the
    // symlink walk. There is also an inherent TOCTOU window between this
    // validation and the later fs op on the returned path: a symlink swapped
    // into a path component AFTER walkAndRejectSymlinks runs could in theory
    // bypass the guard. Risk is low on a trusted single-tenant NAS share;
    // closing it fully would require opening with O_NOFOLLOW per component
    // (not portable on the SMB mount) — deliberately NOT redesigned here.
    if (subPath.includes("\0") || subPath.includes("..")) {
      return null;
    }

    const normalized = subPath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");

    // Reject explicit current-directory references (defense-in-depth for destructive ops)
    if (normalized === "." || normalized === "./") {
      return null;
    }
    const candidate = path.resolve(folderPath, normalized).replace(/\\/g, "/");

    // Verify candidate is within project folder
    const normalFolder = folderPath.replace(/\\/g, "/");
    if (
      !candidate.startsWith(normalFolder + "/") &&
      candidate !== normalFolder
    ) {
      return null;
    }

    // Check for symlinks in path components
    if (fs.existsSync(candidate)) {
      if (!this.walkAndRejectSymlinks(candidate, normalFolder)) {
        return null;
      }
    } else {
      // For new files/folders — check parent
      const parentDir = path.dirname(candidate);
      if (fs.existsSync(parentDir)) {
        if (!this.walkAndRejectSymlinks(parentDir, normalFolder)) {
          return null;
        }
      }
    }

    return candidate;
  }

  private walkAndRejectSymlinks(fullPath: string, basePath: string): boolean {
    const normalFull = fullPath.replace(/\\/g, "/");
    const normalBase = basePath.replace(/\\/g, "/");

    if (!normalFull.startsWith(normalBase)) {
      return false;
    }

    const relative = normalFull.slice(normalBase.length);
    if (!relative) return true; // same as base

    const parts = relative.split("/").filter(Boolean);
    let current = normalBase;

    for (const part of parts) {
      current = current + "/" + part;
      try {
        const lstat = fs.lstatSync(current);
        if (lstat.isSymbolicLink()) {
          return false;
        }
      } catch {
        // Path component doesn't exist yet, that's OK
        break;
      }
    }

    return true;
  }

  private countItems(dirPath: string): number {
    try {
      const entries = fs.readdirSync(dirPath);
      return entries.length;
    } catch (err) {
      console.error(
        "[nas-file-manager] readdir failed for path:",
        dirPath,
        err,
      );
      return 0;
    }
  }

  private formatFileSize(bytes: number): string {
    if (bytes < 1024) {
      return bytes + " B";
    }
    if (bytes < 1048576) {
      return Math.round((bytes / 1024) * 10) / 10 + " KB";
    }
    if (bytes < 1073741824) {
      return Math.round((bytes / 1048576) * 10) / 10 + " MB";
    }
    return Math.round((bytes / 1073741824) * 10) / 10 + " GB";
  }

  private isSuspiciousMime(mime: string): boolean {
    if (SUSPICIOUS_MIMES.includes(mime)) {
      return true;
    }

    // PHP files
    if (mime.includes("php") || mime.includes("x-httpd")) {
      return true;
    }

    return false;
  }
}
