import { fileURLToPath, pathToFileURL } from "node:url";
import { resolve } from "node:path";
import { PrismaClient, StagedRecordStatus } from "@prisma/client";
import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js";
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";

loadWorkspaceEnv();

const prisma = new PrismaClient();

const DEFAULT_REFERENCE_WORKBOOK = resolveWorkspacePath(
  "samples",
  "Dispov2",
  "MandatoryDispoCategories_V3.xlsx",
);
const DEFAULT_PLANNING_WORKBOOK = resolveWorkspacePath(
  "samples",
  "Dispov2",
  "DISPO_2026.xlsx",
);
const DEFAULT_CHARGEABILITY_WORKBOOK = resolveWorkspacePath(
  "samples",
  "Dispov2",
  "20260309_Bi-Weekly_Chargeability_Reporting_Content_Production_V0.943_4Hartmut.xlsx",
);
const DEFAULT_ROSTER_WORKBOOK = resolveWorkspacePath(
  "samples",
  "Dispov2",
  "MV_DispoRoster.xlsx",
);
const DEFAULT_COST_WORKBOOK = resolveWorkspacePath(
  "samples",
  "Dispov2",
  "Resource Roster_MASTER_FY26_CJ_20251201.xlsx",
);

export interface ImportDispoBatchOptions {
  allowTbdUnresolved: boolean;
  chargeabilityWorkbookPath: string;
  costWorkbookPath: string | undefined;
  importTbdProjects: boolean;
  notes: string | undefined;
  planningWorkbookPath: string;
  previewUnresolvedLimit: number;
  referenceWorkbookPath: string;
  rosterWorkbookPath: string | undefined;
  skipCommit: boolean;
  strictSourceData: boolean;
}

interface DispoImportReadinessIssue {
  code: string;
  count: number;
  message: string;
  resolution: string;
  severity: "blocker" | "warning";
}

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

interface StageDispoImportBatchInput {
  chargeabilityWorkbookPath: string;
  costWorkbookPath?: string;
  notes?: string | null;
  planningWorkbookPath: string;
  referenceWorkbookPath: string;
  rosterWorkbookPath?: string;
}

interface StageDispoImportBatchResult {
  batchId: string;
  counts: {
    stagedAssignments: number;
    stagedAvailabilityRules: number;
    stagedClients: number;
    stagedProjects: number;
    stagedResources: number;
    stagedRosterResources: number;
    stagedVacations: number;
    unresolved: number;
  };
  readiness: DispoImportReadinessReport;
}

interface CommitDispoImportBatchInput {
  allowTbdUnresolved?: boolean;
  importTbdProjects?: boolean;
  importBatchId: string;
}

interface CommitDispoImportBatchResult {
  batchId: string;
  counts: {
    committedAssignments: number;
    committedProjects: number;
    committedResources: number;
    committedVacations: number;
    updatedEntitlements: number;
    updatedResourceAvailabilities: number;
    upsertedResourceRoles: number;
  };
  unresolved: {
    blocked: number;
    skippedTbd: number;
  };
}

interface DispoImportModule {
  stageDispoImportBatch(
    db: PrismaClient,
    input: StageDispoImportBatchInput,
  ): Promise<StageDispoImportBatchResult>;
  commitDispoImportBatch(
    db: PrismaClient,
    input: CommitDispoImportBatchInput,
  ): Promise<CommitDispoImportBatchResult>;
}

interface UnresolvedPreviewRecord {
  message: string;
  projectKey: string | null;
  recordType: string;
  resolutionHint: string | null;
  resourceExternalId: string | null;
  sourceRow: number;
  sourceSheet: string;
}

function requireValue(argv: string[], index: number, flag: string): string {
  const value = argv[index + 1];

  if (!value || value.startsWith("--")) {
    throw new Error(`Missing value for ${flag}`);
  }

  return value;
}

