import { chromium } from "patchright";
import { createInterface } from "readline";

// All logging goes to stderr — never corrupt the JSON protocol on stdout
const log = (...args) => process.stderr.write(`[trade-daemon] ${args.join(" ")}\n`);

// --- Protocol helpers ---
function sendJson(obj) {
  process.stdout.write(JSON.stringify(obj) + "\n");
}

function sendResponse(reqId, extras = {}) {
  sendJson({ type: "response", reqId, ok: true, ...extras });
}

function sendError(reqId, error) {
  sendJson({ type: "response", reqId, ok: false, error: String(error) });
}

function sendEvent(event, data = {}) {
  sendJson({ type: "event", event, ...data });
}

// --- Selectors (mirrored from Selectors.cs) ---
const Selectors = {
  LiveSearchButton: 'button.livesearch-btn, button:has-text("Activate Live Search")',
  ListingRow: '.resultset .row, [class*="result"]',
  ListingById: (id) => `[data-id="${id}"]`,
  TravelToHideoutButton:
    'button:has-text("Travel to Hideout"), button:has-text("Visit Hideout"), a:has-text("Travel to Hideout"), [class*="hideout"]',
  ConfirmYesButton:
    'button:has-text("Yes"), button:has-text("Confirm"), button:has-text("OK"), button:has-text("Accept")',
};

// --- State ---
let context = null;
const searchPages = new Map(); // searchId → page
const pausedSearches = new Set();
const scrapPages = new Map(); // scrapId → { page, items }
let scrapIdCounter = 0;

// --- Helpers ---
function extractSearchId(url) {
  const cleaned = url.replace(/\/live\/?$/, "");
  const parts = cleaned.split("/");
  return parts[parts.length - 1] || url;
}

function parseTradeItem(r) {
  const id = r.id || "";
  let w = 1,
    h = 1,
    stashX = 0,
    stashY = 0,
    account = "";

  if (r.item) {
    if (r.item.w != null) w = r.item.w;
    if (r.item.h != null) h = r.item.h;
  }
  if (r.listing) {
    if (r.listing.stash) {
      if (r.listing.stash.x != null) stashX = r.listing.stash.x;
      if (r.listing.stash.y != null) stashY = r.listing.stash.y;
    }
    if (r.listing.account?.name) account = r.listing.account.name;
  }
  return { id, w, h, stashX, stashY, account };
}

function parseTradeItemWithPrice(r) {
  const base = parseTradeItem(r);
  let name = "";
  let priceAmount = 0;
  let priceCurrency = "";

  if (r.item?.icon) {
    try {
      const pathname = new URL(r.item.icon).pathname;
      const filename = pathname.split("/").pop() || "";
      name = filename.replace(".png", "");
    } catch {
      // fallback: regex on last path segment
      const match = r.item.icon.match(/\/([^/]+)\.png/);
      if (match) name = match[1];
    }
  }

  if (r.listing?.price) {
    if (r.listing.price.amount != null) priceAmount = r.listing.price.amount;
    if (r.listing.price.currency) priceCurrency = r.listing.price.currency;
  }

  return { ...base, name, priceAmount, priceCurrency };
}

async function waitForVisible(locator, timeoutMs) {
  try {
    await locator.waitFor({ state: "visible", timeout: timeoutMs });
    return true;
  } catch {
    return false;
  }
}

async function handleConfirmDialog(page) {
  await new Promise((r) => setTimeout(r, 500));
  try {
    const confirmBtn = page.locator(Selectors.ConfirmYesButton).first();
    if (await waitForVisible(confirmBtn, 2000)) {
      await confirmBtn.click();
      log("Confirmed dialog");
    }
  } catch {
    /* No dialog */
  }
}

function handleWebSocket(ws, searchId) {
  if (!ws.url().includes("/api/trade") || !ws.url().includes("/live/")) return;

  log(`WebSocket connected for live search: ${searchId}`);

  ws.on("framereceived", (frame) => {
    if (pausedSearches.has(searchId)) return;
    // Note: trade site now sends JWTs, not raw item IDs.
    // We rely on the fetch response interceptor instead.
  });

  ws.on("close", () => {
    log(`WebSocket closed: ${searchId}`);
    sendEvent("wsClose", { searchId });
  });
}

// --- Command handlers ---

async function cmdStart(reqId, params) {
  const { browserUserDataDir, headless, dashboardUrl } = params;
  log(`Starting browser (stealth), userDataDir=${browserUserDataDir}`);

  context = await chromium.launchPersistentContext(browserUserDataDir, {
    channel: "chrome",
    headless: false,
    viewport: null,
  });

  if (dashboardUrl) {
    const pages = context.pages();
    if (pages.length > 0) {
      await pages[0].goto(dashboardUrl);
    } else {
      const p = await context.newPage();
      await p.goto(dashboardUrl);
    }
    log(`Dashboard opened: ${dashboardUrl}`);
  }

  log("Browser launched (playwright-extra stealth active)");
  sendResponse(reqId);
}

