import { ImportBatchStatus, type Prisma } from "@capakraken/db";
import { parseDispoChargeabilityWorkbook } from "./parse-chargeability-workbook.js";
import { parseDispoPlanningWorkbook } from "./parse-dispo-matrix.js";
import { parseDispoRosterWorkbook } from "./parse-dispo-roster-workbook.js";
import { parseMandatoryDispoReferenceWorkbook } from "./parse-reference-workbook.js";
import { readWorksheetMatrix } from "./read-workbook.js";
import {
  DISPO_REFERENCE_SHEET,
  type DispoImportDbClient,
  isPseudoDemandResourceIdentity,
  normalizeText,
  toJsonObject,
} from "./shared.js";

export interface AssessDispoImportReadinessInput {
  chargeabilityWorkbookPath: string;
  costWorkbookPath?: string;
  importBatchId?: string;
  notes?: string | null;
  planningWorkbookPath: string;
  referenceWorkbookPath: string;
  rosterWorkbookPath?: string;
}

export interface DispoImportReadinessIssue {
  code:
    | "FALLBACK_EMAIL_REQUIRED"
    | "FALLBACK_LCR_REQUIRED"
    | "FALLBACK_UCR_REQUIRED"
    | "PUBLIC_HOLIDAY_IMPORT_REQUIRES_CALENDAR_SYNC"
    | "PLANNING_RESOURCE_MISSING_FROM_ROSTER"
    | "REFERENCE_RESOURCE_MASTER_MISSING"
    | "UNRESOLVED_RECORDS_PRESENT";
  count: number;
  message: string;
  resolution: string;
  severity: "blocker" | "warning";
}

export interface DispoImportReadinessReport {
  assignmentCount: number;
  availabilityRuleCount: number;
  canCommitWithFallbacks: boolean;
  canCommitWithStrictSourceData: boolean;
  fallbackAssumptions: string[];
  issues: DispoImportReadinessIssue[];
  projectCount: number;
  resourceCount: number;
  unresolvedCount: number;
  vacationCount: number;
}

interface MergedResourceReadinessRecord {
  canonicalExternalId: string;
  email: string | null;
  lcrCents: number | null;
  ucrCents: number | null;
}

function filterUnresolvedCount(
  unresolved: ReadonlyArray<{ resourceExternalId?: string | null }>,
  excludedIds: ReadonlySet<string>,
) {
  return unresolved.filter(
    (record) => !record.resourceExternalId || !excludedIds.has(record.resourceExternalId),
  ).length;
}

function buildReadinessIssue(issue: DispoImportReadinessIssue): DispoImportReadinessIssue {
  return issue;
}

function derivePlanningResourceIds(input: {
  assignments: Awaited<ReturnType<typeof parseDispoPlanningWorkbook>>["assignments"];
  availabilityRules: Awaited<ReturnType<typeof parseDispoPlanningWorkbook>>["availabilityRules"];
  vacations: Awaited<ReturnType<typeof parseDispoPlanningWorkbook>>["vacations"];
}) {
  const resourceIds = new Set<string>();

  for (const assignment of input.assignments) {
    if (isPseudoDemandResourceIdentity(assignment.resourceExternalId)) {
      continue;
    }
    resourceIds.add(assignment.resourceExternalId);
  }
  for (const vacation of input.vacations) {
    if (isPseudoDemandResourceIdentity(vacation.resourceExternalId)) {
      continue;
    }
    resourceIds.add(vacation.resourceExternalId);
  }
  for (const rule of input.availabilityRules) {
    if (isPseudoDemandResourceIdentity(rule.resourceExternalId)) {
      continue;
    }
    resourceIds.add(rule.resourceExternalId);
  }

  return resourceIds;
}

async function hasResourceMasterRows(referenceWorkbookPath: string) {
  const rows = await readWorksheetMatrix(referenceWorkbookPath, DISPO_REFERENCE_SHEET);

  for (let index = 1; index < rows.length; index += 1) {
    const firstCell = normalizeText(rows[index]?.[0]);
    if (!firstCell) {
      continue;
    }

    if (/^[a-z0-9]+(?:\.[a-z0-9]+)+$/i.test(firstCell)) {
      return true;
    }
  }

  return false;
}