export function parseImportDispoBatchArgs(argv: string[]): ImportDispoBatchOptions {
  const options: ImportDispoBatchOptions = {
    allowTbdUnresolved: true,
    chargeabilityWorkbookPath: DEFAULT_CHARGEABILITY_WORKBOOK,
    costWorkbookPath: DEFAULT_COST_WORKBOOK,
    importTbdProjects: false,
    notes: undefined,
    planningWorkbookPath: DEFAULT_PLANNING_WORKBOOK,
    previewUnresolvedLimit: 10,
    referenceWorkbookPath: DEFAULT_REFERENCE_WORKBOOK,
    rosterWorkbookPath: DEFAULT_ROSTER_WORKBOOK,
    skipCommit: false,
    strictSourceData: false,
  };

  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index];

    if (argument === "--reference-workbook") {
      options.referenceWorkbookPath = resolve(requireValue(argv, index, argument));
      index += 1;
      continue;
    }

    if (argument === "--planning-workbook") {
      options.planningWorkbookPath = resolve(requireValue(argv, index, argument));
      index += 1;
      continue;
    }

    if (argument === "--chargeability-workbook") {
      options.chargeabilityWorkbookPath = resolve(requireValue(argv, index, argument));
      index += 1;
      continue;
    }

    if (argument === "--roster-workbook") {
      options.rosterWorkbookPath = resolve(requireValue(argv, index, argument));
      index += 1;
      continue;
    }

    if (argument === "--cost-workbook") {
      options.costWorkbookPath = resolve(requireValue(argv, index, argument));
      index += 1;
      continue;
    }

    if (argument === "--notes") {
      options.notes = requireValue(argv, index, argument);
      index += 1;
      continue;
    }

    if (argument === "--preview-unresolved") {
      const value = Number.parseInt(requireValue(argv, index, argument), 10);
      if (!Number.isInteger(value) || value < 0) {
        throw new Error(`Invalid value for ${argument}: expected a non-negative integer`);
      }
      options.previewUnresolvedLimit = value;
      index += 1;
      continue;
    }

    if (argument === "--skip-commit") {
      options.skipCommit = true;
      continue;
    }

    if (argument === "--strict-source-data") {
      options.strictSourceData = true;
      continue;
    }

    if (argument === "--disallow-tbd") {
      options.allowTbdUnresolved = false;
      continue;
    }

    if (argument === "--import-tbd-projects") {
      options.importTbdProjects = true;
      continue;
    }

    if (argument === "--no-roster") {
      options.rosterWorkbookPath = undefined;
      continue;
    }

    if (argument === "--no-cost") {
      options.costWorkbookPath = undefined;
      continue;
    }

    if (argument === "--help" || argument === "-h") {
      throw new Error(buildHelpText());
    }

    throw new Error(`Unknown argument: ${argument}`);
  }

  return options;
}

function buildHelpText() {
  return [
    "Usage: pnpm --filter @capakraken/db db:import:dispo [options]",
    "",
    "Options:",
    "  --reference-workbook <path>      Override MandatoryDispoCategories workbook",
    "  --planning-workbook <path>       Override DISPO planning workbook",
    "  --chargeability-workbook <path>  Override chargeability workbook",
    "  --roster-workbook <path>         Override dispo roster workbook",
    "  --cost-workbook <path>           Override cost-rate workbook",
    "  --no-roster                      Stage without roster workbook",
    "  --no-cost                        Stage without cost-rate workbook",
    "  --notes <text>                   Attach operator notes to the import batch",
    "  --preview-unresolved <count>     Print up to N unresolved rows (default 10)",
    "  --skip-commit                    Stage and assess readiness only",
    "  --strict-source-data             Require readiness without fallback assumptions",
    "  --disallow-tbd                   Fail commit if [tbd] unresolved rows remain",
    "  --import-tbd-projects            Commit [tbd] rows as provisional DRAFT projects",
  ].join("\n");
}

