// Server-side session manager for the legacy PHP backend (backend.acherryontop.com).
//
// WHY THIS EXISTS
// ---------------
// The PHP `/apiv2/*` write endpoints (product edit, image_changes, taxonomy
// set, setup_new, prod_cat/new, po/new, po/add_products) authenticate via a
// PHP session cookie. In the browser the frontend gets that for free with
// `credentials: 'include'` because the user is logged into
// backend.acherryontop.com in the same browser — that flow is unchanged and
// this module does NOT touch it.
//
// This module backs a separate SIDE-SERVICE (see routes/apiv2-bridge.js) that
// lets EXTERNAL apps (the product-import skill, a future product-edit skill)
// post to the PHP API without a browser cookie. The inventory-server holds the
// PHP session itself: it logs in once with a SERVICE account, caches every
// cookie the backend hands back, and replays them on outbound `/apiv2/*`
// requests. Callers authenticate to the inventory-server with the normal user
// JWT; this PHP-session layer is invisible to them.
//
// LOGIN FLOW (discovered from the live login page)
// ------------------------------------------------
//   1. GET  /login            -> sets affinity/session cookies + embeds a
//                                 hidden `anti_csrf` token in the form HTML.
//   2. POST /login/login      -> { anti_csrf, userid, pin, after_login:'',
//                                 Submit:'Submit' } with the cookies from (1).
//                                 On success the backend rotates/sets the
//                                 authenticated session cookie.
//
// EXPIRY DETECTION
// ----------------
// Unauthenticated `/apiv2/*` calls return HTTP 200 with an HTML body (the login
// page), NOT a 401. So we sniff the response body the same way the frontend
// does (isHtmlResponse) and treat an HTML payload as "session dead" -> re-login
// once and retry.

import axios from 'axios';
import { logger } from '../../shared/logging/logger.js';

const BASE_URL = (process.env.ACOT_BACKEND_URL || 'https://backend.acherryontop.com').replace(/\/$/, '');
const USERID = process.env.ACOT_BACKEND_USERID;
const PIN = process.env.ACOT_BACKEND_PIN;
const USER_AGENT = 'inventory-server/apiv2-bridge';

// Single host => a simple name->value map is a sufficient cookie jar. We do NOT
// hardcode any cookie name: the backend sets a load-balancer affinity cookie
// (`S=...`) AND the real session cookie, and both must be replayed for the
// session to stick. Capturing every Set-Cookie generically handles both.
let cookieJar = new Map();
// Mutex: collapse concurrent login attempts into one in-flight promise so a
// burst of bridged requests doesn't trigger N parallel logins.
let loginInFlight = null;

function cookieHeader() {
  return Array.from(cookieJar.entries())
    .map(([name, value]) => `${name}=${value}`)
    .join('; ');
}

function storeSetCookies(setCookieHeaders) {
  if (!setCookieHeaders) return;
  const list = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
  for (const raw of list) {
    const first = String(raw).split(';')[0];
    const eq = first.indexOf('=');
    if (eq === -1) continue;
    const name = first.slice(0, eq).trim();
    const value = first.slice(eq + 1).trim();
    if (!name) continue;
    // A `name=deleted`/empty value means the backend is clearing the cookie.
    if (value === '' || value.toLowerCase() === 'deleted') {
      cookieJar.delete(name);
    } else {
      cookieJar.set(name, value);
    }
  }
}

export function isHtmlResponse(payload) {
  if (typeof payload !== 'string') return false;
  const trimmed = payload.trim().toLowerCase();
  return trimmed.startsWith('<!doctype html') || trimmed.startsWith('<html');
}

// Find the `name=` of the first <input> whose `type=` matches, tolerant of
// attribute order and quote style.
function inputNameByType(html, type) {
  const tags = String(html).match(/<input\b[^>]*>/gi) || [];
  const typeRe = new RegExp(`type=['"]${type}['"]`, 'i');
  for (const tag of tags) {
    if (typeRe.test(tag)) {
      const nm = /name=['"]([^'"]+)['"]/i.exec(tag);
      if (nm) return nm[1];
    }
  }
  return null;
}