async function cmdAddSearch(reqId, params) {
  if (!context) throw new Error("Browser not started");
  const { url } = params;
  const searchId = extractSearchId(url);

  if (searchPages.has(searchId)) {
    log(`Search already open: ${searchId}`);
    sendResponse(reqId, { searchId });
    return;
  }

  log(`Adding trade search: ${url} (${searchId})`);
  const page = await context.newPage();
  searchPages.set(searchId, page);

  // Register WebSocket handler BEFORE navigation so we catch connections during page load
  page.on("websocket", (ws) => handleWebSocket(ws, searchId));

  // Track whether live search is active — don't emit items from initial page load
  let liveActive = false;
  const seenIds = new Set();

  // Intercept fetch responses to get full item data (WebSocket now sends JWTs, not raw IDs)
  page.on("response", async (response) => {
    if (!liveActive) return;
    if (!response.url().includes("/api/trade2/fetch/")) return;
    try {
      const body = await response.text();
      const doc = JSON.parse(body);
      if (doc.result && Array.isArray(doc.result)) {
        const items = doc.result
          .map((r) => parseTradeItem(r))
          .filter((i) => i.id && !seenIds.has(i.id));
        items.forEach((i) => seenIds.add(i.id));
        if (items.length > 0) {
          log(`New listings (fetch): ${searchId} (${items.length} new items)`);
          sendEvent("newListings", { searchId, items });
        }
      }
    } catch {
      /* Non-JSON trade response */
    }
  });

  await page.goto(url, { waitUntil: "networkidle" });
  await new Promise((r) => setTimeout(r, 2000)); // PageLoad delay

  try {
    const liveBtn = page.locator(Selectors.LiveSearchButton).first();
    await liveBtn.click({ timeout: 5000 });
    liveActive = true;
    log(`Live search activated: ${searchId}`);
  } catch {
    log(`Could not click Activate Live Search: ${searchId}`);
  }

  sendResponse(reqId, { searchId });
}

async function cmdPauseSearch(reqId, params) {
  const { searchId } = params;
  pausedSearches.add(searchId);
  const page = searchPages.get(searchId);
  if (page) {
    await page.close();
    searchPages.delete(searchId);
  }
  log(`Search paused: ${searchId}`);
  sendResponse(reqId);
}

async function cmdClickTravel(reqId, params) {
  const { pageId, itemId } = params;
  let page = searchPages.get(pageId) || scrapPages.get(pageId)?.page;
  if (!page) {
    sendResponse(reqId, { clicked: false });
    return;
  }

  try {
    if (itemId) {
      const row = page.locator(Selectors.ListingById(itemId));
      if (await waitForVisible(row, 5000)) {
        const travelBtn = row.locator(Selectors.TravelToHideoutButton).first();
        if (await waitForVisible(travelBtn, 3000)) {
          await travelBtn.click();
          log(`Clicked Travel to Hideout for item ${itemId}`);
          await handleConfirmDialog(page);
          sendResponse(reqId, { clicked: true });
          return;
        }
      }
    }

    const btn = page.locator(Selectors.TravelToHideoutButton).first();
    await btn.click({ timeout: 5000 });
    log("Clicked Travel to Hideout");
    await handleConfirmDialog(page);
    sendResponse(reqId, { clicked: true });
  } catch (ex) {
    log(`Failed to click Travel to Hideout: ${ex.message}`);
    sendResponse(reqId, { clicked: false });
  }
}

async function cmdAddDiamondSearch(reqId, params) {
  if (!context) throw new Error("Browser not started");
  const { url } = params;
  const searchId = extractSearchId(url);

  if (searchPages.has(searchId)) {
    log(`Diamond search already open: ${searchId}`);
    sendResponse(reqId, { searchId });
    return;
  }

  log(`Adding diamond search: ${url} (${searchId})`);
  const page = await context.newPage();
  searchPages.set(searchId, page);

  // Intercept fetch responses for item data + prices
  page.on("response", async (response) => {
    if (!response.url().includes("/api/trade2/fetch/")) return;
    try {
      const body = await response.text();
      const doc = JSON.parse(body);
      if (doc.result && Array.isArray(doc.result)) {
        const items = doc.result.map((r) => parseTradeItemWithPrice(r));
        if (items.length > 0) {
          log(`Diamond listings: ${searchId} (${items.length} items)`);
          sendEvent("diamondListings", { searchId, items });
        }
      }
    } catch {
      /* Non-JSON trade response */
    }
  });

  // Hook WebSocket BEFORE navigation so we catch connections during page load
  page.on("websocket", (ws) => handleWebSocket(ws, searchId));

  await page.goto(url, { waitUntil: "networkidle" });
  await new Promise((r) => setTimeout(r, 2000));

  try {
    const liveBtn = page.locator(Selectors.LiveSearchButton).first();
    await liveBtn.click({ timeout: 5000 });
    log(`Diamond live search activated: ${searchId}`);
  } catch {
    log(`Could not click Activate Live Search: ${searchId}`);
  }

  sendResponse(reqId, { searchId });
}

