import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import {
  findProvider,
  loadConfig,
  type PayTypeMapping,
  type PayMappingConfig,
  type ProviderRate,
} from "./pay-mapping.js";

export interface AmdVisitInput {
  id?: string;
  date: string;
  columnheading?: string;
  columnHeading?: string;
  appointmentType?: string;
  appointmentTypeId?: number;
  apptstatus?: number;
  apptStatus?: number;
  billableNoShow?: boolean;
  chargeNote?: string;
  cancellationNote?: string;
  notes?: string;
  cptCodes?: string[];
  patient_name?: string;
  patientName?: string;
}

export interface EncounterInputs {
  newPatientAppointments: number;
  existingPatientAppointments: number;
  billableNoShowAppointments: number;
}

export interface DailyEncounterCount {
  providerEmail: string;
  providerName: string;
  amdColumnHeading: string;
  date: string;
  role: "psych" | "therapy";
  inputs: EncounterInputs;
  sourceVisitIds: string[];
}

export interface AmdCountsCheckpoint {
  schemaVersion: 1;
  generatedAt: string;
  period: { start: string; end: string };
  source: "advancedmd-api" | "browser-export" | "fixture";
  counts: DailyEncounterCount[];
  totals: EncounterInputs & { providerDates: number; visitsProcessed: number; visitsSkipped: number };
  blockingIssues: BlockingIssue[];
}

export interface BlockingIssue {
  code: string;
  message: string;
  date?: string;
  provider?: string;
  appointmentType?: string;
  appointmentTypeId?: number;
}

export interface RipplingInspection {
  schemaVersion: 1;
  inspectedAt: string;
  period: { start: string; end: string };
  schedules: RipplingSchedule[];
  rows: RipplingTimecardRow[];
  deepRajLunch?: DeepRajLunchResult[];
}

export interface RipplingSchedule {
  id: string;
  name: string;
  roleId?: string;
  roleName?: string;
}

export interface RipplingTimecardRow {
  rowId: string;
  providerEmail: string;
  providerName?: string;
  date: string;
  scheduleId?: string;
  scheduleName?: string;
  roleId?: string;
  roleName?: string;
  status?: string;
  approved: boolean;
  paid: boolean;
  locked?: boolean;
  editable: {
    inputs: boolean;
    approve: boolean;
    unapprove: boolean;
    markPaid: boolean;
    unmarkPaid: boolean;
  };
  shiftInputs: Partial<EncounterInputs>;
  premiums?: Array<{ name: string; quantity?: number; amount?: number }>;
}

export interface DeepRajLunchResult {
  employee: "Laura" | "Shaye" | "Mae" | string;
  status: "verified" | "corrected" | "missing" | "needs_human_review";
  details: string;
}

export type WriteActionType =
  | "enter_inputs_and_approve"
  | "verify_only"
  | "update_paid_or_approved_row"
  | "skip"
  | "needs_human_review";

export interface WritePlanAction {
  action: WriteActionType;
  providerEmail: string;
  providerName: string;
  date: string;
  rowId?: string;
  expectedInputs: EncounterInputs;
  currentInputs?: Partial<EncounterInputs>;
  approved?: boolean;
  paid?: boolean;
  reason: string;
  reversibleStatusChangeRequired?: boolean;
}

export interface WritePlan {
  schemaVersion: 1;
  generatedAt: string;
  period: { start: string; end: string };
  actions: WritePlanAction[];
  summary: {
    enterAndApprove: number;
    verifyOnly: number;
    updatePaidOrApproved: number;
    skipped: number;
    needsHumanReview: number;
  };
  blockingIssues: BlockingIssue[];
}

export interface VerificationCheckpoint {
  schemaVersion: 1;
  verifiedAt: string;
  period: { start: string; end: string };
  rows: Array<{
    providerEmail: string;
    date: string;
    rowId?: string;
    savedInputs: Partial<EncounterInputs>;
    approved: boolean;
    paid: boolean;
  }>;
  mismatches?: VerificationMismatch[];
}

