import "server-only";

import type { DisplayStatus } from "@/lib/status";
import type { ProjectInput } from "@/lib/validation/project";
import type { Prisma } from "@/generated/prisma/client";
import { db } from "../db";

export const PROJECTS_PAGE_SIZE = 25;

export class DuplicateProjectCodeError extends Error {
  constructor(code: string) {
    super(`Project Code "${code}" is already in use`);
    this.name = "DuplicateProjectCodeError";
  }
}

export class ProjectNotFoundError extends Error {
  constructor() {
    super("Project not found");
    this.name = "ProjectNotFoundError";
  }
}

function isUniqueViolation(error: unknown): boolean {
  return typeof error === "object" && error !== null && (error as { code?: string }).code === "P2002";
}

// Every read goes through this so soft-deleted projects never leak out.
const ACTIVE = { deletedAt: null } as const;

async function assertCodeAvailable(projectCode: string, exceptId?: string) {
  const clash = await db.project.findFirst({
    where: {
      ...ACTIVE,
      projectCode: { equals: projectCode, mode: "insensitive" },
      ...(exceptId ? { id: { not: exceptId } } : {}),
    },
    select: { id: true },
  });
  if (clash) throw new DuplicateProjectCodeError(projectCode);
}

// Must mirror displayStatus() in src/lib/status.ts (and STATUS_FILTERS in the dashboard service).
const STATUS_WHERE: Record<DisplayStatus, Prisma.ProjectWhereInput> = {
  WORKING: { monitoringEnabled: true, currentStatus: "WORKING" },
  SLOW: { monitoringEnabled: true, currentStatus: "SLOW" },
  NOT_WORKING: { monitoringEnabled: true, currentStatus: "NOT_WORKING" },
  PENDING: { monitoringEnabled: true, currentStatus: null },
  DISABLED: { monitoringEnabled: false },
};

export async function listProjects({
  search,
  status,
  page,
}: {
  search?: string;
  status?: DisplayStatus | null;
  page: number;
}) {
  const term = search?.trim();
  const where: Prisma.ProjectWhereInput = {
    ...ACTIVE,
    ...(status ? STATUS_WHERE[status] : {}),
    ...(term
      ? {
          OR: [
            { projectCode: { contains: term, mode: "insensitive" } },
            { projectName: { contains: term, mode: "insensitive" } },
          ],
        }
      : {}),
  };

  const [total, projects] = await Promise.all([
    db.project.count({ where }),
    db.project.findMany({
      where,
      orderBy: [{ projectCode: "asc" }, { createdAt: "asc" }],
      skip: (page - 1) * PROJECTS_PAGE_SIZE,
      take: PROJECTS_PAGE_SIZE,
    }),
  ]);

  return { projects, total, pageCount: Math.max(1, Math.ceil(total / PROJECTS_PAGE_SIZE)) };
}

export function getProject(id: string) {
  return db.project.findFirst({ where: { id, ...ACTIVE } });
}

export async function createProject(input: ProjectInput) {
  await assertCodeAvailable(input.projectCode);
  try {
    return await db.project.create({ data: input });
  } catch (error) {
    // The partial unique index is the real guard; this covers a concurrent insert.
    if (isUniqueViolation(error)) throw new DuplicateProjectCodeError(input.projectCode);
    throw error;
  }
}

/**
 * Returns `needsCheck: true` when the edit makes the stored status stale:
 * monitoring was switched on, or what "healthy" means (URL / expected status) changed.
 */
export async function updateProject(id: string, input: ProjectInput): Promise<{ needsCheck: boolean }> {
  const before = await getProject(id);
  if (!before) throw new ProjectNotFoundError();

  await assertCodeAvailable(input.projectCode, id);
  try {
    const { count } = await db.project.updateMany({ where: { id, ...ACTIVE }, data: input });
    if (count === 0) throw new ProjectNotFoundError();
  } catch (error) {
    if (isUniqueViolation(error)) throw new DuplicateProjectCodeError(input.projectCode);
    throw error;
  }

  const targetChanged =
    before.productionUrl !== input.productionUrl || before.expectedHttpStatus !== input.expectedHttpStatus;
  return { needsCheck: input.monitoringEnabled && (!before.monitoringEnabled || targetChanged) };
}

export async function setMonitoringEnabled(id: string, monitoringEnabled: boolean) {
  const { count } = await db.project.updateMany({ where: { id, ...ACTIVE }, data: { monitoringEnabled } });
  if (count === 0) throw new ProjectNotFoundError();
}

/** Soft delete: the row and all of its health-check history are kept. */
export async function softDeleteProject(id: string) {
  const { count } = await db.project.updateMany({ where: { id, ...ACTIVE }, data: { deletedAt: new Date() } });
  if (count === 0) throw new ProjectNotFoundError();
}
