/*
  User provisioning for the Dolibarr admin UI, driven by Playwright.

  Why the UI and not raw SQL:
    - Passwords use MAIN_SECURITY_HASH_ALGO=password_hash (bcrypt), so we let
      Dolibarr hash them by submitting the create form.
    - API keys are stored ENCRYPTED with the instance key DOLI_INSTANCE_UNIQUE_ID.
      Each instance (incl. the sandbox) has its own uuid, so a key can only be
      produced correctly by that instance — we MUST generate it via the UI and
      read back the resulting value, never INSERT a raw key.

  IMPORTANT — selectors below are best-effort and have NOT been verified against
  a live, installed sandbox (the sandbox Dolibarr is not provisioned yet). Field
  names / icon controls are taken from Dolibarr 22 conventions and must be
  confirmed on the first real run. Spots that are guessed are marked GUESS:.
*/
import type { Page } from "playwright";
import forms from "../forms.ts";
import login from "../login.ts";

type UserCtx = {
  page: Page;
  dolibarrAddress: string;
  adminCredentials: { username: string; password: string };
  // imgFolderPath is required by forms.fillForm's signature even though no
  // file input is used here; provisionSandbox.ts supplies it from globalCtx.
  imgFolderPath: string;
};

type NewUser = {
  login: string;
  password: string;
  lastname?: string;
  admin?: boolean;
};

// The text fields filled via forms.fillForm (the admin flag is a checkbox we
// handle separately, so it is intentionally not part of this type).
type UserFormFields = {
  login: string;
  password: string;
  lastname?: string;
};

// fr_FR Dolibarr user create form (/user/card.php?action=create&mode=create).
// GUESS: confirm these input names on first real run. `login` and `lastname`
// are stable across Dolibarr versions; the password field has been `password`
// for the manual-entry case (the "generate automatically" option is a separate
// radio/checkbox we leave untouched so our explicit value is used).
const newUserInputNames = new Map<keyof UserFormFields, string>([
  ["login", "login"],
  ["lastname", "lastname"],
  ["password", "password"],
]);

/*
  Create a user via /user/card.php?action=create and return its numeric id.

  The admin flag is a checkbox (name="admin"); fillForm has no checkbox handler
  so we toggle it explicitly. After submit Dolibarr redirects to the user card
  whose URL carries ?id=<n> — we parse the id from there (falling back to a
  hidden input on the page).
*/
async function createUser(
  globalCtx: UserCtx,
  { login: userLogin, password, lastname, admin = false }: NewUser,
): Promise<number> {
  const { page, dolibarrAddress, imgFolderPath } = globalCtx;
  await login.doAdminLogin(globalCtx);

  // Idempotent: if the login already exists (e.g. a re-run), return its id rather
  // than submitting a duplicate create (which Dolibarr rejects).
  const existingId = await findUserId(globalCtx, userLogin);
  if (existingId !== undefined) return existingId;

  await page.goto(`${dolibarrAddress}/user/card.php?action=create&mode=create`);

  // Fill text inputs (login, lastname, password) without submitting yet.
  const formFields: UserFormFields = {
    login: userLogin,
    password,
    lastname,
  };
  await forms.fillForm(
    { page, imgFolderPath },
    formFields,
    newUserInputNames,
    0,
  );

  // Admin checkbox — only present when the logged-in user is themselves admin.
  // GUESS: input[name="admin"]; on some setups it's a <select> instead.
  const adminCheckbox = page.locator('input[name="admin"]');
  if (await adminCheckbox.count() > 0) {
    if (admin) {
      await adminCheckbox.check();
    } else {
      // Leave unchecked (default), but be explicit/defensive.
      if (await adminCheckbox.isChecked()) await adminCheckbox.uncheck();
    }
  }

  // Submit the create form. The create page has a single primary submit button
  // ("Créer utilisateur"); :nth-match index 1 mirrors the fillForm convention.
  await page.click(':nth-match(input[type="submit"], 1)');

  // After creation Dolibarr lands on the user card. Parse the id.
  await page.waitForLoadState("domcontentloaded");
  const id = await readUserIdFromPage(page);
  if (id === undefined) {
    throw new Error(
      `Could not determine the new user id for login "${userLogin}" after ` +
        `submitting the create form (current URL: ${page.url()}).`,
    );
  }
  return id;
}

/*
  Read the numeric user id from the current user card: first from the URL
  (?id=<n>), then from a hidden input[name="id"] as a fallback.
*/
async function readUserIdFromPage(page: Page): Promise<number | undefined> {
  const fromUrl = new URL(page.url()).searchParams.get("id");
  if (fromUrl && /^\d+$/.test(fromUrl)) return Number(fromUrl);

  // GUESS: a hidden input carrying the id (common on Dolibarr card pages).
  const hidden = page.locator('input[name="id"]');
  if (await hidden.count() > 0) {
    const val = await hidden.first().getAttribute("value");
    if (val && /^\d+$/.test(val)) return Number(val);
  }
  return undefined;
}

/*
  Find an existing user's numeric id by login via the user list, so createUser is
  idempotent across re-runs. Matches the table row containing the login, then reads
  the id from that row's user-card link.
*/
async function findUserId(
  globalCtx: UserCtx,
  userLogin: string,
): Promise<number | undefined> {
  const { page, dolibarrAddress } = globalCtx;
  await page.goto(
    `${dolibarrAddress}/user/list.php?search_login=${encodeURIComponent(userLogin)}`,
  );
  await page.waitForLoadState("domcontentloaded");
  const cardLink = page
    .locator("tr", { hasText: userLogin })
    .locator('a[href*="/user/card.php?id="]')
    .first();
  if (await cardLink.count() === 0) return undefined;
  const href = (await cardLink.getAttribute("href")) ?? "";
  const m = href.match(/[?&]id=(\d+)/);
  return m ? Number(m[1]) : undefined;
}