export interface VerificationMismatch {
  providerEmail: string;
  date: string;
  rowId?: string;
  reason: string;
}

const ZERO_INPUTS: EncounterInputs = {
  newPatientAppointments: 0,
  existingPatientAppointments: 0,
  billableNoShowAppointments: 0,
};

export function buildAmdCounts(
  visits: AmdVisitInput[],
  period: { start: string; end: string },
  cfg: PayMappingConfig,
  source: AmdCountsCheckpoint["source"] = "advancedmd-api"
): AmdCountsCheckpoint {
  const counts = new Map<string, DailyEncounterCount>();
  const blockingIssues: BlockingIssue[] = [];
  let visitsSkipped = 0;

  for (const visit of visits) {
    const date = normalizeDate(visit.date);
    if (!date) {
      visitsSkipped++;
      blockingIssues.push({
        code: "invalid_visit_date",
        message: "AMD visit date is missing or not ISO-formatted.",
        date: visit.date,
        provider: visit.columnheading ?? visit.columnHeading,
      });
      continue;
    }
    if (date < period.start || date > period.end) {
      visitsSkipped++;
      continue;
    }

    const columnHeading = (visit.columnheading ?? visit.columnHeading ?? "").trim();
    const provider = findProvider(columnHeading, cfg);
    if (!provider) {
      visitsSkipped++;
      blockingIssues.push({
        code: "unmapped_provider",
        message: "AMD visit uses an unmapped provider column.",
        date,
        provider: columnHeading || "unknown",
      });
      continue;
    }

    const appointmentTypeId = Number(visit.appointmentTypeId);
    const apptStatus = Number(visit.apptstatus ?? visit.apptStatus);
    const payTypes = classifyInputPayTypes(appointmentTypeId, apptStatus, cfg);
    if (payTypes.length === 0) {
      if (!cfg.appt_status.excluded.includes(apptStatus)) {
        blockingIssues.push({
          code: "unmapped_appointment_type",
          message: "AMD visit has a status/type that is not mapped for shift inputs.",
          date,
          provider: provider.amd_column_heading,
          appointmentType: visit.appointmentType,
          appointmentTypeId,
        });
      }
      visitsSkipped++;
      continue;
    }

    let counted = false;
    let skippedThisVisit = false;
    const key = `${provider.rippling_email}|${date}`;
    const daily = counts.get(key) ?? newDailyCount(provider, date, payTypes[0]?.role ?? "psych");
    for (const payType of payTypes) {
      const rate = provider.rates[payType.ripplingName] ?? 0;
      if (rate <= 0) {
        skippedThisVisit = true;
        continue;
      }
      if (payType.inputKey === "billableNoShowAppointments" && !hasBillableNoShowEvidence(visit)) {
        skippedThisVisit = true;
        blockingIssues.push({
          code: "unverified_billable_noshow",
          message: "No-show visit lacks explicit billable/charge evidence.",
          date,
          provider: provider.amd_column_heading,
          appointmentType: visit.appointmentType,
          appointmentTypeId,
        });
        continue;
      }

      daily.inputs[payType.inputKey]++;
      counted = true;
    }

    if (counted) {
      if (visit.id) daily.sourceVisitIds.push(visit.id);
      counts.set(key, daily);
    } else if (skippedThisVisit) {
      visitsSkipped++;
    }
  }

  const sortedCounts = Array.from(counts.values()).sort(compareProviderDate);
  const totals = sortedCounts.reduce(
    (acc, row) => {
      acc.newPatientAppointments += row.inputs.newPatientAppointments;
      acc.existingPatientAppointments += row.inputs.existingPatientAppointments;
      acc.billableNoShowAppointments += row.inputs.billableNoShowAppointments;
      acc.providerDates += 1;
      return acc;
    },
    { ...ZERO_INPUTS, providerDates: 0, visitsProcessed: visits.length, visitsSkipped }
  );

  return {
    schemaVersion: 1,
    generatedAt: new Date().toISOString(),
    period,
    source,
    counts: sortedCounts,
    totals,
    blockingIssues: dedupeIssues(blockingIssues),
  };
}

