/**
 * Processor for "cca-eligibility-check" jobs.
 *
 * CCA (Commonwealth Care Alliance) uses ScionDental portal.
 * No OTP required — simple username/password persistent session.
 *
 * Flow:
 *   1. Start a session on the Python agent (POST /cca-eligibility)
 *   2. Emit selenium:cca_session_started → frontend stores session_id
 *   3. Poll agent status until completed/error (no OTP handling needed)
 *   4. On completion: decode pdfBase64, save PDF, create/update patient, update status
 *   5. Return { pdfFileId, pdfFilename, patientUpdateStatus, pdfUploadStatus }
 *
 * CCA result returns pdfBase64 (base64-encoded PDF), same as DeltaIns.
 */
import { storage } from "../../storage";
import {
  forwardToSeleniumCCAEligibilityAgent,
  getSeleniumCCASessionStatus,
} from "../../services/seleniumCCAEligibilityClient";
import { splitName, createOrUpdatePatientByInsuranceId } from "./_shared";
import { io } from "../../socket";

// ─── Helpers ────────────────────────────────────────────────────────────────

function now() {
  return new Date().toISOString();
}

function log(tag: string, msg: string, ctx?: any) {
  console.log(`${now()} [${tag}] ${msg}`, ctx ?? "");
}

function emitToSocket(socketId: string | undefined, event: string, payload: any) {
  if (!socketId || !io) return;
  try {
    const socket = io.sockets.sockets.get(socketId);
    if (socket) {
      socket.emit(event, payload);
      log("cca-processor", `emitted ${event}`, { socketId });
    }
  } catch (err: any) {
    log("cca-processor", `emit failed for ${event}`, { err: err?.message });
  }
}

// ─── Types ───────────────────────────────────────────────────────────────────

export interface CCAEligibilityProcessorInput {
  enrichedPayload: any;
  userId: number;
  insuranceId: string;
  formFirstName?: string;
  formLastName?: string;
  formDob?: string;
  socketId?: string;
}

export interface CCAEligibilityProcessorResult {
  patientUpdateStatus?: string;
  pdfUploadStatus?: string;
  pdfFileId?: number | null;
  pdfFilename?: string | null;
}

// ─── Core DB processing ───────────────────────────────────────────────────────

async function processCCAResult(
  userId: number,
  insuranceId: string,
  formFirstName: string | undefined,
  formLastName: string | undefined,
  formDob: string | undefined,
  seleniumResult: any
): Promise<CCAEligibilityProcessorResult> {
  const output: CCAEligibilityProcessorResult = {};
  let createdPdfFileId: number | null = null;

  try {
    // 1) Resolve patient name
    const rawName =
      typeof seleniumResult?.patientName === "string"
        ? seleniumResult.patientName.trim()
        : null;

    const { firstName, lastName } = rawName
      ? splitName(rawName)
      : { firstName: formFirstName ?? "", lastName: formLastName ?? "" };

    // 2) Create / update patient
    await createOrUpdatePatientByInsuranceId({
      insuranceId,
      firstName,
      lastName,
      dob: formDob,
      userId,
    });

    // 3) Fetch patient
    const patient = await storage.getPatientByInsuranceId(insuranceId);
    if (!patient?.id) {
      output.patientUpdateStatus = "Patient not found; no update performed";
      return output;
    }

    // 4) Determine eligibility status
    // Python returns "Eligible" / "Not Eligible" / "Unknown"
    const eligRaw: string = seleniumResult?.eligibility ?? "";
    const eligLower = eligRaw.toLowerCase();
    const newStatus =
      eligLower === "eligible" || eligLower === "active" || eligLower === "y"
        ? "ACTIVE"
        : "INACTIVE";

    // Use insurerName from result if available, fall back to default
    const insuranceProvider =
      typeof seleniumResult?.insurerName === "string" && seleniumResult.insurerName.trim()
        ? seleniumResult.insurerName.trim()
        : "Commonwealth Care Alliance";

    await storage.updatePatient(patient.id, {
      status: newStatus,
      insuranceProvider,
    });
    output.patientUpdateStatus = `Patient status updated to ${newStatus}`;

    // 5) Decode pdfBase64 → Buffer
    const pdfBase64: string = seleniumResult?.pdfBase64 ?? "";
    let pdfBuffer: Buffer | null = null;
    let pdfFilename: string | null = null;

    if (pdfBase64) {
      try {
        pdfBuffer = Buffer.from(pdfBase64, "base64");
        pdfFilename = `cca_eligibility_${insuranceId}_${Date.now()}.pdf`;
        log("cca-processor", "decoded pdfBase64", { bytes: pdfBuffer.length });
      } catch (e: any) {
        output.pdfUploadStatus = `Failed to decode PDF base64: ${e.message}`;
      }
    } else {
      output.pdfUploadStatus = "No PDF data returned from Selenium.";
    }

    // 6) Save PDF to patient document group
    if (pdfBuffer && pdfFilename) {
      const groupTitleKey = "ELIGIBILITY_STATUS";
      const groupTitle = "Eligibility Status";

      let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
      if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
      if (!group?.id) throw new Error("PDF group creation failed");

      const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
      if (created && typeof created === "object" && "id" in created) {
        createdPdfFileId = Number(created.id);
      }
      output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
      output.pdfFilename = pdfFilename;
    }

    output.pdfFileId = createdPdfFileId;
    return output;
  } catch (err: any) {
    return {
      ...output,
      pdfUploadStatus:
        output.pdfUploadStatus ?? `Processing failed: ${err?.message ?? String(err)}`,
      pdfFileId: createdPdfFileId,
    };
  }
}

