/**
 * Processor for "unitedsco-eligibility-check" jobs (Tufts SCO / UnitedHealthcare MA).
 *
 * Same persistent-session flow as DDMA:
 *   1. Start a session on the Python agent (POST /unitedsco-eligibility)
 *   2. Emit selenium:unitedsco_session_started → frontend stores session_id for OTP
 *   3. Poll agent status, emitting selenium:otp_required when OTP is needed
 *   4. On completion: save PDF (file path), create/update patient, update status
 *   5. Return { pdfFileId, pdfFilename, patientUpdateStatus, pdfUploadStatus }
 */
import fs from "fs/promises";
import fsSync from "fs";
import path from "path";
import { storage } from "../../storage";
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
import {
  forwardToSeleniumUnitedSCOEligibilityAgent,
  getSeleniumUnitedSCOSessionStatus,
} from "../../services/seleniumUnitedSCOEligibilityClient";
import { splitName, createOrUpdatePatientByInsuranceId, imageToPdfBuffer } 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("unitedsco-processor", `emitted ${event}`, { socketId });
    }
  } catch (err: any) {
    log("unitedsco-processor", `emit failed for ${event}`, { err: err?.message });
  }
}

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

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

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

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

async function processUnitedSCOResult(
  userId: number,
  insuranceId: string,
  formFirstName: string | undefined,
  formLastName: string | undefined,
  formDob: string | undefined,
  seleniumResult: any
): Promise<UnitedSCOEligibilityProcessorResult> {
  const output: UnitedSCOEligibilityProcessorResult = {};
  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
    const eligStatus = (seleniumResult?.eligibility ?? "").toLowerCase();
    const newStatus =
      eligStatus === "active" || eligStatus === "y" ? "ACTIVE" :
      eligStatus === "inactive" ? "INACTIVE" : null;

    if (newStatus) {
      await storage.updatePatient(patient.id, {
        status: newStatus,
        insuranceProvider: "United Healthcare SCO",
      });
      output.patientUpdateStatus = `Patient status updated to ${newStatus}`;
    } else {
      // Unknown eligibility — still update insuranceProvider but leave status unchanged
      await storage.updatePatient(patient.id, { insuranceProvider: "United Healthcare SCO" });
      output.patientUpdateStatus = `Eligibility unknown — status not changed`;
    }

    // 5) Resolve PDF buffer from file path (same as DDMA)
    let pdfBuffer: Buffer | null = null;
    let pdfFilename: string | null = null;

    const pdfPath: string | null =
      seleniumResult?.pdf_path ?? seleniumResult?.ss_path ?? null;

    if (pdfPath && fsSync.existsSync(pdfPath)) {
      if (pdfPath.endsWith(".pdf")) {
        try {
          pdfBuffer = await fs.readFile(pdfPath);
          pdfFilename = path.basename(pdfPath);
          log("unitedsco-processor", "read PDF directly", { pdfPath });
        } catch (e: any) {
          output.pdfUploadStatus = `Failed to read PDF: ${e.message}`;
        }
      } else if (
        pdfPath.endsWith(".png") ||
        pdfPath.endsWith(".jpg") ||
        pdfPath.endsWith(".jpeg")
      ) {
        try {
          pdfBuffer = await imageToPdfBuffer(pdfPath);
          pdfFilename = `unitedsco_eligibility_${insuranceId}_${Date.now()}.pdf`;
          log("unitedsco-processor", "converted screenshot to PDF", { pdfPath });
        } catch (e: any) {
          output.pdfUploadStatus = `Failed to convert screenshot to PDF: ${e.message}`;
        }
      }
    } else {
      output.pdfUploadStatus = "No valid file path from Selenium; nothing uploaded.";
    }

    // 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,
    };
  } finally {
    const cleanupPath = seleniumResult?.pdf_path ?? seleniumResult?.ss_path ?? null;
    if (cleanupPath) {
      try {
        await emptyFolderContainingFile(cleanupPath);
      } catch (e) {
        log("unitedsco-processor", "cleanup failed", { cleanupPath });
      }
    }
  }
}

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

async function pollUntilDone(
  sessionId: string,
  socketId: string | undefined,
  jobId: 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(
        `UnitedSCO polling timeout (${Math.round(pollTimeoutMs / 1000)}s) for session ${sessionId}`
      );
    }

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

      log("unitedsco-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 === "waiting_for_otp") {
        emitToSocket(socketId, "selenium:otp_required", {
          session_id: sessionId,
          jobId,
          message: "OTP required. Please enter the verification code.",
        });
        await new Promise((r) => setTimeout(r, pollIntervalMs));
        continue;
      }

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

      if (status === "error" || status === "not_found") {
        throw new Error(st?.message || `UnitedSCO 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 UnitedSCO session ${sessionId}`
        );
      }
      const backoff = Math.min(30_000, 500 * Math.pow(2, transientErrors - 1));
      log("unitedsco-processor", `transient error #${transientErrors}, backoff ${backoff}ms`, {
        err: err?.message,
      });
      await new Promise((r) => setTimeout(r, backoff));
    }
  }

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

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

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

  log("unitedsco-processor", "starting Python agent session", { insuranceId });
  const agentResp = await forwardToSeleniumUnitedSCOEligibilityAgent(enrichedPayload);

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

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

  emitToSocket(socketId, "selenium:unitedsco_session_started", {
    session_id: sessionId,
    jobId,
  });

  const seleniumResult = await pollUntilDone(sessionId, socketId, jobId);

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

  log("unitedsco-processor", "processing DB result", { insuranceId });
  const result = await processUnitedSCOResult(
    userId,
    insuranceId,
    formFirstName,
    formLastName,
    formDob,
    seleniumResult
  );

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