export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: RipplingInspection): WritePlan {
  const blockingIssues: BlockingIssue[] = [...counts.blockingIssues];
  if (counts.period.start !== inspection.period.start || counts.period.end !== inspection.period.end) {
    blockingIssues.push({
      code: "period_mismatch",
      message: "AMD counts and Rippling inspection are for different pay periods.",
    });
  }

  const rowsByProviderDate = new Map<string, RipplingTimecardRow>();
  for (const row of inspection.rows) {
    rowsByProviderDate.set(`${row.providerEmail.toLowerCase()}|${row.date}`, row);
  }

  const actions: WritePlanAction[] = counts.counts.map((count) => {
    const row = rowsByProviderDate.get(`${count.providerEmail.toLowerCase()}|${count.date}`);
    if (!row) {
      return {
        action: "needs_human_review",
        providerEmail: count.providerEmail,
        providerName: count.providerName,
        date: count.date,
        expectedInputs: count.inputs,
        reason: "No matching Rippling shift row exists; automation must not create a new time entry.",
      };
    }

    const currentInputs = normalizeInputs(row.shiftInputs);
    const matches = inputsEqual(count.inputs, currentInputs);
    if (matches) {
      if (!row.approved && !row.paid) {
        if (!row.editable.approve) {
          return {
            action: "needs_human_review",
            providerEmail: count.providerEmail,
            providerName: count.providerName,
            date: count.date,
            rowId: row.rowId,
            expectedInputs: count.inputs,
            currentInputs,
            approved: row.approved,
            paid: row.paid,
            reason: "Row already contains the expected shift inputs but the approve control is unavailable.",
          };
        }
        return {
          action: "enter_inputs_and_approve",
          providerEmail: count.providerEmail,
          providerName: count.providerName,
          date: count.date,
          rowId: row.rowId,
          expectedInputs: count.inputs,
          currentInputs,
          approved: row.approved,
          paid: row.paid,
          reason: "Row already contains the expected shift inputs and still needs approval.",
        };
      }
      return {
        action: "verify_only",
        providerEmail: count.providerEmail,
        providerName: count.providerName,
        date: count.date,
        rowId: row.rowId,
        expectedInputs: count.inputs,
        currentInputs,
        approved: row.approved,
        paid: row.paid,
        reason: row.paid
          ? "Paid row already contains the expected shift inputs."
          : row.approved
            ? "Approved row already contains the expected shift inputs."
            : "Row already contains the expected shift inputs.",
      };
    }

    if (row.locked || !row.editable.inputs) {
      return {
        action: "needs_human_review",
        providerEmail: count.providerEmail,
        providerName: count.providerName,
        date: count.date,
        rowId: row.rowId,
        expectedInputs: count.inputs,
        currentInputs,
        approved: row.approved,
        paid: row.paid,
        reason: "Matching Rippling row exists but inputs are locked or not editable.",
      };
    }

    if (row.paid || row.approved) {
      const reversible = row.paid
        ? row.editable.unmarkPaid && row.editable.markPaid
        : row.editable.unapprove && row.editable.approve;
      return {
        action: reversible ? "update_paid_or_approved_row" : "needs_human_review",
        providerEmail: count.providerEmail,
        providerName: count.providerName,
        date: count.date,
        rowId: row.rowId,
        expectedInputs: count.inputs,
        currentInputs,
        approved: row.approved,
        paid: row.paid,
        reversibleStatusChangeRequired: reversible,
        reason: reversible
          ? "Existing paid/approved row differs; reversible status controls are exposed."
          : "Existing paid/approved row differs but reversible status controls are not exposed.",
      };
    }

    if (!row.editable.approve) {
      return {
        action: "needs_human_review",
        providerEmail: count.providerEmail,
        providerName: count.providerName,
        date: count.date,
        rowId: row.rowId,
        expectedInputs: count.inputs,
        currentInputs,
        approved: row.approved,
        paid: row.paid,
        reason: "Inputs are editable but the approve control is unavailable.",
      };
    }

    return {
      action: "enter_inputs_and_approve",
      providerEmail: count.providerEmail,
      providerName: count.providerName,
      date: count.date,
      rowId: row.rowId,
      expectedInputs: count.inputs,
      currentInputs,
      approved: row.approved,
      paid: row.paid,
      reason: "Editable unapproved row with mismatched or missing shift inputs.",
    };
  });

  return {
    schemaVersion: 1,
    generatedAt: new Date().toISOString(),
    period: counts.period,
    actions,
    summary: summarizeActions(actions),
    blockingIssues,
  };
}

