import type { Prisma } from "@capakraken/db";
import { serializeEstimateExport } from "@capakraken/engine";
import {
  EstimateExportFormat,
  EstimateStatus,
  EstimateVersionStatus,
  type ApproveEstimateVersionInput,
  type CreateEstimateExportInput,
  type CreateEstimateRevisionInput,
  type SubmitEstimateVersionInput,
} from "@capakraken/shared";
import {
  ESTIMATE_DETAIL_INCLUDE,
  type EstimateDbClient,
  type EstimateVersionDetails,
  type EstimateWithDetails,
} from "./shared.js";

async function getEstimateOrThrow(
  db: EstimateDbClient,
  estimateId: string,
): Promise<EstimateWithDetails> {
  const estimate = await db.estimate.findUnique({
    where: { id: estimateId },
    include: ESTIMATE_DETAIL_INCLUDE,
  });

  if (!estimate) {
    throw new Error("Estimate not found");
  }

  return estimate;
}

function resolveVersion(
  estimate: EstimateWithDetails,
  versionId: string | undefined,
  matcher: (version: EstimateVersionDetails) => boolean,
  errorMessage: string,
) {
  const version = versionId
    ? estimate.versions.find((candidate) => candidate.id === versionId)
    : estimate.versions.find(matcher);

  if (!version) {
    throw new Error(errorMessage);
  }

  return version;
}

function sanitizeFileNameSegment(value: string) {
  const normalized = value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");

  return normalized.length > 0 ? normalized : "estimate";
}

function buildExportFileName(
  estimateName: string,
  versionNumber: number,
  format: EstimateExportFormat,
) {
  return `${sanitizeFileNameSegment(estimateName)}-v${versionNumber}.${format.toLowerCase()}`;
}

export async function submitEstimateVersion(
  db: EstimateDbClient,
  input: SubmitEstimateVersionInput,
): Promise<EstimateWithDetails> {
  const estimate = await getEstimateOrThrow(db, input.estimateId);
  const targetVersion = resolveVersion(
    estimate,
    input.versionId,
    (version) => version.status === EstimateVersionStatus.WORKING,
    input.versionId
      ? "Estimate version not found"
      : "Estimate has no working version",
  );

  if (targetVersion.status !== EstimateVersionStatus.WORKING) {
    throw new Error("Only working versions can be submitted");
  }

  const supersededIds = estimate.versions
    .filter(
      (version) =>
        version.id !== targetVersion.id &&
        version.status === EstimateVersionStatus.SUBMITTED,
    )
    .map((version) => version.id);

  await db.$transaction(async (tx) => {
    if (supersededIds.length > 0) {
      await tx.estimateVersion.updateMany({
        where: { id: { in: supersededIds } },
        data: { status: EstimateVersionStatus.SUPERSEDED },
      });
    }

    await tx.estimateVersion.update({
      where: { id: targetVersion.id },
      data: {
        status: EstimateVersionStatus.SUBMITTED,
        lockedAt: targetVersion.lockedAt ?? new Date(),
      },
    });

    await tx.estimate.update({
      where: { id: estimate.id },
      data: { status: EstimateStatus.IN_REVIEW },
    });
  });

  return getEstimateOrThrow(db, estimate.id);
}

export async function approveEstimateVersion(
  db: EstimateDbClient,
  input: ApproveEstimateVersionInput,
): Promise<EstimateWithDetails> {
  const estimate = await getEstimateOrThrow(db, input.estimateId);
  const targetVersion = resolveVersion(
    estimate,
    input.versionId,
    (version) => version.status === EstimateVersionStatus.SUBMITTED,
    input.versionId
      ? "Estimate version not found"
      : "Estimate has no submitted version",
  );

  if (targetVersion.status !== EstimateVersionStatus.SUBMITTED) {
    throw new Error("Only submitted versions can be approved");
  }

  const supersededIds = estimate.versions
    .filter(
      (version) =>
        version.id !== targetVersion.id &&
        (version.status === EstimateVersionStatus.SUBMITTED ||
          version.status === EstimateVersionStatus.APPROVED),
    )
    .map((version) => version.id);

  await db.$transaction(async (tx) => {
    if (supersededIds.length > 0) {
      await tx.estimateVersion.updateMany({
        where: { id: { in: supersededIds } },
        data: { status: EstimateVersionStatus.SUPERSEDED },
      });
    }

    await tx.estimateVersion.update({
      where: { id: targetVersion.id },
      data: {
        status: EstimateVersionStatus.APPROVED,
        lockedAt: targetVersion.lockedAt ?? new Date(),
      },
    });

    await tx.estimate.update({
      where: { id: estimate.id },
      data: { status: EstimateStatus.APPROVED },
    });
  });

  return getEstimateOrThrow(db, estimate.id);
}

