// Meta (Instagram + Messenger) adapter for DewLink — browser-session, DRAFT-ONLY.
//
// One adapter drives both Meta DM surfaces; META_MODE picks which:
//   META_MODE=instagram  → instagram.com/direct/inbox
//   META_MODE=messenger  → messenger.com
//
// Same model as the LinkedIn adapter: drive the user's *system* Chrome via playwright-core
// (channel:"chrome") in a DEDICATED, isolated profile dir; reuse the user's existing login by
// decrypting + injecting that site's cookies from their real Chrome profile (no re-login, no
// password ever seen); run a REAL headful window parked far off-screen so it's invisible.
//
// Safety (hard wall #1 — never auto-send): READS DM threads into the unified inbox and can TYPE a
// draft into the compose box, but NEVER clicks Send. The AI drafts; the human sends.
//
// DOM caveat: Instagram/Messenger ship obfuscated, unstable class names. The thread selectors below
// are a best-effort first pass (like LinkedIn's) and will need tuning against the live logged-in DOM.
// Per-thread failures are swallowed so one bad selector never kills the whole sync.

import { chromium, type BrowserContext, type Page } from "playwright-core";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import crypto from "node:crypto";
import { execSync } from "node:child_process";
import { DatabaseSync } from "node:sqlite";
import { emitEvent } from "./events.ts";

type Mode = "instagram" | "messenger";
const MODE: Mode = (process.env.META_MODE as Mode) === "messenger" ? "messenger" : "instagram";

const HOME = process.env.HOME || "";
const account = process.env.META_ACCOUNT_NAME || "default";
const profileDir =
  process.env.META_PROFILE_DIR || path.join(HOME, `.dewlink-${MODE}`, account);
const HEADLESS = process.env.META_HEADLESS === "1";

export type MetaSession = { context: BrowserContext; page: Page };

/** Per-surface configuration — the only thing that differs between Instagram and Messenger. */
const CONF: Record<Mode, {
  label: string;
  cookieHost: string;        // SQL LIKE pattern for the cookie host_key
  inboxUrl: string;
  loginUrl: string;
  loggedOutRe: RegExp;       // url patterns that mean "not logged in"
  threadSel: string;         // conversation-list rows
  nameSel: string;           // participant name within a row
  snippetSel: string;        // last-message snippet within a row
  threadUrl: (id: string) => string;
  composeSel: string;        // compose textbox on a thread page
}> = {
  instagram: {
    label: "instagram",
    cookieHost: "%instagram.com",
    inboxUrl: "https://www.instagram.com/direct/inbox/",
    loginUrl: "https://www.instagram.com/accounts/login/",
    loggedOutRe: /\/accounts\/login|\/accounts\/signup/,
    threadSel: "div[role='list'] div[role='listitem'], a[href^='/direct/t/']",
    nameSel: "span[dir='auto']",
    snippetSel: "span[dir='auto']:last-child",
    threadUrl: (id) => `https://www.instagram.com/direct/t/${id}/`,
    composeSel: "div[role='textbox'][contenteditable='true'], textarea[placeholder]",
  },
  messenger: {
    label: "messenger",
    cookieHost: "%messenger.com",
    inboxUrl: "https://www.messenger.com/",
    loginUrl: "https://www.messenger.com/login/",
    loggedOutRe: /\/login/,
    threadSel: "div[role='grid'] div[role='row'], a[href^='/t/']",
    nameSel: "span[dir='auto']",
    snippetSel: "span[dir='auto']:last-child",
    threadUrl: (id) => `https://www.messenger.com/t/${id}/`,
    composeSel: "div[role='textbox'][contenteditable='true'], div[aria-label='Message']",
  },
};
const C = CONF[MODE];

function log(level: "info" | "warn" | "error", msg: string) {
  emitEvent({ type: "log", level, msg: `[${C.label}] ${msg}` });
}

type Cookie = {
  name: string; value: string; domain: string; path: string;
  httpOnly: boolean; secure: boolean; expires: number;
};

/** Chrome's macOS cookie key: PBKDF2(SHA1, Keychain "Chrome Safe Storage", "saltysalt", 1003, 16). */
function chromeKey(): Buffer | null {
  try {
    const pw = execSync(`security find-generic-password -ws "Chrome Safe Storage"`, {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    }).trim();
    return crypto.pbkdf2Sync(pw, "saltysalt", 1003, 16, "sha1");
  } catch (e: any) {
    log("warn", `keychain read failed (allow the Chrome Safe Storage prompt): ${e?.message}`);
    return null;
  }
}

