import type { AdvancedMdReadGateway } from "./advancedmd-gateway.ts";
import type { PolicyToolResult, SchedulingToolPort } from "./policy-proxy.ts";
import { verifyPatientIdentity } from "./scheduling-policy.ts";
import type { ApprovalToken, NextAppointmentSummary, VerifiedPatient } from "./types.ts";

export interface ReadOnlyAdvancedMdSchedulingPortOptions {
  reads: AdvancedMdReadGateway;
  callerNumber?: string;
  lookupTimeoutMs?: number;
}

export class ReadOnlyAdvancedMdSchedulingPort implements SchedulingToolPort {
  private verifiedPatient?: VerifiedPatient;

  constructor(private readonly opts: ReadOnlyAdvancedMdSchedulingPortOptions) {}

  async verifyPatientIdentity(args: Record<string, unknown>): Promise<PolicyToolResult> {
    const statedLegalName = requiredString(args.stated_legal_name);
    const statedDob = requiredString(args.stated_dob);
    const callerNumber = requiredString(args.caller_number) ?? this.opts.callerNumber;

    if (!statedLegalName || !statedDob || !callerNumber) {
      return {
        ok: false,
        action: "transfer_to_staff",
        message: "Identity verification requires caller phone, legal name, and DOB.",
      };
    }

    const candidates = await withLookupTimeout(
      this.opts.reads.searchPatients(statedLegalName),
      this.opts.lookupTimeoutMs,
    );
    if (candidates.timedOut) return amdLookupTimeoutResult("patient verification");

    const verification = verifyPatientIdentity({
      callerNumber,
      statedLegalName,
      statedDob,
      candidates: candidates.value,
    });

    if (!verification.ok) {
      return {
        ok: false,
        action: "transfer_to_staff",
        message: `Identity verification failed: ${verification.reason}.`,
        data: { reason: verification.reason },
      };
    }

    this.verifiedPatient = verification.patient;
    const history = await withLookupTimeout(
      this.opts.reads.getAppointmentHistory(verification.patient.patientId),
      this.opts.lookupTimeoutMs,
    );
    if (history.timedOut) return amdLookupTimeoutResult("appointment lookup");

    const nextAppointment = findNextAppointment(history.value);
    const data: Record<string, unknown> = {
      patient_id: verification.patient.patientId,
      legal_name: verification.patient.legalName,
    };
    if (nextAppointment) data.next_appointment = policyAppointmentData(nextAppointment);

    return {
      ok: true,
      action: "continue",
      message: nextAppointment
        ? `Identity verified. Next appointment is ${nextAppointment.spokenDateTime} with ${nextAppointment.providerName}.`
        : "Identity verified, but no future appointment was found in AdvancedMD appointment history.",
      data,
    };
  }

  async findRescheduleOptions(args: Record<string, unknown>): Promise<PolicyToolResult> {
    const patientId = requiredString(args.patient_id);
    const appointmentId = requiredString(args.appointment_id);

    if (!patientId || !appointmentId) {
      return {
        ok: false,
        action: "transfer_to_staff",
        message: "Reschedule lookup requires a verified patient and appointment.",
      };
    }

    if (!this.verifiedPatient || this.verifiedPatient.patientId !== patientId) {
      return {
        ok: false,
        action: "transfer_to_staff",
        message: "The caller is not verified for that patient.",
      };
    }

    const history = await withLookupTimeout(
      this.opts.reads.getAppointmentHistory(patientId),
      this.opts.lookupTimeoutMs,
    );
    if (history.timedOut) return amdLookupTimeoutResult("appointment lookup");

    return {
      ok: true,
      action: "callback_required",
      message:
        "Identity is verified and AdvancedMD appointment history was read, but autonomous slot selection remains disabled until the approved slot-availability mapping is wired.",
      data: { patient_id: patientId, appointment_id: appointmentId },
    };
  }

  async requestReschedule(
    args: Record<string, unknown>,
    approval?: ApprovalToken,
  ): Promise<PolicyToolResult> {
    const patientId = requiredString(args.patient_id);
    if (!this.verifiedPatient || !patientId || patientId !== this.verifiedPatient.patientId) {
      return {
        ok: false,
        action: "transfer_to_staff",
        message:
          "AdvancedMD writes are blocked until this call has a positively identified patient.",
        data: {
          reason: "patient_not_verified",
          patient_id: patientId,
        },
      };
    }

    if (!approval) {
      return {
        ok: false,
        action: "approval_required",
        message:
          "AdvancedMD writes require an approved write policy after patient verification.",
        data: {
          reason: "write_approval_required",
          patient_id: this.verifiedPatient.patientId,
        },
      };
    }

    return {
      ok: false,
      action: "approval_required",
      message:
        "Patient is verified, but AdvancedMD appointment writes are not connected to a narrow write gateway yet.",
      data: {
        reason: "write_gateway_not_configured",
        patient_id: this.verifiedPatient.patientId,
      },
    };
  }