export async function createEstimateRevision(
  db: EstimateDbClient,
  input: CreateEstimateRevisionInput,
): Promise<EstimateWithDetails> {
  const estimate = await getEstimateOrThrow(db, input.estimateId);
  const existingWorkingVersion = estimate.versions.find(
    (version) => version.status === EstimateVersionStatus.WORKING,
  );

  if (existingWorkingVersion) {
    throw new Error("Estimate already has a working version");
  }

  const sourceVersion = resolveVersion(
    estimate,
    input.sourceVersionId,
    (version) =>
      version.status !== EstimateVersionStatus.WORKING || version.lockedAt != null,
    input.sourceVersionId
      ? "Estimate version not found"
      : "Estimate has no locked version to revise",
  );

  if (
    sourceVersion.status === EstimateVersionStatus.WORKING &&
    sourceVersion.lockedAt == null
  ) {
    throw new Error("Source version must be locked before creating a revision");
  }

  const nextVersionNumber = estimate.latestVersionNumber + 1;

  await db.$transaction(async (tx) => {
    const newVersion = await tx.estimateVersion.create({
      data: {
        estimateId: estimate.id,
        versionNumber: nextVersionNumber,
        label: input.label ?? `Revision ${nextVersionNumber}`,
        status: EstimateVersionStatus.WORKING,
        notes:
          input.notes ?? `Revision created from v${sourceVersion.versionNumber}`,
        projectSnapshot: sourceVersion.projectSnapshot as Prisma.InputJsonValue,
      },
    });

    if (sourceVersion.assumptions.length > 0) {
      await tx.estimateAssumption.createMany({
        data: sourceVersion.assumptions.map((assumption) => ({
          estimateVersionId: newVersion.id,
          category: assumption.category,
          key: assumption.key,
          label: assumption.label,
          valueType: assumption.valueType,
          value: assumption.value as Prisma.InputJsonValue,
          sortOrder: assumption.sortOrder,
          ...(assumption.notes != null ? { notes: assumption.notes } : {}),
        })),
      });
    }

    const scopeItemIdMap = new Map<string, string>();

    for (const scopeItem of sourceVersion.scopeItems) {
      const createdScopeItem = await tx.scopeItem.create({
        data: {
          estimateVersionId: newVersion.id,
          sequenceNo: scopeItem.sequenceNo,
          scopeType: scopeItem.scopeType,
          ...(scopeItem.packageCode != null
            ? { packageCode: scopeItem.packageCode }
            : {}),
          name: scopeItem.name,
          ...(scopeItem.description != null
            ? { description: scopeItem.description }
            : {}),
          ...(scopeItem.scene != null ? { scene: scopeItem.scene } : {}),
          ...(scopeItem.page != null ? { page: scopeItem.page } : {}),
          ...(scopeItem.location != null ? { location: scopeItem.location } : {}),
          ...(scopeItem.assumptionCategory != null
            ? { assumptionCategory: scopeItem.assumptionCategory }
            : {}),
          technicalSpec: scopeItem.technicalSpec as Prisma.InputJsonValue,
          ...(scopeItem.frameCount != null
            ? { frameCount: scopeItem.frameCount }
            : {}),
          ...(scopeItem.itemCount != null ? { itemCount: scopeItem.itemCount } : {}),
          ...(scopeItem.unitMode != null ? { unitMode: scopeItem.unitMode } : {}),
          ...(scopeItem.internalComments != null
            ? { internalComments: scopeItem.internalComments }
            : {}),
          ...(scopeItem.externalComments != null
            ? { externalComments: scopeItem.externalComments }
            : {}),
          sortOrder: scopeItem.sortOrder,
          metadata: scopeItem.metadata as Prisma.InputJsonValue,
        },
      });

      scopeItemIdMap.set(scopeItem.id, createdScopeItem.id);
    }

    if (sourceVersion.demandLines.length > 0) {
      await tx.estimateDemandLine.createMany({
        data: sourceVersion.demandLines.map((line) => ({
          estimateVersionId: newVersion.id,
          scopeItemId:
            line.scopeItemId != null
              ? (scopeItemIdMap.get(line.scopeItemId) ?? null)
              : null,
          roleId: line.roleId ?? null,
          resourceId: line.resourceId ?? null,
          lineType: line.lineType,
          name: line.name,
          chapter: line.chapter ?? null,
          hours: line.hours,
          days: line.days ?? null,
          fte: line.fte ?? null,
          rateSource: line.rateSource ?? null,
          costRateCents: line.costRateCents,
          billRateCents: line.billRateCents,
          currency: line.currency,
          costTotalCents: line.costTotalCents,
          priceTotalCents: line.priceTotalCents,
          monthlySpread: line.monthlySpread as Prisma.InputJsonValue,
          staffingAttributes: line.staffingAttributes as Prisma.InputJsonValue,
          metadata: line.metadata as Prisma.InputJsonValue,
        })),
      });
    }

    if (sourceVersion.resourceSnapshots.length > 0) {
      await tx.resourceCostSnapshot.createMany({
        data: sourceVersion.resourceSnapshots.map((snapshot) => ({
          estimateVersionId: newVersion.id,
          ...(snapshot.resourceId != null ? { resourceId: snapshot.resourceId } : {}),
          ...(snapshot.sourceEid != null ? { sourceEid: snapshot.sourceEid } : {}),
          displayName: snapshot.displayName,
          ...(snapshot.chapter != null ? { chapter: snapshot.chapter } : {}),
          ...(snapshot.roleId != null ? { roleId: snapshot.roleId } : {}),
          currency: snapshot.currency,
          lcrCents: snapshot.lcrCents,
          ucrCents: snapshot.ucrCents,
          ...(snapshot.fte != null ? { fte: snapshot.fte } : {}),
          ...(snapshot.location != null ? { location: snapshot.location } : {}),
          ...(snapshot.country != null ? { country: snapshot.country } : {}),
          ...(snapshot.level != null ? { level: snapshot.level } : {}),
          ...(snapshot.workType != null ? { workType: snapshot.workType } : {}),
          attributes: snapshot.attributes as Prisma.InputJsonValue,
        })),
      });
    }

    if (sourceVersion.metrics.length > 0) {
      await tx.estimateMetric.createMany({
        data: sourceVersion.metrics.map((metric) => ({
          estimateVersionId: newVersion.id,
          key: metric.key,
          label: metric.label,
          ...(metric.metricGroup != null
            ? { metricGroup: metric.metricGroup }
            : {}),
          valueDecimal: metric.valueDecimal,
          ...(metric.valueCents != null ? { valueCents: metric.valueCents } : {}),
          ...(metric.currency != null ? { currency: metric.currency } : {}),
          metadata: metric.metadata as Prisma.InputJsonValue,
        })),
      });
    }

    await tx.estimate.update({
      where: { id: estimate.id },
      data: {
        latestVersionNumber: nextVersionNumber,
        status: EstimateStatus.DRAFT,
      },
    });
  });

  return getEstimateOrThrow(db, estimate.id);
}