/*
  Grant a set of rights to a user on /user/perms.php?id=<userId>.

  Dolibarr renders each grantable permission as a link whose href carries
  action=addrights&rights=<id> (and a matching action=delrights to revoke).
  A right that is already granted shows the delrights link instead, so we treat
  the presence of an addrights link as "not yet granted" and click it; if it's
  absent we assume the right is already on and skip it (defensive/idempotent).
*/
async function assignRights(
  globalCtx: UserCtx,
  userId: number,
  rightIds: number[],
): Promise<void> {
  const { page, dolibarrAddress } = globalCtx;
  await login.doAdminLogin(globalCtx);

  for (const rightId of rightIds) {
    // Re-navigate per right: Dolibarr reloads the perms table after each grant,
    // which would otherwise stale the locators.
    await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`);

    // The grant control is an <a> linking to action=addrights&rights=<id>. The
    // trailing "&" anchors the id so rights=12 does NOT substring-match rights=121
    // / rights=1232 etc. (Dolibarr hrefs are ...&rights=<id>&confirm=yes...).
    const addLink = page.locator(
      `a[href*="action=addrights"][href*="rights=${rightId}&"]`,
    );

    if (await addLink.count() === 0) {
      // Either already granted (only a delrights link is shown) or the row is
      // not visible for this user. Skip rather than fail the whole run.
      continue;
    }
    await addLink.first().click();
    await page.waitForLoadState("domcontentloaded");
  }
}

/*
  Generate (or read) the user's API key on /user/card.php?id=<userId> in edit
  mode and RETURN it as a string.

  With the API module enabled the user card edit form shows an api_key text
  input plus a "generate" control (a dice/refresh icon-link, typically
  id="generate_api_key" or an <a> with action=...generate...apikey). We trigger
  generation, then read the value back out of the api_key input. If a key is
  already present we read and return it without regenerating (idempotent).

  The caller is responsible for handling the returned secret safely — DO NOT log
  it at debug verbosity.
*/
async function generateApiKey(
  globalCtx: UserCtx,
  userId: number,
): Promise<string> {
  const { page, dolibarrAddress } = globalCtx;
  await login.doAdminLogin(globalCtx);

  await page.goto(`${dolibarrAddress}/user/card.php?id=${userId}&action=edit`);

  // GUESS: the API key field is input[name="api_key"].
  const apiKeyInput = page.locator('input[name="api_key"]');
  if (await apiKeyInput.count() === 0) {
    throw new Error(
      `API key input (input[name="api_key"]) not found on the user card for ` +
        `id=${userId}. Is the REST API module enabled? Confirm the field name ` +
        `on the running sandbox.`,
    );
  }

  // Reuse the existing key ONLY if it is a clean plaintext key. After an iso-prod
  // refresh the instance unique-id changes, so a key encrypted with the OLD id can
  // no longer be decrypted — Dolibarr then renders non-UTF-8 bytes in the field.
  // Reusing that (the old bug) wrote a corrupt key file. Treat anything that isn't
  // a plausible key (≥24 url-safe chars) as garbage: clear it and regenerate.
  const existing = (await apiKeyInput.first().inputValue()).trim();
  if (/^[A-Za-z0-9_-]{24,}$/.test(existing)) return existing;
  if (existing.length > 0) {
    await apiKeyInput.first().fill(""); // drop the undecryptable/garbage value
  }

  // Trigger generation. GUESS: a generate control next to the field. We try a
  // few likely selectors in order and click the first that exists.
  const generateSelectors = [
    "#generate_api_key",
    'a[href*="action=generateapikey"]',
    'a[href*="generate"][href*="apikey"]',
    // Icon-only fallbacks commonly used by Dolibarr "generate" buttons.
    "span.fa-dice",
    "a.fa-refresh",
  ];

  let clicked = false;
  for (const sel of generateSelectors) {
    const ctrl = page.locator(sel);
    if (await ctrl.count() > 0) {
      await ctrl.first().click();
      clicked = true;
      break;
    }
  }
  if (!clicked) {
    throw new Error(
      `No API-key generate control found near input[name="api_key"] for ` +
        `id=${userId} (tried ${generateSelectors.join(", ")}). Confirm the ` +
        `generate control on the running sandbox.`,
    );
  }

  // The generate control fills the input client-side; poll the input value
  // (via the locator, no browser globals) until it is non-empty or we time out.
  let key = "";
  const deadline = Date.now() + 10_000;
  while (Date.now() < deadline) {
    key = (await apiKeyInput.first().inputValue()).trim();
    if (key.length > 0) break;
    await page.waitForTimeout(200);
  }
  if (key.length === 0) {
    throw new Error(
      `API key field was still empty after triggering generation for ` +
        `id=${userId}.`,
    );
  }

  // Persist: the generate control only fills the field client-side. Submit the
  // edit form so Dolibarr saves api_key to the DB — otherwise it stays NULL and
  // the generated key never authenticates.
  const saveBtn = page.locator('input[name="save"], button[name="save"]');
  if (await saveBtn.count() > 0) {
    await saveBtn.first().click();
  } else {
    await page.click(':nth-match(input[type="submit"], 1)');
  }
  await page.waitForLoadState("domcontentloaded");

  return key;
}

export default {
  createUser,
  assignRights,
  generateApiKey,
};
