import type { Prisma } from "@capakraken/db";
import { AllocationType, DispoImportSourceKind, OrderType, StagedRecordStatus } from "@capakraken/db";
import { DISPO_INTERNAL_PROJECT_BUCKETS } from "@capakraken/shared";
import { parseDispoPlanningWorkbook } from "./parse-dispo-matrix.js";
import {
  classifyDispoProject,
  deriveResolvedDispoProjectIdentity,
  deriveTbdDispoProjectIdentity,
} from "./tbd-projects.js";
import {
  DISPO_PLANNING_SHEET,
  ensureImportBatch,
  finalizeImportBatchStage,
  getWorkbookFileName,
  isPseudoDemandResourceIdentity,
  type DispoImportDbClient,
  type DispoPlanningImportInput,
  normalizeText,
} from "./shared.js";

interface ResolvedStagedProject {
  allocationType: AllocationType;
  clientCode: string | null;
  isInternal: boolean;
  isTbd: boolean;
  name: string;
  orderType: OrderType;
  projectKey: string;
  rawTokens: Set<string>;
  shortCode: string;
  sourceColumn: string;
  sourceRow: number;
  startDate: Date;
  endDate: Date;
  utilizationCategoryCode: string | null;
  warnings: Set<string>;
  winProbability: number | null;
}

function updateDateRange(project: ResolvedStagedProject, assignmentDate: Date) {
  if (assignmentDate < project.startDate) {
    project.startDate = assignmentDate;
  }
  if (assignmentDate > project.endDate) {
    project.endDate = assignmentDate;
  }
}

export interface StageDispoProjectsResult {
  batchId: string;
  counts: {
    stagedProjects: number;
    warnings: number;
  };
}