export async function createEstimateExport(
  db: EstimateDbClient,
  input: CreateEstimateExportInput,
): Promise<EstimateWithDetails> {
  const estimate = await getEstimateOrThrow(db, input.estimateId);
  const targetVersion = resolveVersion(
    estimate,
    input.versionId,
    () => true,
    input.versionId
      ? "Estimate version not found"
      : "Estimate has no version to export",
  );
  const projectSnapshot =
    typeof targetVersion.projectSnapshot === "object" &&
    targetVersion.projectSnapshot !== null &&
    !Array.isArray(targetVersion.projectSnapshot)
      ? (targetVersion.projectSnapshot as Record<string, unknown>)
      : null;

  const payload = await serializeEstimateExport(
    {
      estimate,
      version: targetVersion,
      project: estimate.project
        ? {
            ...estimate.project,
            startDate:
              typeof projectSnapshot?.startDate === "string"
                ? projectSnapshot.startDate
                : null,
            endDate:
              typeof projectSnapshot?.endDate === "string"
                ? projectSnapshot.endDate
                : null,
          }
        : null,
    },
    input.format,
  );

  await db.estimateExport.create({
    data: {
      estimateVersionId: targetVersion.id,
      format: input.format,
      fileName: buildExportFileName(
        estimate.name,
        targetVersion.versionNumber,
        input.format,
      ),
      payload: payload as unknown as Prisma.InputJsonValue,
    },
  });

  return getEstimateOrThrow(db, estimate.id);
}