export async function assessDispoImportReadiness(
  input: AssessDispoImportReadinessInput,
): Promise<DispoImportReadinessReport> {
  const [
    referenceWorkbook,
    chargeabilityWorkbook,
    planningWorkbook,
    resourceMasterPresent,
    rosterWorkbook,
  ] =
    await Promise.all([
      parseMandatoryDispoReferenceWorkbook(input.referenceWorkbookPath),
      parseDispoChargeabilityWorkbook(input.chargeabilityWorkbookPath),
      parseDispoPlanningWorkbook(input.planningWorkbookPath),
      hasResourceMasterRows(input.referenceWorkbookPath),
      input.rosterWorkbookPath
        ? parseDispoRosterWorkbook(input.rosterWorkbookPath, {
          ...(input.costWorkbookPath ? { costWorkbookPath: input.costWorkbookPath } : {}),
        })
        : null,
    ]);

  const mergedResources = new Map<string, MergedResourceReadinessRecord>();
  const excludedIds = new Set(rosterWorkbook?.excludedCanonicalExternalIds ?? []);
  for (const resource of chargeabilityWorkbook.resources) {
    if (excludedIds.has(resource.canonicalExternalId)) {
      continue;
    }
    mergedResources.set(resource.canonicalExternalId, {
      canonicalExternalId: resource.canonicalExternalId,
      email: resource.email,
      lcrCents: null,
      ucrCents: null,
    });
  }
  for (const resource of rosterWorkbook?.resources ?? []) {
    if (excludedIds.has(resource.canonicalExternalId)) {
      continue;
    }
    const existing = mergedResources.get(resource.canonicalExternalId);
    mergedResources.set(resource.canonicalExternalId, {
      canonicalExternalId: resource.canonicalExternalId,
      email: resource.email ?? existing?.email ?? null,
      lcrCents: resource.lcrCents ?? existing?.lcrCents ?? null,
      ucrCents: resource.ucrCents ?? existing?.ucrCents ?? null,
    });
  }

  const rosterIds = new Set(mergedResources.keys());
  const planningResourceIds = derivePlanningResourceIds(planningWorkbook);
  const planningResourceMissingFromRoster = Array.from(planningResourceIds).filter(
    (resourceId) => !excludedIds.has(resourceId) && !rosterIds.has(resourceId),
  );
  const unresolvedCount =
    filterUnresolvedCount(chargeabilityWorkbook.unresolved, excludedIds) +
    filterUnresolvedCount(planningWorkbook.unresolved, excludedIds) +
    filterUnresolvedCount(rosterWorkbook?.unresolved ?? [], excludedIds);
  const publicHolidayImportCount = planningWorkbook.vacations.filter(
    (vacation) =>
      !excludedIds.has(vacation.resourceExternalId) && vacation.vacationType === "PUBLIC_HOLIDAY",
  ).length;
  const missingEmailCount = Array.from(mergedResources.values()).filter(
    (resource) => !resource.email,
  ).length;
  const missingLcrCount = Array.from(mergedResources.values()).filter(
    (resource) => resource.lcrCents === null,
  ).length;
  const missingUcrCount = Array.from(mergedResources.values()).filter(
    (resource) => resource.ucrCents === null,
  ).length;

  const issues: DispoImportReadinessIssue[] = [];

  if (!resourceMasterPresent && !rosterWorkbook) {
    issues.push(
      buildReadinessIssue({
        code: "REFERENCE_RESOURCE_MASTER_MISSING",
        count: 1,
        message:
          "MandatoryDispoCategories_V3.xlsx contains reference/glossary sections only; it does not contain row-based resource master records.",
        resolution:
          "Provide a real resource master source or explicitly approve generated fallback values for missing required Resource fields.",
        severity: "blocker",
      }),
    );
  }

  if (missingEmailCount > 0) {
    issues.push(
      buildReadinessIssue({
        code: "FALLBACK_EMAIL_REQUIRED",
        count: missingEmailCount,
        message:
          "Some imported resources still do not have real email addresses after merging the Dispo roster and SAP source data.",
        resolution:
          "Approve generated placeholder emails for the remaining resources or provide a missing roster/SAP source with email addresses.",
        severity: "blocker",
      }),
    );
  }

  if (missingLcrCount > 0) {
    issues.push(
      buildReadinessIssue({
        code: "FALLBACK_LCR_REQUIRED",
        count: missingLcrCount,
        message:
          "Some staged resources still do not have resolved LCR values after merging roster data with the cost-rate workbook.",
        resolution:
          "Provide missing per-resource LCR values or approve placeholders only for the remaining unresolved resources.",
        severity: "blocker",
      }),
    );
  }

  if (missingUcrCount > 0) {
    issues.push(
      buildReadinessIssue({
        code: "FALLBACK_UCR_REQUIRED",
        count: missingUcrCount,
        message:
          "Some staged resources still do not have resolved UCR values after merging roster data with the cost-rate workbook.",
        resolution:
          "Provide missing per-resource UCR values or approve placeholders only for the remaining unresolved resources.",
        severity: "blocker",
      }),
    );
  }

  if (planningResourceMissingFromRoster.length > 0) {
    issues.push(
      buildReadinessIssue({
        code: "PLANNING_RESOURCE_MISSING_FROM_ROSTER",
        count: planningResourceMissingFromRoster.length,
        message:
          "Some resource identities appear in planning data but not in the merged roster/resource-master inputs.",
        resolution:
          "Add the missing resources to the roster or chargeability inputs, or decide how planning-only identities should be imported.",
        severity: "blocker",
      }),
    );
  }

  if (publicHolidayImportCount > 0) {
    issues.push(
      buildReadinessIssue({
        code: "PUBLIC_HOLIDAY_IMPORT_REQUIRES_CALENDAR_SYNC",
        count: publicHolidayImportCount,
        message:
          "Planning import contains PUBLIC_HOLIDAY rows. Public holidays must be managed through holiday calendars so country/state/city-specific rules stay canonical.",
        resolution:
          "Import or update the relevant holiday calendars first, then remove PUBLIC_HOLIDAY rows from the generic planning/vacation import before commit.",
        severity: "blocker",
      }),
    );
  }

  if (unresolvedCount > 0) {
    issues.push(
      buildReadinessIssue({
        code: "UNRESOLVED_RECORDS_PRESENT",
        count: unresolvedCount,
        message:
          "The staging inputs contain unresolved rows, primarily [tbd] project references that must stay out of final project commit.",
        resolution:
          "Review unresolved rows before final commit or keep automatic commit scoped to resolved rows only.",
        severity: "warning",
      }),
    );
  }

  const strictBlockers = issues.filter((issue) => issue.severity === "blocker");
  const fallbackOnlyBlockers = new Set<DispoImportReadinessIssue["code"]>([
    "FALLBACK_EMAIL_REQUIRED",
    "FALLBACK_LCR_REQUIRED",
    "FALLBACK_UCR_REQUIRED",
    "REFERENCE_RESOURCE_MASTER_MISSING",
  ]);

  const canCommitWithStrictSourceData = strictBlockers.length === 0;
  const canCommitWithFallbacks = strictBlockers.every((issue) =>
    fallbackOnlyBlockers.has(issue.code),
  );

  return {
    resourceCount: mergedResources.size,
    projectCount: planningWorkbook.assignments.filter(
      (assignment) =>
        assignment.projectKey !== null && !assignment.isTbd && !assignment.isUnassigned,
    ).length,
    assignmentCount: planningWorkbook.assignments.length,
    vacationCount: planningWorkbook.vacations.length,
    availabilityRuleCount: planningWorkbook.availabilityRules.length,
    unresolvedCount,
    canCommitWithStrictSourceData,
    canCommitWithFallbacks,
    fallbackAssumptions: canCommitWithFallbacks
      ? [
          "Generate fallback email as <enterpriseId>@accenture.com for imported resources that do not have one in the source files.",
          "Commit placeholder LCR/UCR values only for resources still unresolved after roster-to-rate matching and level-average fallback.",
          "Keep unresolved [tbd] rows staged and exclude them from final project creation.",
        ]
      : [],
    issues,
  };
}

export async function persistDispoImportReadiness(
  db: DispoImportDbClient,
  input: AssessDispoImportReadinessInput & { importBatchId: string },
) {
  const report = await assessDispoImportReadiness(input);
  const batch = await db.importBatch.findUnique({
    where: { id: input.importBatchId },
    select: { id: true, summary: true },
  });

  if (!batch) {
    throw new Error(`Import batch "${input.importBatchId}" not found`);
  }

  const nextSummary = {
    ...toJsonObject(batch.summary),
    readiness: report,
  };

  await db.importBatch.update({
    where: { id: batch.id },
    data: {
      status: ImportBatchStatus.STAGED,
      summary: nextSummary as unknown as Prisma.InputJsonValue,
    },
  });

  return report;
}
