import fs from "fs/promises";
import fsSync from "fs";
import path from "path";
import { storage } from "../../storage";
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
import {
  forwardToSeleniumDentaQuestEligibilityAgent,
  getSeleniumDentaQuestSessionStatus,
} from "../../services/seleniumDentaQuestEligibilityClient";
import { splitName, createOrUpdatePatientByInsuranceId, imageToPdfBuffer } from "./_shared";
import { io } from "../../socket";

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("dentaquest-processor", `emitted ${event}`, { socketId });
    }
  } catch (err: any) {
    log("dentaquest-processor", `emit failed for ${event}`, { err: err?.message });
  }
}

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

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

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

  try {
    const rawName =
      typeof seleniumResult?.patientName === "string"
        ? seleniumResult.patientName.trim()
        : null;

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

    await createOrUpdatePatientByInsuranceId({
      insuranceId,
      firstName,
      lastName,
      dob: formDob,
      userId,
    });

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

    const eligStatus = (seleniumResult?.eligibility ?? "").toLowerCase();
    const newStatus =
      eligStatus === "active" || eligStatus === "y"
        ? "ACTIVE"
        : eligStatus.includes("plan not accepted") || eligStatus.includes("plan_not_accepted")
        ? "PLAN_NOT_ACCEPTED"
        : "INACTIVE";

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

    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("dentaquest-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 = `dentaquest_eligibility_${insuranceId}_${Date.now()}.pdf`;
          log("dentaquest-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.";
    }

    if (pdfBuffer && pdfFilename) {
      const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
      const cloudFile = await storage.savePdfToCloudStorage(
        userId,
        patient.id,
        patientName,
        "Eligibility",
        pdfFilename,
        pdfBuffer
      );
      createdPdfFileId = `cloud:${cloudFile.id}`;
      output.pdfUploadStatus = "PDF saved to Eligibility folder";
      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("dentaquest-processor", "cleanup failed", { cleanupPath });
      }
    }
  }
}

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(
        `DentaQuest polling timeout (${Math.round(pollTimeoutMs / 1000)}s) for session ${sessionId}`
      );
    }

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

      log("dentaquest-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("dentaquest-processor", "session completed", { sessionId });
        return st.result;
      }

      if (status === "error" || status === "not_found") {
        const terminalErr: any = new Error(st?.message || `DentaQuest session ended with status: ${status}`);
        terminalErr.terminal = true;
        throw terminalErr;
      }

      await new Promise((r) => setTimeout(r, pollIntervalMs));
    } catch (err: any) {
      const isTerminal =
        err?.terminal === true ||
        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 DentaQuest session ${sessionId}`
        );
      }
      const backoff = Math.min(30_000, 500 * Math.pow(2, transientErrors - 1));
      log("dentaquest-processor", `transient error #${transientErrors}, backoff ${backoff}ms`, {
        err: err?.message,
      });
      await new Promise((r) => setTimeout(r, backoff));
    }
  }

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

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

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

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

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

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

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

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

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

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