// ─── Polling loop ────────────────────────────────────────────────────────────

async function pollUntilDone(
  sessionId: string,
  pollTimeoutMs = 5 * 60 * 1000
): Promise<any> {
  const maxAttempts = 600;
  const pollIntervalMs = 500;
  const maxTransientErrors = 12;
  const noProgressLimit = 120;

  let transientErrors = 0;
  let consecutiveNoProgress = 0;
  let lastStatus: string | null = null;
  const deadline = Date.now() + pollTimeoutMs;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    if (Date.now() > deadline) {
      throw new Error(
        `CCA polling timeout (${Math.round(pollTimeoutMs / 1000)}s) for session ${sessionId}`
      );
    }

    try {
      const st = await getSeleniumCCASessionStatus(sessionId);
      const status: string = st?.status ?? "unknown";

      log("cca-processor", `poll attempt=${attempt}`, { sessionId, status });

      transientErrors = 0;

      const isTerminal =
        status === "completed" || status === "error" || status === "not_found";
      if (status === lastStatus && !isTerminal) {
        consecutiveNoProgress++;
      } else {
        consecutiveNoProgress = 0;
      }

      lastStatus = status;

      if (consecutiveNoProgress >= noProgressLimit) {
        throw new Error(
          `No progress from Python agent (status="${status}") after ${consecutiveNoProgress} polls`
        );
      }

      if (status === "completed") {
        log("cca-processor", "session completed", { sessionId });
        return st.result;
      }

      if (status === "error" || status === "not_found") {
        throw new Error(st?.message || `CCA session ended with status: ${status}`);
      }

      await new Promise((r) => setTimeout(r, pollIntervalMs));
    } catch (err: any) {
      const isTerminal =
        err?.response?.status === 404 ||
        (typeof err?.message === "string" &&
          (err.message.includes("not_found") ||
            err.message.includes("polling timeout") ||
            err.message.includes("No progress")));

      if (isTerminal) throw err;

      transientErrors++;
      if (transientErrors > maxTransientErrors) {
        throw new Error(
          `Too many transient network errors polling CCA session ${sessionId}`
        );
      }
      const backoff = Math.min(30_000, 500 * Math.pow(2, transientErrors - 1));
      log("cca-processor", `transient error #${transientErrors}, backoff ${backoff}ms`, {
        err: err?.message,
      });
      await new Promise((r) => setTimeout(r, backoff));
    }
  }

  throw new Error(`CCA polling exhausted all attempts for session ${sessionId}`);
}

// ─── Main processor entry point ───────────────────────────────────────────────

export async function runCCAEligibilityProcessor(
  input: CCAEligibilityProcessorInput,
  jobId: string
): Promise<CCAEligibilityProcessorResult> {
  const {
    enrichedPayload,
    userId,
    insuranceId,
    formFirstName,
    formLastName,
    formDob,
    socketId,
  } = input;

  // 1) Tell Python agent to start a CCA session
  log("cca-processor", "starting Python agent session", { insuranceId });
  const agentResp = await forwardToSeleniumCCAEligibilityAgent(enrichedPayload);

  if (!agentResp?.session_id) {
    throw new Error("Python agent did not return a session_id for CCA eligibility");
  }

  const sessionId = agentResp.session_id as string;
  log("cca-processor", "got session_id", { sessionId });

  // 2) Emit session started so frontend can track progress
  emitToSocket(socketId, "selenium:cca_session_started", {
    session_id: sessionId,
    jobId,
  });

  // 3) Poll until done (no OTP required for CCA)
  const seleniumResult = await pollUntilDone(sessionId);

  if (!seleniumResult || seleniumResult.status === "error") {
    throw new Error(seleniumResult?.message ?? "CCA session returned an error result");
  }

  // 4) Process DB writes and PDF upload
  log("cca-processor", "processing DB result", { insuranceId });
  const result = await processCCAResult(
    userId,
    insuranceId,
    formFirstName,
    formLastName,
    formDob,
    seleniumResult
  );

  log("cca-processor", "done", { result });
  return result;
}
