import { logError, logInfo } from "@/lib/logger";
import type { TDashboardStorage } from "@/lib/storage/dashboard-storage";
import { buildVersionPrefix } from "@/lib/storage/dashboard-storage";
import type {
  ProcessUploadInput,
  ProcessUploadResult,
} from "./dashboard-bundle.types";
import { validateDashboardBundle } from "./dashboard-bundle.validator";

type TDashboardUploadServiceDeps = {
  DashboardStorage: TDashboardStorage;
};

/**
 * Storage-only half of a version upload: validate the zip against the Bundle
 * Contract, then write it to GCS under a fresh version prefix — or write
 * nothing at all.
 *
 * Deliberately writes no DB rows. The controller (prompt 3) owns the
 * transaction that inserts dashboard_versions and flips
 * dashboards.current_version_id, so the whole DB story stays in one place and
 * this service stays testable without a database.
 */
export const DashboardUploadServices = ({
  DashboardStorage,
}: TDashboardUploadServiceDeps) => {
  const ProcessUpload = async ({
    dashboardId,
    versionId,
    zipBuffer,
    uploadedBy,
  }: ProcessUploadInput): Promise<ProcessUploadResult> => {
    // (a) Validate first. Nothing reaches storage until the whole bundle is
    //     known-good.
    const validation = await validateDashboardBundle(zipBuffer);
    if (!validation.ok) {
      return { ok: false, errors: validation.errors };
    }

    // (b) Every version gets its own UUID-keyed prefix, so an upload can never
    //     overwrite a published version — or a concurrent upload — in place.
    const storagePrefix = buildVersionPrefix(dashboardId, versionId);

    // (c) Upload, tracking what landed so we can undo it.
    try {
      for (const file of validation.files) {
        await DashboardStorage.putObject(
          storagePrefix,
          file.path,
          file.buffer,
          file.contentType,
        );
      }
    } catch (err) {
      logError(
        err,
        `Dashboard upload failed for ${dashboardId} version ${versionId}; rolling back ${storagePrefix}`,
      );
      // (d) Best-effort cleanup: a half-written prefix must never be left where
      //     a later publish could point at it. A failure to clean up is logged
      //     but must not mask the original error.
      try {
        await DashboardStorage.deleteByPrefix(storagePrefix);
      } catch (cleanupErr) {
        logError(
          cleanupErr,
          `Failed cleaning up partial dashboard upload at ${storagePrefix}`,
        );
      }
      return {
        ok: false,
        errors: [
          `Failed storing the dashboard bundle: ${(err as Error).message}. No files were kept.`,
        ],
      };
    }

    logInfo(
      `Stored dashboard bundle ${storagePrefix} (${validation.fileCount} files, ${validation.totalBytes} bytes) uploaded by ${uploadedBy}`,
    );

    // (e) Hand the facts back; the caller writes the row.
    return {
      ok: true,
      storagePrefix,
      entryPoint: validation.entryPoint,
      htmlFiles: validation.files
        .filter((f) => f.path.toLowerCase().endsWith(".html"))
        .map((f) => f.path),
      manifest: validation.manifest,
      sizeBytes: validation.totalBytes,
      fileCount: validation.fileCount,
      warnings: validation.warnings,
    };
  };

  return { ProcessUpload };
};

export type TDashboardUploadServices = ReturnType<
  typeof DashboardUploadServices
>;
