import fs from "fs";
import path from "path";
import { config } from "../config/env";

/**
 * NAS storage for financial/order documents (issued invoices, received
 * invoices, issued purchase orders — any document archived under the standard
 * Vydané/Přijaté layout). One instance is bound to a single base path, so a
 * separate singleton is exported per document family (invoices vs orders).
 * Structure: {basePath}/Vydané/YYYY/MM/ and {basePath}/Přijaté/YYYY/MM/
 *
 * PERFORMANCE NOTE (audit finding — DEFERRED, intentional):
 * Every method here uses SYNCHRONOUS fs calls (mkdirSync/writeFileSync/
 * readFileSync/readdirSync/statSync). When basePath is a network (SMB) share, a
 * slow or hung NAS will BLOCK the Node event loop for the duration of the call,
 * stalling all other requests in the same process. This is a known trade-off:
 * the file sizes are small (PDFs), the call sites are low-frequency admin
 * operations, and the synchronous API lets callers treat a returned path as a
 * verified, durable write (see migrate-received-invoices-to-nas.ts, which
 * relies on this to safely null out the DB BLOB). Converting to the async
 * fs.promises API is a large, cross-cutting change touching every caller and
 * their error handling — deliberately NOT done here to keep this fix scoped.
 */

const DIR_ISSUED = "Vydané";
const DIR_RECEIVED = "Přijaté";

export class NasDocumentManager {
  private readonly basePath: string;

  constructor(basePath: string) {
    this.basePath = basePath ? path.resolve(basePath).replace(/\\/g, "/") : "";
  }

  isConfigured(): boolean {
    if (!this.basePath) return false;
    try {
      // Intentional side effect: ensure the base directory exists as part of the
      // configured-check so the first save doesn't fail on a missing root. This
      // mirrors nas-offers-manager.isConfigured() (established convention).
      fs.mkdirSync(this.basePath, { recursive: true });
      return fs.statSync(this.basePath).isDirectory();
    } catch (err) {
      console.error(
        "[nas-financials-manager] mkdir/stat failed for basePath:",
        this.basePath,
        err,
      );
      return false;
    }
  }

  // ── Created (issued) documents ───────────────────────────────────

  /**
   * Remove any existing PDF for this document number across all year/month
   * folders — including stale `<number>_N.pdf` duplicates that the old
   * uniquePath-based save left behind when two archive calls raced (the
   * save-after-edit fire-and-forget vs. the /file fallback). Document
   * numbers are digit-only and never contain "_", so the suffix match
   * cannot hit a different document.
   */
  cleanIssued(documentNumber: string): void {
    if (!this.basePath) return;
    const safeBase = this.sanitizeFilename(documentNumber);
    const stalePattern = new RegExp(
      `^${safeBase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(_\\d+)?\\.pdf$`,
    );
    const issuedDir = path.join(this.basePath, DIR_ISSUED);
    try {
      if (!fs.existsSync(issuedDir)) return;
      for (const yearDir of fs.readdirSync(issuedDir)) {
        const yearPath = path.join(issuedDir, yearDir);
        if (!fs.statSync(yearPath).isDirectory()) continue;
        for (const monthDir of fs.readdirSync(yearPath)) {
          const monthPath = path.join(yearPath, monthDir);
          try {
            if (!fs.statSync(monthPath).isDirectory()) continue;
          } catch (err) {
            console.error(
              "[nas-financials-manager] stat failed for monthPath:",
              monthPath,
              err,
            );
            continue;
          }
          for (const file of fs.readdirSync(monthPath)) {
            if (stalePattern.test(file)) {
              fs.unlinkSync(path.join(monthPath, file));
            }
          }
        }
      }
    } catch (err) {
      console.error(
        "[nas-financials-manager] cleanIssued failed for:",
        documentNumber,
        err,
      );
    }
  }

  saveIssuedPdf(
    documentNumber: string,
    year: number,
    month: number,
    pdfBuffer: Buffer,
  ): string | null {
    // Guard parity with saveReceived: without this, an unconfigured
    // (empty) basePath would let ensureDir build a RELATIVE path and write the
    // PDF into the process CWD instead of the NAS. Bail out instead.
    if (!this.isConfigured()) return null;

    const dir = this.ensureDir(DIR_ISSUED, year, month);
    if (!dir) return null;

    // Deterministic path, OVERWRITE in place (same model as the offers
    // manager): the archive of a numbered document has exactly one canonical
    // file. uniquePath here used to spawn `<number>_1.pdf` duplicates
    // whenever two archive calls raced — those are user-visible junk on the
    // NAS and invisible to the reader/cleaner, which match the exact name.
    const safeName = this.sanitizeFilename(documentNumber) + ".pdf";
    const fullPath = path.join(dir, safeName);

    try {
      fs.writeFileSync(fullPath, pdfBuffer);
      return `${DIR_ISSUED}/${year}/${this.pad(month)}/${safeName}`;
    } catch (err) {
      console.error(
        "[nas-financials-manager] write failed for issued document:",
        fullPath,
        err,
      );
      return null;
    }
  }