async function loadDispoImportModule(): Promise<DispoImportModule> {
  const modulePath = resolveWorkspacePath("packages", "application", "src", "index.ts");
  return import(pathToFileURL(modulePath).href) as Promise<DispoImportModule>;
}

function printWorkbookSources(options: ImportDispoBatchOptions) {
  console.log("Dispo import sources:");
  console.log(`  reference:      ${options.referenceWorkbookPath}`);
  console.log(`  planning:       ${options.planningWorkbookPath}`);
  console.log(`  chargeability:  ${options.chargeabilityWorkbookPath}`);
  console.log(`  roster:         ${options.rosterWorkbookPath ?? "(disabled)"}`);
  console.log(`  cost rates:     ${options.costWorkbookPath ?? "(disabled)"}`);
}

function printReadiness(report: DispoImportReadinessReport) {
  console.log("Readiness:");
  console.log(`  resources:            ${report.resourceCount}`);
  console.log(`  assignment rows:      ${report.assignmentCount}`);
  console.log(`  project rows:         ${report.projectCount}`);
  console.log(`  vacations:            ${report.vacationCount}`);
  console.log(`  availability rules:   ${report.availabilityRuleCount}`);
  console.log(`  unresolved rows:      ${report.unresolvedCount}`);
  console.log(`  strict commit ready:  ${report.canCommitWithStrictSourceData ? "yes" : "no"}`);
  console.log(`  fallback commit ready:${report.canCommitWithFallbacks ? " yes" : " no"}`);

  if (report.issues.length === 0) {
    console.log("  issues:               none");
    return;
  }

  console.log("  issues:");
  for (const issue of report.issues) {
    console.log(`    - [${issue.severity}] ${issue.code} (${issue.count})`);
    console.log(`      ${issue.message}`);
    console.log(`      resolution: ${issue.resolution}`);
  }

  if (report.fallbackAssumptions.length > 0) {
    console.log("  approved fallback assumptions required:");
    for (const assumption of report.fallbackAssumptions) {
      console.log(`    - ${assumption}`);
    }
  }
}

async function loadUnresolvedPreview(
  batchId: string,
  limit: number,
): Promise<UnresolvedPreviewRecord[]> {
  if (limit <= 0) {
    return [];
  }

  return prisma.stagedUnresolvedRecord.findMany({
    where: {
      importBatchId: batchId,
      status: StagedRecordStatus.UNRESOLVED,
    },
    orderBy: [
      { recordType: "asc" },
      { sourceSheet: "asc" },
      { sourceRow: "asc" },
    ],
    take: limit,
    select: {
      message: true,
      projectKey: true,
      recordType: true,
      resolutionHint: true,
      resourceExternalId: true,
      sourceRow: true,
      sourceSheet: true,
    },
  });
}

function printUnresolvedPreview(records: ReadonlyArray<UnresolvedPreviewRecord>, total: number) {
  if (records.length === 0) {
    return;
  }

  console.log(`Unresolved preview (${records.length}/${total}):`);
  for (const record of records) {
    const location = `${record.sourceSheet}:${record.sourceRow}`;
    const identity = [record.resourceExternalId, record.projectKey].filter(Boolean).join(" | ");
    console.log(`  - ${record.recordType} @ ${location}${identity ? ` | ${identity}` : ""}`);
    console.log(`    ${record.message}`);
    if (record.resolutionHint) {
      console.log(`    resolution: ${record.resolutionHint}`);
    }
  }
}

function ensureCommitAllowed(options: ImportDispoBatchOptions, readiness: DispoImportReadinessReport) {
  if (options.strictSourceData) {
    if (!readiness.canCommitWithStrictSourceData) {
      throw new Error("Readiness is not strict-source-data clean. Re-run without --strict-source-data or fix blockers.");
    }
    return;
  }

  if (!readiness.canCommitWithFallbacks) {
    throw new Error("Readiness has unresolved blocker issues that are not covered by the agreed fallback rules.");
  }
}