export function verifyWritePlan(plan: WritePlan, verification: VerificationCheckpoint): VerificationCheckpoint {
  const rowsByProviderDate = new Map<string, VerificationCheckpoint["rows"][number]>();
  for (const row of verification.rows) {
    rowsByProviderDate.set(`${row.providerEmail.toLowerCase()}|${row.date}`, row);
  }

  const mismatches: VerificationMismatch[] = [];
  for (const action of plan.actions) {
    if (!["enter_inputs_and_approve", "update_paid_or_approved_row", "verify_only"].includes(action.action)) {
      continue;
    }
    const row = rowsByProviderDate.get(`${action.providerEmail.toLowerCase()}|${action.date}`);
    if (!row) {
      mismatches.push({
        providerEmail: action.providerEmail,
        date: action.date,
        rowId: action.rowId,
        reason: "Verification did not include the row.",
      });
      continue;
    }
    if (!inputsEqual(action.expectedInputs, normalizeInputs(row.savedInputs))) {
      mismatches.push({
        providerEmail: action.providerEmail,
        date: action.date,
        rowId: action.rowId,
        reason: "Saved shift inputs do not match the write plan.",
      });
    }
    if (action.action !== "verify_only" && !row.approved) {
      mismatches.push({
        providerEmail: action.providerEmail,
        date: action.date,
        rowId: action.rowId,
        reason: "Row was not approved after write.",
      });
    }
    if (action.action === "verify_only") {
      if (action.approved !== undefined && row.approved !== action.approved) {
        mismatches.push({
          providerEmail: action.providerEmail,
          date: action.date,
          rowId: action.rowId,
          reason: "Row approval status changed before verification.",
        });
      }
      if (action.paid !== undefined && row.paid !== action.paid) {
        mismatches.push({
          providerEmail: action.providerEmail,
          date: action.date,
          rowId: action.rowId,
          reason: "Row paid status changed before verification.",
        });
      }
    }
  }

  return {
    ...verification,
    mismatches,
  };
}