  /**
   * Locate and read the archived PDF for an issued document by its number
   * alone, sweeping every Vydané/YYYY/MM folder (the same walk cleanIssued
   * uses). Needed because the archive folder is derived from the document
   * date at save time and the date can be edited afterwards — a fixed
   * expected-path read would miss a file that does exist in another month
   * folder. Returns null when not found (callers fall back to a live render).
   */
  readIssuedByNumber(
    documentNumber: string,
  ): { data: Buffer; fileName: string } | null {
    if (!this.basePath) return null;
    const safeName = this.sanitizeFilename(documentNumber) + ".pdf";
    const issuedDir = path.join(this.basePath, DIR_ISSUED);
    try {
      if (!fs.existsSync(issuedDir)) return null;
      for (const yearDir of fs.readdirSync(issuedDir)) {
        const yearPath = path.join(issuedDir, yearDir);
        if (!fs.statSync(yearPath).isDirectory()) continue;
        for (const monthDir of fs.readdirSync(yearPath)) {
          const monthPath = path.join(yearPath, monthDir);
          try {
            if (!fs.statSync(monthPath).isDirectory()) continue;
          } catch (err) {
            console.error(
              "[nas-financials-manager] stat failed for monthPath:",
              monthPath,
              err,
            );
            continue;
          }
          const filePath = path.join(monthPath, safeName);
          if (fs.existsSync(filePath)) {
            return { data: fs.readFileSync(filePath), fileName: safeName };
          }
        }
      }
      return null;
    } catch (err) {
      console.error(
        "[nas-financials-manager] readIssuedByNumber failed for:",
        documentNumber,
        err,
      );
      return null;
    }
  }

  readIssued(relativePath: string): { data: Buffer; fileName: string } | null {
    const fullPath = this.resolveSafePath(relativePath);
    if (!fullPath) return null;
    try {
      const data = fs.readFileSync(fullPath);
      return { data, fileName: path.basename(fullPath) };
    } catch (err) {
      console.error(
        "[nas-financials-manager] read failed for issued document:",
        fullPath,
        err,
      );
      return null;
    }
  }

  deleteIssued(relativePath: string): boolean {
    const fullPath = this.resolveSafePath(relativePath);
    if (!fullPath) return false;
    try {
      fs.unlinkSync(fullPath);
      return true;
    } catch (err) {
      console.error(
        "[nas-financials-manager] delete failed for issued document:",
        fullPath,
        err,
      );
      return false;
    }
  }

  // ── Received documents ───────────────────────────────────────────

  buildReceivedPath(fileName: string, year: number, month: number): string {
    const safeName = this.sanitizeFilename(fileName) || "file";
    return `${DIR_RECEIVED}/${year}/${this.pad(month)}/${safeName}`;
  }

  saveReceived(
    originalName: string,
    year: number,
    month: number,
    fileBuffer: Buffer,
  ): { filePath: string } | { error: string } {
    if (!this.isConfigured()) {
      return { error: "NAS path is not configured" };
    }

    const dir = this.ensureDir(DIR_RECEIVED, year, month);
    if (!dir) return { error: "Nelze vytvořit adresář na NAS" };

    let safeName = this.sanitizeFilename(originalName);
    if (!safeName) safeName = "file";

    const fullPath = this.uniquePath(dir, safeName);

    try {
      fs.writeFileSync(fullPath, fileBuffer);
      return {
        filePath: `${DIR_RECEIVED}/${year}/${this.pad(month)}/${path.basename(fullPath)}`,
      };
    } catch (e) {
      return {
        error: `Nelze uložit soubor na NAS: ${e instanceof Error ? e.message : String(e)}`,
      };
    }
  }

  readReceived(filePath: string): { data: Buffer; fileName: string } | null {
    const fullPath = this.resolveSafePath(filePath);
    if (!fullPath) return null;

    try {
      const data = fs.readFileSync(fullPath);
      return { data, fileName: path.basename(fullPath) };
    } catch (err) {
      console.error(
        "[nas-financials-manager] read failed for received document:",
        fullPath,
        err,
      );
      return null;
    }
  }

  deleteReceived(filePath: string): boolean {
    const fullPath = this.resolveSafePath(filePath);
    if (!fullPath) return false;

    try {
      fs.unlinkSync(fullPath);
      return true;
    } catch (err) {
      console.error(
        "[nas-financials-manager] delete failed for received document:",
        fullPath,
        err,
      );
      return false;
    }
  }

  // ── Private helpers ──────────────────────────────────────────────

  private ensureDir(
    category: string,
    year: number,
    month: number,
  ): string | null {
    const dirPath = path.join(
      this.basePath,
      category,
      String(year),
      this.pad(month),
    );
    try {
      fs.mkdirSync(dirPath, { recursive: true });
      return dirPath;
    } catch (err) {
      console.error(
        "[nas-financials-manager] mkdir failed for path:",
        dirPath,
        err,
      );
      return null;
    }
  }

  private pad(n: number): string {
    return String(n).padStart(2, "0");
  }

  private 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;
      safe = ext
        ? [...base].slice(0, maxBase).join("") + "." + ext
        : [...base].slice(0, maxBase).join("");
    }
    return safe;
  }

  private uniquePath(dir: string, fileName: string): string {
    let fullPath = path.join(dir, fileName);
    if (!fs.existsSync(fullPath)) return fullPath;

    const ext = path.extname(fileName);
    const base = path.basename(fileName, ext);
    let counter = 1;
    while (fs.existsSync(fullPath)) {
      fullPath = path.join(dir, `${base}_${counter}${ext}`);
      counter++;
    }
    return fullPath;
  }

  private resolveSafePath(relativePath: string): string | null {
    if (!this.basePath) return null;
    if (relativePath.includes("\0") || relativePath.includes("..")) {
      return null;
    }

    const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
    const candidate = path
      .resolve(this.basePath, normalized)
      .replace(/\\/g, "/");
    const normalBase = this.basePath.replace(/\\/g, "/");

    if (!candidate.startsWith(normalBase + "/")) {
      return null;
    }

    return candidate;
  }
}

export const nasInvoicesManager = new NasDocumentManager(
  config.nas.invoicesPath,
);
export const nasOrdersManager = new NasDocumentManager(config.nas.ordersPath);
