import "server-only";

import { analyseImport, extractImportRows, importRowSchema, type ImportPreview, type ImportRowInput } from "@/lib/import/rows";
import { PROJECT_DEFAULTS } from "@/lib/validation/project";
import { db } from "../db";
import { logger } from "../logger";
import { parseSpreadsheet, SpreadsheetError } from "./spreadsheet";

export { SpreadsheetError };

export class ImportFileError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ImportFileError";
  }
}

export const MAX_IMPORT_FILE_BYTES = 4 * 1024 * 1024;
export const MAX_IMPORT_ROWS = 2000;

/** Parses the uploaded sheet and compares it with the apps currently on the dashboard. */
export async function previewImport(fileName: string, data: Buffer): Promise<ImportPreview> {
  if (data.length === 0) throw new ImportFileError("The file is empty.");
  if (data.length > MAX_IMPORT_FILE_BYTES) throw new ImportFileError("The file is larger than 4 MB.");

  const grid = parseSpreadsheet(fileName, data); // throws SpreadsheetError
  const extracted = extractImportRows(grid);
  if (!extracted.ok) throw new ImportFileError(extracted.error);
  if (extracted.rows.length > MAX_IMPORT_ROWS) {
    throw new ImportFileError(`The sheet has ${extracted.rows.length} rows; at most ${MAX_IMPORT_ROWS} can be imported at once.`);
  }

  const existing = await db.project.findMany({
    where: { deletedAt: null },
    select: { id: true, projectCode: true, projectName: true, productionUrl: true },
  });
  return analyseImport(extracted.rows, existing);
}

export type ImportOutcome = {
  created: number;
  updated: number;
  unchanged: number;
  failed: Array<{ projectCode: string; error: string }>;
  /** Apps that should be health-checked now: created, or re-targeted at a new URL. */
  checkIds: string[];
};

/**
 * Applies the rows the user ticked: an app whose Project Code already exists
 * (case-insensitively) gets its name and URL updated, anything else is created
 * with the default monitoring settings. Rows are re-validated and de-duplicated
 * here — the preview the browser sent back is never trusted. Each row is its
 * own unit of work so one bad row cannot roll back the rest.
 */
export async function importProjects(rows: unknown[], by: string): Promise<ImportOutcome> {
  const outcome: ImportOutcome = { created: 0, updated: 0, unchanged: 0, failed: [], checkIds: [] };
  const seen = new Set<string>();

  for (const candidate of rows) {
    const parsed = importRowSchema.safeParse(candidate);
    if (!parsed.success) {
      const code = typeof (candidate as { projectCode?: unknown })?.projectCode === "string" ? (candidate as { projectCode: string }).projectCode : "?";
      outcome.failed.push({ projectCode: code, error: parsed.error.issues.map((i) => i.message).join("; ") });
      continue;
    }
    const row = parsed.data;
    const key = row.projectCode.toLowerCase();
    if (seen.has(key)) continue; // duplicate within the submission: first one wins
    seen.add(key);

    try {
      const result = await upsertProject(row);
      outcome[result.action]++;
      if (result.needsCheck) outcome.checkIds.push(result.id);
    } catch (error) {
      logger.error("import.row.failed", { projectCode: row.projectCode, error });
      outcome.failed.push({ projectCode: row.projectCode, error: "Could not be saved" });
    }
  }

  logger.info("import.completed", {
    created: outcome.created,
    updated: outcome.updated,
    unchanged: outcome.unchanged,
    failed: outcome.failed.length,
    by,
  });
  return outcome;
}

type UpsertResult = { id: string; action: "created" | "updated" | "unchanged"; needsCheck: boolean };

async function upsertProject(row: ImportRowInput): Promise<UpsertResult> {
  const existing = await db.project.findFirst({
    where: { deletedAt: null, projectCode: { equals: row.projectCode, mode: "insensitive" } },
    select: { id: true, projectName: true, productionUrl: true, monitoringEnabled: true },
  });

  if (existing) {
    const urlChanged = existing.productionUrl !== row.productionUrl;
    if (!urlChanged && existing.projectName === row.projectName) {
      return { id: existing.id, action: "unchanged", needsCheck: false };
    }
    // The stored code keeps its original casing; only name and URL come from the sheet.
    await db.project.update({
      where: { id: existing.id },
      data: { projectName: row.projectName, productionUrl: row.productionUrl },
    });
    return { id: existing.id, action: "updated", needsCheck: urlChanged && existing.monitoringEnabled };
  }

  const created = await db.project.create({
    data: {
      projectCode: row.projectCode,
      projectName: row.projectName,
      productionUrl: row.productionUrl,
      monitoringEnabled: true,
      expectedHttpStatus: PROJECT_DEFAULTS.expectedHttpStatus,
      timeoutSeconds: PROJECT_DEFAULTS.timeoutSeconds,
    },
    select: { id: true },
  });
  return { id: created.id, action: "created", needsCheck: true };
}