function hasPasswordField(html) {
  return /<input\b[^>]*type=['"]password['"]/i.test(String(html));
}

async function performLogin() {
  if (!USERID || !PIN) {
    throw new Error(
      'ACOT backend service credentials are not configured (set ACOT_BACKEND_USERID and ACOT_BACKEND_PIN)'
    );
  }

  // Fresh jar for a clean login.
  cookieJar = new Map();

  // Step 1: GET the login page for cookies + the anti-CSRF token. The login is
  // IP-based and serves DIFFERENT forms per network (office: userid/pin; outside:
  // username/password), so we parse the served form rather than hardcoding field
  // names — the credential pair maps to whatever identifier/secret fields appear.
  const getRes = await axios.get(`${BASE_URL}/login`, {
    maxRedirects: 0,
    validateStatus: () => true,
    headers: { 'User-Agent': USER_AGENT },
    responseType: 'text',
    transformResponse: (d) => d,
  });
  storeSetCookies(getRes.headers['set-cookie']);
  const page = getRes.data || '';

  const csrfMatch = /name=['"]anti_csrf['"][^>]*value=['"]([^'"]+)['"]/i.exec(page);
  if (!csrfMatch) {
    throw new Error('Could not locate anti_csrf token on the backend login page');
  }
  const antiCsrf = csrfMatch[1];

  const actionMatch = /<form\b[^>]*action=['"]([^'"]+)['"]/i.exec(page);
  const action = actionMatch ? actionMatch[1] : '/login/login';
  const loginUrl = action.startsWith('http') ? action : `${BASE_URL}${action}`;

  // Identifier field is the first text/email input; secret is the password input.
  const idField = inputNameByType(page, 'text') || inputNameByType(page, 'email') || 'username';
  const secretField = inputNameByType(page, 'password') || 'password';

  // Step 2: POST credentials with the cookies from step 1.
  const form = new URLSearchParams();
  form.append('anti_csrf', antiCsrf);
  form.append(idField, USERID);
  form.append(secretField, PIN);
  form.append('after_login', '');
  form.append('Submit', 'Submit');

  const postRes = await axios.post(loginUrl, form.toString(), {
    maxRedirects: 0,
    validateStatus: () => true,
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
      'User-Agent': USER_AGENT,
      Cookie: cookieHeader(),
    },
    responseType: 'text',
    transformResponse: (d) => d,
  });
  storeSetCookies(postRes.headers['set-cookie']);

  // Success heuristic: a failed login re-renders the login form (a page that
  // still has a password field). A redirect, or any non-form response, is
  // treated as success here — but the authoritative check is the /apiv2 probe in
  // checkSession(), since the form may differ across deployments.
  const redirected = postRes.status >= 300 && postRes.status < 400;
  if (!redirected && hasPasswordField(postRes.data)) {
    throw new Error('Backend login failed — credentials rejected (login form re-served)');
  }

  logger.info(
    { status: postRes.status, idField, secretField, cookies: cookieJar.size },
    'acot backend session established'
  );
}

async function ensureSession() {
  if (cookieJar.size > 0) return;
  if (!loginInFlight) {
    loginInFlight = performLogin().finally(() => {
      loginInFlight = null;
    });
  }
  await loginInFlight;
}

async function forceRelogin() {
  cookieJar = new Map();
  if (!loginInFlight) {
    loginInFlight = performLogin().finally(() => {
      loginInFlight = null;
    });
  }
  await loginInFlight;
}

/**
 * Make an authenticated request to a PHP `/apiv2/...` path, transparently
 * handling login and a single re-login-on-expiry retry.
 *
 * @param {object} opts
 * @param {string} opts.method            HTTP method (GET/POST/...).
 * @param {string} opts.path              Path beginning with `/apiv2/...`.
 * @param {string} [opts.query]           Raw query string (without leading `?`).
 * @param {string} [opts.contentType]     Outbound Content-Type header.
 * @param {string|Buffer} [opts.body]     Serialized request body.
 * @returns {Promise<{status:number, contentType:string, data:string}>}
 */
export async function apiv2Request({ method, path, query, contentType, body }) {
  await ensureSession();

  const url = `${BASE_URL}${path}${query ? `?${query}` : ''}`;
  const send = () =>
    axios.request({
      url,
      method,
      data: body,
      maxRedirects: 0,
      validateStatus: () => true,
      responseType: 'text',
      transformResponse: (d) => d,
      headers: {
        'User-Agent': USER_AGENT,
        Cookie: cookieHeader(),
        ...(contentType ? { 'Content-Type': contentType } : {}),
      },
    });

  let res = await send();

  // Session expired / not actually authenticated -> the backend serves the
  // HTML login page. Re-login once and retry.
  if (isHtmlResponse(res.data)) {
    logger.warn({ path }, 'acot apiv2 returned HTML (session expired) — re-logging in');
    await forceRelogin();
    res = await send();
  }

  return {
    status: res.status,
    contentType: res.headers['content-type'] || 'application/json',
    data: typeof res.data === 'string' ? res.data : JSON.stringify(res.data),
  };
}

/** Force a fresh login, then PROVE the session by probing `/apiv2`. A login
 *  that merely "doesn't show a login form" is not proof — the only reliable
 *  signal is that `/apiv2` answers with JSON rather than the HTML login page.
 *  The probe is a no-op: an empty product edit (`products=[]`) edits nothing.
 *  Reports cookie NAMES only (never values). Used by the bridge `_health` route. */
export async function checkSession() {
  let loginError = null;
  try {
    await forceRelogin();
  } catch (err) {
    loginError = err.message;
  }

  let probe;
  try {
    probe = await apiv2Request({
      method: 'POST',
      path: '/apiv2/product/edit',
      contentType: 'application/x-www-form-urlencoded; charset=utf-8',
      body: 'products=%5B%5D', // products=[]
    });
  } catch (err) {
    return {
      authenticated: false,
      error: `apiv2 probe failed: ${err.message}`,
      loginError,
      cookieNames: Array.from(cookieJar.keys()),
    };
  }

  const servedLoginPage = isHtmlResponse(probe.data);
  return {
    authenticated: !servedLoginPage,
    probeStatus: probe.status,
    cookieNames: Array.from(cookieJar.keys()),
    ...(loginError ? { loginError } : {}),
    sample: servedLoginPage
      ? '(received HTML login page — PHP rejected the session)'
      : String(probe.data).slice(0, 160),
  };
}