export async function runImportDispoBatch(options: ImportDispoBatchOptions) {
  assertCapaKrakenDbTarget("db:import:dispo");
  const dispoImport = await loadDispoImportModule();

  printWorkbookSources(options);
  console.log("");
  console.log("Staging workbook data...");

  const stageResult = await dispoImport.stageDispoImportBatch(prisma, {
    chargeabilityWorkbookPath: options.chargeabilityWorkbookPath,
    ...(options.costWorkbookPath ? { costWorkbookPath: options.costWorkbookPath } : {}),
    ...(options.notes ? { notes: options.notes } : {}),
    planningWorkbookPath: options.planningWorkbookPath,
    referenceWorkbookPath: options.referenceWorkbookPath,
    ...(options.rosterWorkbookPath ? { rosterWorkbookPath: options.rosterWorkbookPath } : {}),
  });

  console.log(`Staged import batch: ${stageResult.batchId}`);
  console.log("Stage counts:");
  console.log(`  clients:              ${stageResult.counts.stagedClients}`);
  console.log(`  resources:            ${stageResult.counts.stagedResources}`);
  console.log(`  roster resources:     ${stageResult.counts.stagedRosterResources}`);
  console.log(`  projects:             ${stageResult.counts.stagedProjects}`);
  console.log(`  assignments:          ${stageResult.counts.stagedAssignments}`);
  console.log(`  vacations:            ${stageResult.counts.stagedVacations}`);
  console.log(`  availability rules:   ${stageResult.counts.stagedAvailabilityRules}`);
  console.log(`  unresolved:           ${stageResult.counts.unresolved}`);
  printReadiness(stageResult.readiness);

  const unresolvedPreview = await loadUnresolvedPreview(
    stageResult.batchId,
    options.previewUnresolvedLimit,
  );
  printUnresolvedPreview(unresolvedPreview, stageResult.readiness.unresolvedCount);

  if (options.skipCommit) {
    console.log("");
    console.log("Commit skipped by operator flag.");
    return { stageResult, commitResult: null };
  }

  ensureCommitAllowed(options, stageResult.readiness);

  console.log("");
  console.log("Committing staged rows into live CapaKraken tables...");

  const commitResult = await dispoImport.commitDispoImportBatch(prisma, {
    allowTbdUnresolved: options.allowTbdUnresolved,
    importTbdProjects: options.importTbdProjects,
    importBatchId: stageResult.batchId,
  });

  console.log(`Committed import batch: ${commitResult.batchId}`);
  console.log("Commit counts:");
  console.log(`  resources:                 ${commitResult.counts.committedResources}`);
  console.log(`  resource roles:            ${commitResult.counts.upsertedResourceRoles}`);
  console.log(`  projects:                  ${commitResult.counts.committedProjects}`);
  console.log(`  assignments:               ${commitResult.counts.committedAssignments}`);
  console.log(`  vacations/public holidays: ${commitResult.counts.committedVacations}`);
  console.log(`  vacation entitlements:     ${commitResult.counts.updatedEntitlements}`);
  console.log(`  availability overlays:     ${commitResult.counts.updatedResourceAvailabilities}`);
  console.log(`  blocked unresolved:        ${commitResult.unresolved.blocked}`);
  console.log(`  skipped [tbd] unresolved:  ${commitResult.unresolved.skippedTbd}`);

  return { stageResult, commitResult };
}

async function main() {
  try {
    const options = parseImportDispoBatchArgs(process.argv.slice(2));
    await runImportDispoBatch(options);
  } catch (error) {
    if (error instanceof Error) {
      console.error(error.message);
    } else {
      console.error(error);
    }
    process.exitCode = 1;
  } finally {
    await prisma.$disconnect();
  }
}

const currentFile = fileURLToPath(import.meta.url);
const entryFile = process.argv[1] ? resolve(process.argv[1]) : null;

if (entryFile === currentFile) {
  void main();
}