export function renderRunReport(args: {
  counts: AmdCountsCheckpoint;
  plan?: WritePlan;
  verification?: VerificationCheckpoint;
  inspection?: RipplingInspection;
}): string {
  const { counts, plan, verification, inspection } = args;
  const lines: string[] = [];
  lines.push(`# Shift Approval Run Report`);
  lines.push("");
  lines.push(`Pay period: ${counts.period.start} to ${counts.period.end}`);
  lines.push(`Generated: ${new Date().toISOString()}`);
  lines.push("");
  lines.push("## Encounter Summary");
  lines.push(`Provider/date rows: ${counts.totals.providerDates}`);
  lines.push(`New patient appointments: ${counts.totals.newPatientAppointments}`);
  lines.push(`Existing patient appointments: ${counts.totals.existingPatientAppointments}`);
  lines.push(`Billable no-show appointments: ${counts.totals.billableNoShowAppointments}`);
  lines.push(`AMD visits processed: ${counts.totals.visitsProcessed}`);
  lines.push(`AMD visits skipped: ${counts.totals.visitsSkipped}`);
  lines.push("");

  if (plan) {
    lines.push("## Write Plan");
    lines.push(`Enter and approve: ${plan.summary.enterAndApprove}`);
    lines.push(`Verify only: ${plan.summary.verifyOnly}`);
    lines.push(`Update paid/approved rows: ${plan.summary.updatePaidOrApproved}`);
    lines.push(`Skipped: ${plan.summary.skipped}`);
    lines.push(`Needs human review: ${plan.summary.needsHumanReview}`);
    lines.push("");

    const skipped = plan.actions.filter((action) => action.action === "skip" || action.action === "needs_human_review");
    if (skipped.length > 0) {
      lines.push("## Skipped Or Human Review");
      for (const action of skipped) {
        lines.push(`- ${action.providerName} ${action.date}: ${action.reason}`);
      }
      lines.push("");
    }
  }

  if (inspection?.deepRajLunch?.length) {
    lines.push("## DeepRaj Lunch");
    for (const result of inspection.deepRajLunch) {
      lines.push(`- ${result.employee}: ${result.status} - ${result.details}`);
    }
    lines.push("");
  }

  const blockingIssues = [...counts.blockingIssues, ...(plan?.blockingIssues ?? [])];
  if (blockingIssues.length > 0) {
    lines.push("## Blocking Issues");
    for (const issue of dedupeIssues(blockingIssues)) {
      lines.push(`- ${issue.code}: ${issue.message}`);
    }
    lines.push("");
  }

  if (verification) {
    lines.push("## Verification");
    lines.push(`Rows verified: ${verification.rows.length}`);
    lines.push(`Mismatches: ${verification.mismatches?.length ?? 0}`);
    lines.push("");
  }

  lines.push("No payroll submitted.");
  return lines.join("\n");
}

export function defaultRunDir(period: { start: string; end: string }): string {
  return `/tmp/payroll-runs/${period.start}_${period.end}`;
}

export function readJsonFile<T>(path: string): T {
  return JSON.parse(readFileSync(path, "utf-8")) as T;
}

export function readCheckpointFile<T extends { schemaVersion?: number }>(path: string): T {
  const checkpoint = readJsonFile<T>(path);
  if (checkpoint.schemaVersion !== 1) {
    throw new Error(`Invalid checkpoint schemaVersion in ${path}: expected 1`);
  }
  return checkpoint;
}

export function writeJsonFile(path: string, data: unknown): void {
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
  chmodSync(dirname(path), 0o700);
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
}

export function writeRunArtifacts(runDir: string, artifacts: Partial<{
  amdCounts: AmdCountsCheckpoint;
  ripplingInspection: RipplingInspection;
  writePlan: WritePlan;
  verification: VerificationCheckpoint;
  runReport: string;
}>): void {
  mkdirSync(runDir, { recursive: true, mode: 0o700 });
  chmodSync(runDir, 0o700);
  if (artifacts.amdCounts) writeJsonFile(join(runDir, "amd-counts.json"), artifacts.amdCounts);
  if (artifacts.ripplingInspection) writeJsonFile(join(runDir, "rippling-inspection.json"), artifacts.ripplingInspection);
  if (artifacts.writePlan) writeJsonFile(join(runDir, "write-plan.json"), artifacts.writePlan);
  if (artifacts.verification) writeJsonFile(join(runDir, "verification.json"), artifacts.verification);
  if (artifacts.runReport) writeFileSync(join(runDir, "run-report.md"), artifacts.runReport);
}

export function loadMapping(configPath?: string): PayMappingConfig {
  return loadConfig(configPath);
}

interface ClassifiedInputPayType {
  ripplingName: string;
  inputKey: keyof EncounterInputs;
  role: DailyEncounterCount["role"];
}