/** Decrypt a macOS v10 Chrome cookie value (AES-128-CBC, IV=16×0x20). Strips the M127+ domain-hash prefix. */
function decryptV10(enc: Buffer, key: Buffer): string | null {
  if (!enc || enc.length < 3 || enc.subarray(0, 3).toString() !== "v10") return null;
  try {
    const iv = Buffer.alloc(16, 0x20);
    const d = crypto.createDecipheriv("aes-128-cbc", key, iv);
    d.setAutoPadding(false);
    let out = Buffer.concat([d.update(enc.subarray(3)), d.final()]);
    const pad = out[out.length - 1];
    if (pad > 0 && pad <= 16) out = out.subarray(0, out.length - pad);
    const stripped = out.length > 32 ? out.subarray(32) : out;
    const asStr = stripped.toString("utf8");
    return /[^\x09\x0A\x0D\x20-\x7E]/.test(asStr) ? out.toString("utf8") : asStr;
  } catch {
    return null;
  }
}

/** Reuse the user's existing login by decrypting this surface's cookies from their real Chrome profile. */
function loadRealCookies(): Cookie[] {
  const src = process.env.META_CHROME_PROFILE;
  if (!src || !fs.existsSync(src)) return [];
  const key = chromeKey();
  if (!key) return [];
  const srcCookies = fs.existsSync(path.join(src, "Network", "Cookies"))
    ? path.join(src, "Network", "Cookies")
    : path.join(src, "Cookies");
  if (!fs.existsSync(srcCookies)) return [];
  const tmp = path.join(os.tmpdir(), `dewlink-meta-${process.pid}.sqlite`);
  try {
    fs.copyFileSync(srcCookies, tmp);
    const db = new DatabaseSync(tmp, { readOnly: true } as any);
    const rows = db
      .prepare(
        `SELECT host_key, name, encrypted_value, path, is_secure, is_httponly,
                (expires_utc / 1000000.0 - 11644473600) AS exp_sec
         FROM cookies WHERE host_key LIKE ?`,
      )
      .all(C.cookieHost) as any[];
    db.close();
    const cookies: Cookie[] = [];
    for (const r of rows) {
      const blob = Buffer.isBuffer(r.encrypted_value) ? r.encrypted_value : Buffer.from(r.encrypted_value);
      const value = decryptV10(blob, key);
      if (!value) continue;
      const exp = Number(r.exp_sec) || -1;
      cookies.push({
        name: String(r.name),
        value,
        domain: String(r.host_key),
        path: String(r.path) || "/",
        httpOnly: !!r.is_httponly,
        secure: !!r.is_secure,
        expires: exp > 0 ? Math.floor(exp) : -1,
      });
    }
    log("info", `loaded ${cookies.length} ${C.label} cookie(s) from ${path.basename(src)}`);
    return cookies;
  } catch (e: any) {
    log("warn", `cookie load failed: ${e?.message}`);
    return [];
  } finally {
    try { fs.unlinkSync(tmp); } catch {}
  }
}

async function launch(): Promise<MetaSession | null> {
  fs.mkdirSync(profileDir, { recursive: true });
  try {
    const hidden = process.env.META_HIDDEN === "1";
    const args = ["--no-first-run", "--no-default-browser-check"];
    if (hidden) args.push("--window-position=-32000,-32000", "--window-size=1280,900");
    const context = await chromium.launchPersistentContext(profileDir, {
      channel: "chrome",
      headless: HEADLESS,
      viewport: { width: 1280, height: 900 },
      args,
    });
    const page = context.pages()[0] ?? (await context.newPage());
    return { context, page };
  } catch (e: any) {
    log("error", `chrome launch failed: ${e?.message}. Is Google Chrome installed?`);
    return null;
  }
}

async function isLoggedIn(page: Page): Promise<boolean> {
  await page.goto(C.inboxUrl, { waitUntil: "domcontentloaded", timeout: 45_000 }).catch(() => {});
  const url = page.url();
  if (C.loggedOutRe.test(url)) return false;
  // Instagram bounces logged-out users to /accounts/login; Messenger to /login.
  return url.includes(MODE === "instagram" ? "/direct" : "messenger.com");
}