  async createCallback(args: Record<string, unknown>): Promise<PolicyToolResult> {
    return {
      ok: true,
      action: "callback_required",
      message: "Callback should be queued by staff or an approved callback-write gateway.",
      data: { reason: requiredString(args.reason) ?? "caller needs scheduling help" },
    };
  }

  async transferToStaff(args: Record<string, unknown>): Promise<PolicyToolResult> {
    return {
      ok: true,
      action: "transfer_to_staff",
      message: "Transfer caller to staff.",
      data: { reason: requiredString(args.reason) ?? "voice agent escalation" },
    };
  }
}

type AppointmentCandidate = NextAppointmentSummary & { sortTimeMs: number };

export function findNextAppointment(
  history: unknown,
  now = new Date(),
): NextAppointmentSummary | undefined {
  const nowTime = now.getTime();
  return collectAppointmentRows(history)
    .map((row) => appointmentCandidateFromRow(row, now))
    .filter((candidate): candidate is AppointmentCandidate => Boolean(candidate))
    .filter((candidate) => candidate.sortTimeMs >= nowTime)
    .sort((a, b) => a.sortTimeMs - b.sortTimeMs)[0];
}

function policyAppointmentData(appointment: NextAppointmentSummary): Record<string, unknown> {
  return {
    appointment_id: appointment.appointmentId,
    starts_at: appointment.startsAt,
    spoken_date_time: appointment.spokenDateTime,
    provider_name: appointment.providerName,
    status: appointment.status,
  };
}

function appointmentCandidateFromRow(
  row: Record<string, unknown>,
  now: Date,
): AppointmentCandidate | undefined {
  const status = rowStringValue(row, ["apptstatus", "appointmentstatus", "status"]);
  if (status && isCanceledStatus(status)) return undefined;

  const dateValue = rowStringValue(row, [
    "date",
    "visitdate",
    "appointmentdate",
    "apptdate",
    "scheduleddate",
    "datetime",
    "start",
    "startsat",
    "visitstartdatetime",
  ]);
  if (!dateValue) return undefined;

  const timeValue = rowStringValue(row, [
    "time",
    "visittime",
    "appointmenttime",
    "appttime",
    "starttime",
    "visitstarttime",
  ]);
  const sourceDateTime =
    timeValue && !hasClockTime(dateValue) ? `${dateValue} ${timeValue}` : dateValue;
  const parsed = parseLocalAppointmentDate(sourceDateTime);
  if (!parsed) return undefined;

  const providerName =
    rowStringValue(row, [
      "columnheading",
      "provider",
      "providername",
      "providerfullname",
      "clinician",
      "doctor",
      "resource",
      "staff",
      "staffname",
      "heading",
    ]) ?? "your provider";

  return {
    appointmentId: rowStringValue(row, [
      "id",
      "visitid",
      "visituid",
      "appointmentid",
      "apptid",
      "eventid",
      "referenceid",
    ]),
    startsAt: parsed.date.toISOString(),
    spokenDateTime: formatSpokenDateTime(parsed.date, parsed.hasTime, now),
    providerName,
    status,
    sortTimeMs: parsed.date.getTime(),
  };
}

function collectAppointmentRows(value: unknown): Record<string, unknown>[] {
  const rows: Record<string, unknown>[] = [];
  collectRows(value, rows, new Set<object>());
  return rows;
}

function collectRows(
  value: unknown,
  rows: Record<string, unknown>[],
  seen: Set<object>,
): void {
  if (typeof value === "string") {
    const parsed = parseJson(value);
    if (parsed !== undefined) collectRows(parsed, rows, seen);
    return;
  }

  if (Array.isArray(value)) {
    for (const item of value) collectRows(item, rows, seen);
    return;
  }

  if (!isRecord(value) || seen.has(value)) return;
  seen.add(value);

  if (looksLikeAppointmentRow(value)) rows.push(value);
  for (const child of Object.values(value)) {
    if (Array.isArray(child) || isRecord(child) || typeof child === "string") {
      collectRows(child, rows, seen);
    }
  }
}