async function cmdOpenScrapPage(reqId, params) {
  if (!context) throw new Error("Browser not started");
  const { url } = params;
  const scrapId = `scrap-${++scrapIdCounter}`;

  const page = await context.newPage();
  const items = [];

  page.on("response", async (response) => {
    if (!response.url().includes("/api/trade2/fetch/")) return;
    try {
      const body = await response.text();
      const doc = JSON.parse(body);
      if (doc.result && Array.isArray(doc.result)) {
        for (const r of doc.result) {
          items.push(parseTradeItem(r));
        }
      }
    } catch {
      /* Non-JSON trade response */
    }
  });

  await page.goto(url, { waitUntil: "networkidle" });
  await new Promise((r) => setTimeout(r, 2000)); // PageLoad delay

  scrapPages.set(scrapId, { page, items: [...items] });
  log(`Scrap page opened: ${url} (${items.length} items) → ${scrapId}`);
  sendResponse(reqId, { scrapId, items });
}

async function cmdReloadScrapPage(reqId, params) {
  const { scrapId } = params;
  const entry = scrapPages.get(scrapId);
  if (!entry) throw new Error(`Unknown scrapId: ${scrapId}`);

  const { page } = entry;
  const items = [];

  const onResponse = async (response) => {
    if (!response.url().includes("/api/trade2/fetch/")) return;
    try {
      const body = await response.text();
      const doc = JSON.parse(body);
      if (doc.result && Array.isArray(doc.result)) {
        for (const r of doc.result) {
          items.push(parseTradeItem(r));
        }
      }
    } catch {
      /* Non-JSON */
    }
  };

  page.on("response", onResponse);
  await page.reload({ waitUntil: "networkidle" });
  await new Promise((r) => setTimeout(r, 2000)); // PageLoad delay
  page.removeListener("response", onResponse);

  entry.items = [...items];
  log(`Scrap page reloaded: ${scrapId} (${items.length} items)`);
  sendResponse(reqId, { items });
}

async function cmdCloseScrapPage(reqId, params) {
  const { scrapId } = params;
  const entry = scrapPages.get(scrapId);
  if (entry) {
    try {
      await entry.page.close();
    } catch {
      /* already closed */
    }
    scrapPages.delete(scrapId);
  }
  log(`Scrap page closed: ${scrapId}`);
  sendResponse(reqId);
}

async function cmdStop(reqId) {
  log("Stopping daemon...");
  for (const [id, page] of searchPages) {
    try {
      await page.close();
    } catch {
      /* ignore */
    }
  }
  searchPages.clear();

  for (const [id, entry] of scrapPages) {
    try {
      await entry.page.close();
    } catch {
      /* ignore */
    }
  }
  scrapPages.clear();

  if (context) {
    try {
      await context.close();
    } catch {
      /* ignore */
    }
    context = null;
  }

  sendResponse(reqId);
  log("Daemon stopped");
  process.exit(0);
}

// --- Command dispatch ---
const handlers = {
  start: cmdStart,
  addSearch: cmdAddSearch,
  addDiamondSearch: cmdAddDiamondSearch,
  pauseSearch: cmdPauseSearch,
  clickTravel: cmdClickTravel,
  openScrapPage: cmdOpenScrapPage,
  reloadScrapPage: cmdReloadScrapPage,
  closeScrapPage: cmdCloseScrapPage,
  stop: cmdStop,
};

async function handleCommand(line) {
  let msg;
  try {
    msg = JSON.parse(line);
  } catch {
    log(`Invalid JSON: ${line}`);
    return;
  }

  const { reqId, cmd, ...params } = msg;
  const handler = handlers[cmd];
  if (!handler) {
    sendError(reqId, `Unknown command: ${cmd}`);
    return;
  }

  try {
    await handler(reqId, params);
  } catch (ex) {
    log(`Command ${cmd} failed: ${ex.message}`);
    sendError(reqId, ex.message);
  }
}

// --- Main ---
const rl = createInterface({ input: process.stdin });
rl.on("line", (line) => handleCommand(line.trim()));
rl.on("close", () => {
  log("stdin closed, shutting down");
  process.exit(0);
});

// Signal ready
sendJson({ type: "ready" });
log("Daemon ready, waiting for commands...");