export async function stageDispoProjects(
  db: DispoImportDbClient,
  input: DispoPlanningImportInput,
): Promise<StageDispoProjectsResult> {
  const batchInput: {
    importBatchId?: string;
    notes?: string | null;
    planningSourceFile: string;
  } = {
    planningSourceFile: getWorkbookFileName(input.planningWorkbookPath),
  };

  if (input.importBatchId !== undefined) {
    batchInput.importBatchId = input.importBatchId;
  }
  if (input.notes !== undefined) {
    batchInput.notes = input.notes;
  }

  const batch = await ensureImportBatch(db, batchInput);
  const parsed = await parseDispoPlanningWorkbook(input.planningWorkbookPath);
  const excludedIds = new Set(input.excludedResourceExternalIds ?? []);
  const sourceWorkbook = getWorkbookFileName(input.planningWorkbookPath);
  const projects = new Map<string, ResolvedStagedProject>();

  for (const bucket of DISPO_INTERNAL_PROJECT_BUCKETS) {
    projects.set(bucket.shortCode, {
      allocationType: AllocationType.INT,
      clientCode: null,
      isInternal: true,
      isTbd: false,
      name: bucket.name,
      orderType: OrderType.INTERNAL,
      projectKey: bucket.shortCode,
      rawTokens: new Set<string>([`{${bucket.sourceToken}}`]),
      shortCode: bucket.shortCode,
      sourceColumn: "A",
      sourceRow: 0,
      startDate: new Date("2100-01-01T00:00:00.000Z"),
      endDate: new Date("1970-01-01T00:00:00.000Z"),
      utilizationCategoryCode: bucket.utilizationCategoryCode,
      warnings: new Set<string>(),
      winProbability: 100,
    });
  }

  for (const assignment of parsed.assignments) {
    if (
      excludedIds.has(assignment.resourceExternalId) ||
      isPseudoDemandResourceIdentity(assignment.resourceExternalId)
    ) {
      continue;
    }

    if (assignment.isUnassigned) {
      continue;
    }

    if (assignment.isInternal) {
      const internalBucket = DISPO_INTERNAL_PROJECT_BUCKETS.find(
        (bucket) => assignment.rawToken.includes(`{${bucket.sourceToken}}`),
      );
      if (!internalBucket) {
        continue;
      }

      const project = projects.get(internalBucket.shortCode);
      if (!project) {
        continue;
      }

      updateDateRange(project, assignment.assignmentDate);
      project.rawTokens.add(assignment.rawToken);
      continue;
    }

    if (!assignment.projectKey && !assignment.isTbd) {
      continue;
    }

    const identity = assignment.isTbd
      ? deriveTbdDispoProjectIdentity(assignment.rawToken, assignment.utilizationCategoryCode)
      : deriveResolvedDispoProjectIdentity(assignment.rawToken, assignment.projectKey!);
    const classification = classifyDispoProject(assignment.utilizationCategoryCode);
    const existing = projects.get(identity.projectKey);

    if (!existing) {
      projects.set(identity.projectKey, {
        allocationType: classification.allocationType,
        clientCode: identity.clientCode,
        isInternal: false,
        isTbd: assignment.isTbd,
        name: identity.name,
        orderType: classification.orderType,
        projectKey: identity.projectKey,
        rawTokens: new Set<string>([assignment.rawToken]),
        shortCode: identity.shortCode,
        sourceColumn: assignment.sourceColumn,
        sourceRow: assignment.sourceRow,
        startDate: assignment.assignmentDate,
        endDate: assignment.assignmentDate,
        utilizationCategoryCode: assignment.utilizationCategoryCode,
        warnings: new Set<string>(assignment.warnings),
        winProbability: assignment.winProbability,
      });
      continue;
    }

    updateDateRange(existing, assignment.assignmentDate);
    existing.rawTokens.add(assignment.rawToken);

    if (identity.clientCode && existing.clientCode && existing.clientCode !== identity.clientCode) {
      existing.warnings.add(
        `Conflicting client codes for project ${identity.projectKey}: ${existing.clientCode} vs ${identity.clientCode}`,
      );
    }

    if (!existing.clientCode && identity.clientCode) {
      existing.clientCode = identity.clientCode;
    }

    if (normalizeText(existing.name) !== normalizeText(identity.name)) {
      existing.warnings.add(
        `Multiple project names observed for ${identity.projectKey}; using "${existing.name}"`,
      );
    }

    if (
      existing.utilizationCategoryCode &&
      assignment.utilizationCategoryCode &&
      existing.utilizationCategoryCode !== assignment.utilizationCategoryCode
    ) {
      existing.warnings.add(
        `Conflicting utilization categories for ${identity.projectKey}: ${existing.utilizationCategoryCode} vs ${assignment.utilizationCategoryCode}`,
      );
    }

    if (!existing.utilizationCategoryCode && assignment.utilizationCategoryCode) {
      existing.utilizationCategoryCode = assignment.utilizationCategoryCode;
    }

    if (
      existing.winProbability !== null &&
      assignment.winProbability !== null &&
      existing.winProbability !== assignment.winProbability
    ) {
      existing.warnings.add(
        `Conflicting win probabilities for ${identity.projectKey}: ${existing.winProbability} vs ${assignment.winProbability}`,
      );
    }

    if (existing.winProbability === null && assignment.winProbability !== null) {
      existing.winProbability = assignment.winProbability;
    }
  }

  await db.stagedProject.deleteMany({
    where: {
      importBatchId: batch.id,
      sourceKind: DispoImportSourceKind.PLANNING,
    },
  });

  const stagedProjects = Array.from(projects.values())
    .filter((project) => {
      if (!project.isInternal) {
        return true;
      }

      return project.startDate <= project.endDate;
    })
    .map((project) => ({
      importBatchId: batch.id,
      status: project.warnings.size > 0
        ? StagedRecordStatus.PARSED
        : StagedRecordStatus.NORMALIZED,
      sourceKind: DispoImportSourceKind.PLANNING,
      sourceWorkbook,
      sourceSheet: DISPO_PLANNING_SHEET,
      sourceRow: project.sourceRow,
      sourceColumn: project.sourceColumn,
      projectKey: project.projectKey,
      shortCode: project.shortCode,
      name: project.name,
      clientCode: project.clientCode,
      utilizationCategoryCode: project.utilizationCategoryCode,
      orderType: project.orderType,
      allocationType: project.allocationType,
      winProbability: project.winProbability,
      isInternal: project.isInternal,
      isTbd: project.isTbd,
      startDate: project.startDate,
      endDate: project.endDate,
      warnings: Array.from(project.warnings),
      rawPayload: {
        rawTokens: Array.from(project.rawTokens),
      } as Prisma.InputJsonValue,
      normalizedData: {
        clientCode: project.clientCode,
        name: project.name,
        utilizationCategoryCode: project.utilizationCategoryCode,
        winProbability: project.winProbability,
      } as Prisma.InputJsonValue,
    }));

  if (stagedProjects.length > 0) {
    await db.stagedProject.createMany({
      data: stagedProjects,
    });
  }

  const warningCount = stagedProjects.reduce((count, project) => count + project.warnings.length, 0);
  const summary = {
    stagedProjects: stagedProjects.length,
    warnings: warningCount,
  };

  await finalizeImportBatchStage(db, {
    batchId: batch.id,
    existingSummary: batch.summary,
    key: "projectResolution",
    summary,
  });

  return {
    batchId: batch.id,
    counts: summary,
  };
}