async function waitForLogin(page: Page, timeoutMs: number): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (await isLoggedIn(page)) return true;
    await page.waitForTimeout(3000);
  }
  return false;
}

/** Best-effort scrape of the DM list → emit `message` events (IN, historical). */
async function syncMessaging(page: Page, limit = 20): Promise<void> {
  try {
    await page.goto(C.inboxUrl, { waitUntil: "domcontentloaded", timeout: 45_000 });
    await page.waitForTimeout(4000); // let the DM list hydrate (heavy SPA)
    const threads = page.locator(C.threadSel);
    const n = Math.min(await threads.count().catch(() => 0), limit);
    log("info", `found ${n} conversation(s)`);
    let emitted = 0;
    for (let i = 0; i < n; i++) {
      try {
        const row = threads.nth(i);
        const texts = (await row.locator(C.nameSel).allTextContents().catch(() => [])) as string[];
        const name = (texts[0] || "").replace(/\s+/g, " ").trim() || `${C.label} ${i + 1}`;
        const snippet = (texts[texts.length - 1] || "").replace(/\s+/g, " ").trim();
        if (!snippet || snippet === name) continue;
        // Prefer a stable thread id from the row's href (/direct/t/<id>/ or /t/<id>/); else key by name.
        const href = (await row.getAttribute("href").catch(() => null))
          || (await row.locator("a[href*='/t/']").first().getAttribute("href").catch(() => null));
        const idMatch = href?.match(/\/t\/([^/]+)/);
        const threadId = idMatch ? idMatch[1] : `${C.label}:${name}`;
        emitEvent({
          type: "message",
          convExternalId: threadId,
          convTitle: name,
          direction: "IN",
          body: snippet,
          sender: name,
          ts: Date.now(),
          providerMsgId: `${threadId}:${Buffer.from(snippet).toString("base64").slice(0, 28)}`,
          historical: true,
        });
        emitted++;
      } catch (e: any) {
        log("warn", `thread ${i} scrape failed: ${e?.message}`);
      }
    }
    log("info", `ingested ${emitted}/${n} conversation(s)`);
  } catch (e: any) {
    log("error", `messaging sync failed: ${e?.message}`);
  }
}

/** DRAFT-ONLY: type `text` into a thread's compose box; never clicks Send. */
export async function placeDraft(page: Page, threadId: string, text: string): Promise<boolean> {
  try {
    // threadId may be a real id or our "<label>:name" fallback; only navigate for real ids.
    if (!threadId.startsWith(`${C.label}:`)) {
      await page.goto(C.threadUrl(threadId), { waitUntil: "domcontentloaded", timeout: 45_000 });
      await page.waitForTimeout(2500);
    }
    const editor = page.locator(C.composeSel).first();
    await editor.click();
    await editor.fill(text); // places the draft; deliberately NO Enter / Send
    log("info", `draft placed in thread ${threadId} (not sent — review & send manually)`);
    return true;
  } catch (e: any) {
    log("error", `draft placement failed: ${e?.message}`);
    return false;
  }
}

/** Public entry: connect, sync, return the live session for the command channel. */
export async function startMeta(): Promise<MetaSession | null> {
  emitEvent({ type: "status", state: "connecting" });
  const session = await launch();
  if (!session) { emitEvent({ type: "status", state: "close" }); return null; }
  const { context, page } = session;

  const real = loadRealCookies();
  if (real.length) {
    try { await context.addCookies(real as any); log("info", `injected existing ${C.label} session`); }
    catch (e: any) { log("warn", `cookie inject failed: ${e?.message}`); }
  }

  if (await isLoggedIn(page)) {
    emitEvent({ type: "status", state: "open", user: null });
    await syncMessaging(page);
    return session;
  }

  emitEvent({ type: "status", state: "logged_out" });
  if (HEADLESS) { log("warn", `not logged in and headless — open the ${C.label} account to log in.`); return session; }
  log("info", `please log in to ${C.label} in the Chrome window that just opened…`);
  await page.goto(C.loginUrl, { waitUntil: "domcontentloaded" }).catch(() => {});
  const ok = await waitForLogin(page, 300_000);
  if (ok) { emitEvent({ type: "status", state: "open", user: null }); await syncMessaging(page); }
  else log("warn", "login not completed within 5 minutes.");
  return session;
}
