/**
 * AMD New Patient Notifier
 *
 * Cloudflare Worker that polls AdvancedMD every 5 minutes for new patients
 * and sends a Teams notification via the Teams Worker API.
 */

interface Env {
  STATE: KVNamespace;
  AMD_OFFICE_KEY: string;
  AMD_USERNAME: string;
  AMD_PASSWORD: string;
  AMD_APPNAME: string;
  TEAMS_WORKER_URL: string;
  TEAMS_API_KEY: string;
  TEAMS_CHAT_ID: string;
}

const PARTNER_LOGIN_URL =
  "https://partnerlogin.advancedmd.com/practicemanager/xmlrpc/processrequest.aspx";

// -------------------------------------------------------------------------
// XML helpers
// -------------------------------------------------------------------------

function escapeXml(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function nowStr(): string {
  const d = new Date();
  const pad = (n: number) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

function extractAttr(xml: string, tag: string, attr: string): string | null {
  const re = new RegExp(`<${tag}[^>]*?\\s${attr}="([^"]*)"`, "i");
  const m = xml.match(re);
  return m ? m[1] : null;
}

function extractElementText(xml: string, tag: string): string | null {
  const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i");
  const m = xml.match(re);
  return m ? m[1].trim() : null;
}

// -------------------------------------------------------------------------
// AMD API
// -------------------------------------------------------------------------

let cachedSession: { webServer: string; token: string } | null = null;

async function amdLogin(env: Env): Promise<{ webServer: string; token: string }> {
  const loginXml =
    `<ppmdmsg action="login" class="login" msgtime="${escapeXml(nowStr())}" ` +
    `username="${escapeXml(env.AMD_USERNAME)}" psw="${escapeXml(env.AMD_PASSWORD)}" ` +
    `officecode="${escapeXml(env.AMD_OFFICE_KEY)}" appname="${escapeXml(env.AMD_APPNAME)}"/>`;

  // Step 1: partner login → redirect
  const res1 = await fetch(PARTNER_LOGIN_URL, {
    method: "POST",
    headers: { "Content-Type": "text/xml" },
    body: loginXml,
  });
  const text1 = await res1.text();

  let webServer = extractAttr(text1, "usercontext", "webserver");
  if (!webServer) {
    const desc = text1.match(/https:\/\/[^\s.]+\.advancedmd\.com\/processrequest\/[^\s"<]+/);
    if (desc) webServer = desc[0];
  }
  if (!webServer) throw new Error(`AMD login step 1 failed: ${text1.slice(0, 300)}`);

  // Step 2: resubmit to webserver → token
  const apiUrl = `${webServer}/xmlrpc/processrequest.aspx`;
  const res2 = await fetch(apiUrl, {
    method: "POST",
    headers: { "Content-Type": "text/xml" },
    body: loginXml,
  });
  const text2 = await res2.text();

  const token = extractElementText(text2, "usercontext");
  if (!token) throw new Error(`AMD login step 2 failed: ${text2.slice(0, 300)}`);

  cachedSession = { webServer, token };
  return cachedSession;
}

async function amdApiCall(
  env: Env,
  action: string,
  attrs: string,
  children: string,
): Promise<string> {
  if (!cachedSession) await amdLogin(env);
  const { webServer, token } = cachedSession!;

  const body =
    `<ppmdmsg action="${escapeXml(action)}" class="api" msgtime="${escapeXml(nowStr())}" ${attrs}>` +
    children +
    `</ppmdmsg>`;

  const res = await fetch(`${webServer}/xmlrpc/processrequest.aspx`, {
    method: "POST",
    headers: { "Content-Type": "text/xml", Cookie: `token=${token}` },
    body,
  });
  const text = await res.text();

  // Handle session expiry
  if (
    res.status === 401 ||
    text.toLowerCase().includes("session expired") ||
    text.toLowerCase().includes("not authenticated")
  ) {
    cachedSession = null;
    await amdLogin(env);
    const retry = await fetch(`${cachedSession!.webServer}/xmlrpc/processrequest.aspx`, {
      method: "POST",
      headers: { "Content-Type": "text/xml", Cookie: `token=${cachedSession!.token}` },
      body,
    });
    return retry.text();
  }

  return text;
}

// -------------------------------------------------------------------------
// Patient parsing
// -------------------------------------------------------------------------

interface PatientRecord {
  id: string;
  name: string;
  createdAt: string;
  changedAt: string;
  nextAppointment?: string;
  nextApptProvider?: string;
  nextApptType?: string;
  referringProvider?: string;
}

function parsePatients(xml: string): PatientRecord[] {
  const patients: PatientRecord[] = [];
  const re = /<patient\s[^>]*>/gi;
  let match;
  while ((match = re.exec(xml)) !== null) {
    const tag = match[0];
    const id = extractAttrFromTag(tag, "id") ?? extractAttrFromTag(tag, "patientid") ?? "";
    const name = extractAttrFromTag(tag, "name") ?? "";
    const createdAt = extractAttrFromTag(tag, "createdat") ?? "";
    const changedAt = extractAttrFromTag(tag, "changedat") ?? "";
    if (id && name) {
      patients.push({ id, name, createdAt, changedAt });
    }
  }
  return patients;
}

function extractAttrFromTag(tag: string, attr: string): string | null {
  const re = new RegExp(`${attr}="([^"]*)"`, "i");
  const m = tag.match(re);
  return m ? m[1] : null;
}

// Fetch next appointment for a patient
async function getNextAppointment(
  env: Env,
  patientId: string,
): Promise<{ date: string; provider: string; type: string } | null> {
  try {
    const xml = await amdApiCall(
      env,
      "getpatientvisits",
      `patientid="${escapeXml(patientId)}"`,
      `<visit date="VisitDate" duration="Duration" apptstatus="ApptStatus" columnheading="ColumnHeading"/>`,
    );

    const today = formatDate(new Date());
    const visits: Array<{ date: string; provider: string; type: string; status: string }> = [];
    const visitRe = /<visit\s[^>]*>/gi;
    let m;
    while ((m = visitRe.exec(xml)) !== null) {
      const tag = m[0];
      const date = extractAttrFromTag(tag, "visitdate") ?? extractAttrFromTag(tag, "date") ?? "";
      const provider = extractAttrFromTag(tag, "columnheading") ?? "";
      const type = extractAttrFromTag(tag, "duration") ?? "";
      const status = extractAttrFromTag(tag, "apptstatus") ?? "";
      if (date >= today && status !== "Cancelled") {
        visits.push({ date, provider, type, status });
      }
    }

    visits.sort((a, b) => a.date.localeCompare(b.date));
    return visits[0] ?? null;
  } catch {
    return null;
  }
}

// Fetch referring provider for a patient via demographic lookup
async function getReferringProvider(
  env: Env,
  patientId: string,
): Promise<string | null> {
  try {
    const xml = await amdApiCall(
      env,
      "getpatientdemographic",
      `patientid="${escapeXml(patientId)}"`,
      "",
    );
    // Look for referring provider in the response
    const refProvider =
      extractAttr(xml, "referringprovider", "name") ??
      extractAttr(xml, "patient", "referringprovidername") ??
      extractAttr(xml, "demographic", "referringprovider") ??
      extractAttrFromTag(xml, "referringprovidername");

    return refProvider || null;
  } catch {
    return null;
  }
}

// Enrich patients with appointment and referral data
async function enrichPatients(env: Env, patients: PatientRecord[]): Promise<void> {
  for (const p of patients) {
    const [appt, refProvider] = await Promise.all([
      getNextAppointment(env, p.id),
      getReferringProvider(env, p.id),
    ]);
    if (appt) {
      p.nextAppointment = appt.date;
      p.nextApptProvider = appt.provider;
      p.nextApptType = appt.type;
    }
    if (refProvider) {
      p.referringProvider = refProvider;
    }
  }
}

// -------------------------------------------------------------------------
// Teams notification
// -------------------------------------------------------------------------

async function sendTeamsNotification(env: Env, patients: PatientRecord[]): Promise<void> {
  const lines = patients.map((p) => {
    let line = `- ${p.name} (ID: ${p.id})`;
    if (p.nextAppointment) {
      line += `\n  Next appt: ${p.nextAppointment}`;
      if (p.nextApptProvider) line += ` with ${p.nextApptProvider}`;
    } else {
      line += "\n  No upcoming appointment scheduled";
    }
    if (p.referringProvider) {
      line += `\n  Referring: ${p.referringProvider}`;
    }
    return line;
  });

  const header = patients.length === 1
    ? `New patient added in AMD: ${patients[0].name}`
    : `${patients.length} new patients added in AMD:`;

  const text = `${header}\n${lines.join("\n")}`;

  await fetch(`${env.TEAMS_WORKER_URL}/api/messages/send`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": env.TEAMS_API_KEY,
    },
    body: JSON.stringify({
      chat_id: env.TEAMS_CHAT_ID,
      text,
    }),
  });
}

// -------------------------------------------------------------------------
// Poll logic
// -------------------------------------------------------------------------

async function pollNewPatients(env: Env): Promise<number> {
  // Get last poll date from KV
  const lastPollRaw = await env.STATE.get("lastPollDate");
  const today = new Date();
  const lastPollDate = lastPollRaw ?? formatDate(new Date(today.getTime() - 86400_000));

  // Query AMD for patients modified since last poll date
  const xml = await amdApiCall(
    env,
    "getupdatedpatients",
    `datechanged="${escapeXml(lastPollDate)}"`,
    `<patient name="Name" ssn="SSN" changedat="ChangedAt" createdat="CreatedAt"/>`,
  );

  const patients = parsePatients(xml);

  // Load seen patient IDs
  const seenRaw = await env.STATE.get("seenPatientIds");
  const seenIds: string[] = seenRaw ? JSON.parse(seenRaw) : [];
  const seenSet = new Set(seenIds);

  // Filter to truly new patients (created recently, not just modified)
  const todayStr = formatDate(today);
  const newPatients = patients.filter((p) => {
    if (seenSet.has(p.id)) return false;
    // Only notify for patients created today or since last poll
    if (p.createdAt && p.createdAt >= lastPollDate) return true;
    return false;
  });

  if (newPatients.length > 0) {
    await enrichPatients(env, newPatients);
    await sendTeamsNotification(env, newPatients);

    // Mark as seen
    for (const p of newPatients) seenSet.add(p.id);
    const seenArr = Array.from(seenSet);
    if (seenArr.length > 5000) seenArr.splice(0, seenArr.length - 5000);
    await env.STATE.put("seenPatientIds", JSON.stringify(seenArr));
  }

  // Update last poll date
  await env.STATE.put("lastPollDate", todayStr);

  return newPatients.length;
}

function formatDate(d: Date): string {
  const pad = (n: number) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}

// -------------------------------------------------------------------------
// Worker entry point
// -------------------------------------------------------------------------

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return new Response(
        JSON.stringify({ ok: true, service: "exult-amd-notify" }),
        { headers: { "Content-Type": "application/json" } },
      );
    }

    // Manual trigger for testing
    if (url.pathname === "/trigger" && request.headers.get("X-API-Key") === env.TEAMS_API_KEY) {
      try {
        const count = await pollNewPatients(env);
        return new Response(
          JSON.stringify({ ok: true, newPatients: count }),
          { headers: { "Content-Type": "application/json" } },
        );
      } catch (err) {
        return new Response(
          JSON.stringify({ error: String(err) }),
          { status: 500, headers: { "Content-Type": "application/json" } },
        );
      }
    }

    if (url.pathname === "/status" && request.headers.get("X-API-Key") === env.TEAMS_API_KEY) {
      const lastPoll = await env.STATE.get("lastPollDate");
      const seenRaw = await env.STATE.get("seenPatientIds");
      const seenCount = seenRaw ? JSON.parse(seenRaw).length : 0;
      return new Response(
        JSON.stringify({ lastPollDate: lastPoll, seenPatients: seenCount }),
        { headers: { "Content-Type": "application/json" } },
      );
    }

    return new Response("Exult AMD Notify Worker", { status: 200 });
  },

  async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext): Promise<void> {
    try {
      const count = await pollNewPatients(env);
      if (count > 0) {
        console.log(`Found ${count} new patient(s) in AMD`);
      }
    } catch (err) {
      console.error("AMD poll failed:", err);
    }
  },
};