function classifyInputPayTypes(appointmentTypeId: number, apptStatus: number, cfg: PayMappingConfig): ClassifiedInputPayType[] {
  const isNoShow = cfg.appt_status.noshow.includes(apptStatus);
  const isCompleted = cfg.appt_status.completed.includes(apptStatus);
  if (!isNoShow && !isCompleted) return [];

  const types = cfg.pay_types.filter((pt) => pt.amd_type_ids.includes(appointmentTypeId));
  const applicable = isNoShow
    ? types.filter((pt) => pt.category.endsWith("noshow"))
    : types.filter((pt) => !pt.category.endsWith("noshow") && !pt.cpt_codes);

  return applicable
    .map(inputMappingForPayType)
    .filter((mapped): mapped is ClassifiedInputPayType => mapped !== null);
}

function inputMappingForPayType(payType: PayTypeMapping): ClassifiedInputPayType | null {
  const role: DailyEncounterCount["role"] = payType.category.startsWith("therapy_") ? "therapy" : "psych";
  switch (payType.category) {
    case "psych_new":
    case "therapy_new":
      return { ripplingName: payType.rippling_name, inputKey: "newPatientAppointments", role };
    case "psych_fu":
    case "therapy_fu":
      return { ripplingName: payType.rippling_name, inputKey: "existingPatientAppointments", role };
    case "psych_noshow":
    case "therapy_noshow":
      return { ripplingName: payType.rippling_name, inputKey: "billableNoShowAppointments", role };
    default:
      return null;
  }
}

function hasBillableNoShowEvidence(visit: AmdVisitInput): boolean {
  if (visit.billableNoShow === true) return true;
  const text = [visit.chargeNote, visit.cancellationNote, visit.notes].filter(Boolean).join(" ").toLowerCase();
  return /\b(billable no[- ]show|billable late cancellation|no[- ]show charge|late cancellation charge|charge note|charge patient|patient charged)\b/.test(text);
}

function newDailyCount(provider: ProviderRate, date: string, role: DailyEncounterCount["role"]): DailyEncounterCount {
  return {
    providerEmail: provider.rippling_email,
    providerName: provider.amd_column_heading,
    amdColumnHeading: provider.amd_column_heading,
    date,
    role,
    inputs: { ...ZERO_INPUTS },
    sourceVisitIds: [],
  };
}

function normalizeDate(value: string): string | null {
  if (!value) return null;
  const match = value.match(/^(\d{4}-\d{2}-\d{2})/);
  if (match) return match[1];
  return null;
}

function normalizeInputs(inputs: Partial<EncounterInputs> = {}): EncounterInputs {
  return {
    newPatientAppointments: Number(inputs.newPatientAppointments ?? 0),
    existingPatientAppointments: Number(inputs.existingPatientAppointments ?? 0),
    billableNoShowAppointments: Number(inputs.billableNoShowAppointments ?? 0),
  };
}

function inputsEqual(left: EncounterInputs, right: Partial<EncounterInputs>): boolean {
  const normalizedRight = normalizeInputs(right);
  return left.newPatientAppointments === normalizedRight.newPatientAppointments
    && left.existingPatientAppointments === normalizedRight.existingPatientAppointments
    && left.billableNoShowAppointments === normalizedRight.billableNoShowAppointments;
}

function summarizeActions(actions: WritePlanAction[]): WritePlan["summary"] {
  return {
    enterAndApprove: actions.filter((a) => a.action === "enter_inputs_and_approve").length,
    verifyOnly: actions.filter((a) => a.action === "verify_only").length,
    updatePaidOrApproved: actions.filter((a) => a.action === "update_paid_or_approved_row").length,
    skipped: actions.filter((a) => a.action === "skip").length,
    needsHumanReview: actions.filter((a) => a.action === "needs_human_review").length,
  };
}

function compareProviderDate(a: DailyEncounterCount, b: DailyEncounterCount): number {
  return a.providerName.localeCompare(b.providerName) || a.date.localeCompare(b.date);
}

function dedupeIssues(issues: BlockingIssue[]): BlockingIssue[] {
  const seen = new Set<string>();
  return issues.filter((issue) => {
    const key = [
      issue.code,
      issue.date ?? "",
      issue.provider ?? "",
      issue.appointmentType ?? "",
      issue.appointmentTypeId ?? "",
      issue.message,
    ].join("|");
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}