function looksLikeAppointmentRow(row: Record<string, unknown>): boolean {
  return Boolean(
    rowStringValue(row, [
      "date",
      "visitdate",
      "appointmentdate",
      "apptdate",
      "scheduleddate",
      "datetime",
      "start",
      "startsat",
      "visitstartdatetime",
    ]),
  );
}

function rowStringValue(row: Record<string, unknown>, keys: string[]): string | undefined {
  const wanted = new Set(keys);
  for (const [key, value] of Object.entries(row)) {
    if (!wanted.has(normalizeKey(key))) continue;
    const text = stringValue(value);
    if (text) return text;
  }
  return undefined;
}

function normalizeKey(key: string): string {
  return key.toLowerCase().replace(/[^a-z0-9]/g, "");
}

function stringValue(value: unknown): string | undefined {
  if (typeof value === "string" && value.trim()) return value.trim();
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
  return undefined;
}

function parseJson(value: string): unknown | undefined {
  try {
    return JSON.parse(value);
  } catch {
    return undefined;
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isCanceledStatus(value: string): boolean {
  return /cancel|deleted|no\s*-?\s*show|rescheduled/i.test(value);
}

function hasClockTime(value: string): boolean {
  return /\d{1,2}:\d{2}/.test(value) || /\b(?:am|pm)\b/i.test(value);
}

function parseLocalAppointmentDate(
  value: string,
): { date: Date; hasTime: boolean } | undefined {
  const cleaned = value.replace(/\s+/g, " ").trim();
  const us = cleaned.match(
    /^(\d{1,2})\/(\d{1,2})\/(\d{2,4})(?:\s+(\d{1,2})(?::(\d{2}))?\s*(AM|PM)?)?$/i,
  );
  if (us) {
    const year = normalizeYear(Number(us[3]));
    const month = Number(us[1]) - 1;
    const day = Number(us[2]);
    const time = normalizeTime(us[4], us[5], us[6]);
    const date = new Date(year, month, day, time.hour, time.minute);
    return isValidDate(date) ? { date, hasTime: Boolean(us[4]) } : undefined;
  }

  const iso = cleaned.match(
    /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2})(?::(\d{2}))?(?::\d{2})?)?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/i,
  );
  if (iso) {
    const time = normalizeTime(iso[4], iso[5]);
    const date = new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]), time.hour, time.minute);
    return isValidDate(date) ? { date, hasTime: Boolean(iso[4]) } : undefined;
  }

  const native = new Date(cleaned);
  if (!isValidDate(native)) return undefined;
  return { date: native, hasTime: hasClockTime(cleaned) };
}

function normalizeYear(year: number): number {
  return year < 100 ? 2000 + year : year;
}

function normalizeTime(
  rawHour?: string,
  rawMinute?: string,
  meridiem?: string,
): { hour: number; minute: number } {
  let hour = rawHour ? Number(rawHour) : 0;
  const minute = rawMinute ? Number(rawMinute) : 0;
  const marker = meridiem?.toLowerCase();
  if (marker === "pm" && hour < 12) hour += 12;
  if (marker === "am" && hour === 12) hour = 0;
  return { hour, minute };
}

function isValidDate(date: Date): boolean {
  return Number.isFinite(date.getTime());
}

function formatSpokenDateTime(date: Date, hasTime: boolean, now: Date): string {
  const includeYear = date.getFullYear() !== now.getFullYear();
  const dateText = new Intl.DateTimeFormat("en-US", {
    weekday: "long",
    month: "long",
    day: "numeric",
    ...(includeYear ? { year: "numeric" as const } : {}),
  }).format(date);
  if (!hasTime) return dateText;

  const timeText = new Intl.DateTimeFormat("en-US", {
    hour: "numeric",
    minute: "2-digit",
  }).format(date);
  return `${dateText} at ${timeText}`;
}

function requiredString(value: unknown): string | undefined {
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
}

function amdLookupTimeoutResult(step: string): PolicyToolResult {
  return {
    ok: false,
    action: "callback_required",
    message: `AdvancedMD ${step} is taking too long; offer staff transfer or callback.`,
    data: { reason: "amd_lookup_timeout", step },
  };
}

async function withLookupTimeout<T>(
  promise: Promise<T>,
  timeoutMs = 6000,
): Promise<{ timedOut: false; value: T } | { timedOut: true }> {
  let timeout: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      promise.then((value) => ({ timedOut: false as const, value })),
      new Promise<{ timedOut: true }>((resolve) => {
        timeout = setTimeout(() => resolve({ timedOut: true }), timeoutMs);
      }),
    ]);
  } finally {
    if (timeout) clearTimeout(timeout);
  }
}
