/**
 * @license
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * Login / authentication client.
 *
 * The backend login endpoint is the ONE thing that still needs confirming — see
 * the CONFIG block below. Everything else (request shape, response parsing,
 * role mapping, error handling) is wired and ready. Once the four CONFIG values
 * match the real API, the login form verifies credentials and gates entry.
 *
 * Requests are routed through the existing Vite dev proxy so no secret/CORS
 * concern reaches the browser (see vite.config.ts: `/fiesta/*` → fiesta.nearle.app,
 * `/hasura/*` → api.workolik.com). If the login route lives on a different host,
 * add a proxy entry there and point LOGIN_ENDPOINT at it.
 */

import { firstRow, num, str, type Row } from './fiestaApi';

// ── Backend login config ──────────────────────────────────────────────────────

/**
 * Fiesta application login — called directly at
 * https://fiesta.nearle.app/live/api/v1/web/users/applogin (CORS-enabled).
 * Observed shape:
 *   request:  { authname: <email>, password: <password>, configid: 1, userfcmtoken: null }
 *   failure:  { code: 409, message: "Invalid Email", status: false }
 *   success:  status !== false  (Fiesta envelope, optionally with `details`)
 */
const LOGIN_ENDPOINT = import.meta.env.VITE_FIESTA_URL 
  ? `${import.meta.env.VITE_FIESTA_URL}/users/applogin` 
  : 'https://fiesta.nearle.app/live/api/v1/web/users/applogin';

/** Request body field names the endpoint expects for the credentials. */
const REQUEST_FIELDS = {
  email: 'authname',
  password: 'password',
} as const;

/** Extra fields the applogin endpoint expects alongside the credentials. */
const EXTRA_FIELDS = {
  configid: 1,
  userfcmtoken: null,
} as const;

/**
 * Field names read from the user record (login response `details`, else the
 * tenant user list). Verified against the live `users/getallusers` response.
 */
const RESPONSE_FIELDS = {
  roleid: 'roleid',
  firstname: 'firstname',
  fullname: 'fullname',
  email: 'email',
  contactno: 'contactno',
  userid: 'userid',
  // Tenant binding: the merchant this user belongs to. Drives every Fiesta query
  // scope (locations, summaries, orders, stock) so a user only sees their own
  // tenant's data — not the shared sandbox tenant the constant defaults to.
  tenantid: 'tenantid',
  // Store binding: a non-admin user is allocated to an app-location via
  // applocationid; `applocation` is its human-readable name (e.g. "Coimbatore").
  // locationid/locationname are captured when present (often 0/absent on the
  // user record — the outlet is resolved from the tenant locations list).
  applocationid: 'applocationid',
  applocation: 'applocation',
  locationid: 'locationid',
  locationname: 'locationname',
  // Server-derived flag, checked before roleid: a platform operator who
  // isn't scoped to any tenant. Never inferred client-side (a roleid the
  // client could send/expect would be spoofable) — this comes back on the
  // login row itself, gated server-side in AppLogin.
  issuperadmin: 'issuperadmin',
} as const;

/**
 * roleids that land on the ADMIN dashboard; everyone else lands on the user page.
 * From fiestaApi.roleName(): 1 = Owner, 2 = Manager, 3 = Admin, 4 = Staff,
 * 5 = Rider, 6 = Cashier.
 */
const ADMIN_ROLE_IDS = new Set<number>([1, 3]);

// ──────────────────────────────────────────────────────────────────────────────

export type LoginRole = 'admin' | 'user' | 'super_admin';

export interface AuthUser {
  role: LoginRole;
  name: string;
  email: string;
  userid?: number;
  roleid?: number;
  /** Platform operator, not scoped to any tenant — see RESPONSE_FIELDS.issuperadmin. */
  issuperadmin?: boolean;
  /** Phone number on the user record. */
  contactno?: string;
  /** The merchant/tenant this user belongs to — scopes every Fiesta query. */
  tenantid?: number;
  /** The app-location this user is allocated to. */
  applocationid?: number;
  /** App-location / zone name on the user record (e.g. "Coimbatore"). */
  applocation?: string;
  /** Outlet/location id, when the record carries it directly (often 0). */
  locationid?: number;
  /** Outlet display name, when the record carries it directly. */
  locationname?: string;
}

/** Map a numeric roleid to the workspace the user is allowed into. */
export function roleFromRoleId(roleid: number): LoginRole {
  return ADMIN_ROLE_IDS.has(roleid) ? 'admin' : 'user';
}

/** Build a friendly display name from the response, falling back to the email. */
function displayName(row: Record<string, unknown>, email: string): string {
  const full = str(row[RESPONSE_FIELDS.fullname]).trim();
  if (full) return full;
  const first = str(row[RESPONSE_FIELDS.firstname]).trim();
  if (first) return first;
  // Fallback: derive from the email's local part.
  return (
    email
      .split('@')[0]
      .split(/[._-]+/)
      .filter(Boolean)
      .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
      .join(' ') || 'Account'
  );
}

/** The verified-login response: the user record (if any) + the resolved email. */
export interface LoginResult {
  /** User record from the login response `details`, or null if it carried none. */
  row: Row | null;
  /** Whether `row` already includes the role (so no tenant-list lookup is needed). */
  hasRole: boolean;
  email: string;
}

/**
 * Thrown by `loginRequest` when the account exists but has never had a password
 * set (a freshly onboarded tenant admin or store login — see AppLogin's
 * "No password set" branch, `{ code: 409, status: true, details: { setup: true } }`).
 * Distinct from a plain login failure: the caller should route to a
 * create-password step, not show an error.
 */
export class PasswordSetupRequiredError extends Error {
  userid: number;
  constructor(userid: number, message = 'Please set up a password to continue.') {
    super(message);
    this.name = 'PasswordSetupRequiredError';
    this.userid = userid;
  }
}

/**
 * POST the credentials to the Fiesta web-login endpoint. Resolves with the raw
 * user record on success; throws an Error with a user-facing message on invalid
 * credentials or any failure. Role resolution is left to the caller so the
 * (optional) tenant-user lookup can go through React Query — see `useLogin`.
 */
export async function loginRequest(email: string, password: string): Promise<LoginResult> {
  const trimmedEmail = email.trim();

  let res: Response;
  try {
    res = await fetch(LOGIN_ENDPOINT, {
      method: 'POST',
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({
        [REQUEST_FIELDS.email]: trimmedEmail,
        [REQUEST_FIELDS.password]: password,
        ...EXTRA_FIELDS,
      }),
    });
  } catch {
    throw new Error('Unable to reach the login service. Check your connection and try again.');
  }

  // Parse the JSON body (may be absent on some error responses).
  const json = (await res.json().catch(() => null)) as
    | { code?: number; status?: boolean; message?: string; details?: unknown }
    | null;

  // Account exists but has no password yet — server-side this fires before it
  // even looks at the password the caller sent, so it happens on any attempt
  // (including checkEmailRequest's password-less probe). `status: true` here,
  // so this must be checked before the generic status===false failure branch.
  const details = json?.details as { userid?: number; setup?: boolean } | undefined;
  if (json?.code === 409 && details?.setup === true && typeof details.userid === 'number') {
    throw new PasswordSetupRequiredError(details.userid, json.message);
  }

  // Failure: HTTP error, or the Fiesta `status: false` envelope (e.g. wrong
  // email/password → { code: 409, message: "Invalid Email", status: false }).
  if (!res.ok || (json && json.status === false)) {
    throw new Error(json?.message?.trim() || 'Incorrect email or password.');
  }

  const row = firstRow<Row>(json);
  const resolvedEmail = (row && str(row[RESPONSE_FIELDS.email]).trim()) || trimmedEmail;
  return { row, hasRole: Boolean(row && row[RESPONSE_FIELDS.roleid] != null), email: resolvedEmail };
}

export interface EmailCheckResult {
  exists: boolean;
  /** True when the account has never had a password set — skip straight to
   * the create-password step instead of asking for a password it doesn't
   * have yet (see PasswordSetupRequiredError for why this is detectable
   * even from this password-less probe request). */
  needsSetup: boolean;
  userid?: number;
}

/**
 * Checks if the email/authname exists and is registered by sending email and configid: 1.
 */
export async function checkEmailRequest(email: string): Promise<EmailCheckResult> {
  const trimmedEmail = email.trim();

  let res: Response;
  try {
    res = await fetch(LOGIN_ENDPOINT, {
      method: 'POST',
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({
        [REQUEST_FIELDS.email]: trimmedEmail,
        ...EXTRA_FIELDS,
      }),
    });
  } catch {
    throw new Error('Unable to reach the login service. Check your connection and try again.');
  }

  const json = (await res.json().catch(() => null)) as
    | { code?: number; status?: boolean; message?: string; details?: unknown }
    | null;

  // Account exists but has no password yet — fires regardless of the
  // (absent) password in this probe request. See loginRequest's identical
  // check for the full explanation.
  const details = json?.details as { userid?: number; setup?: boolean } | undefined;
  if (json?.code === 409 && details?.setup === true && typeof details.userid === 'number') {
    return { exists: true, needsSetup: true, userid: details.userid };
  }

  // A 409 Invalid Email code means the email does not exist.
  if (json && json.status === false && (json.code === 409 || json.message?.trim().toLowerCase() === 'invalid email')) {
    return { exists: false, needsSetup: false };
  }

  // Any other result (such as 403 Unauthorized email) means the email is registered.
  return { exists: true, needsSetup: false };
}


/** Find a user in the tenant user list by email or authname (case-insensitive). */
export function matchTenantUser(users: Row[], email: string): Row | null {
  const target = email.toLowerCase();
  const prefix = target.split('@')[0] + '@';
  
  return (
    users.find(
      (u) =>
        str(u[RESPONSE_FIELDS.email]).toLowerCase() === target ||
        str(u.authname).toLowerCase() === target
    ) ?? 
    users.find(
      (u) =>
        str(u[RESPONSE_FIELDS.email]).toLowerCase().startsWith(prefix) ||
        str(u.authname).toLowerCase().startsWith(prefix)
    ) ?? null
  );
}

/** Assemble the final AuthUser (role + identity) from a resolved user record. */
export function buildAuthUser(row: Row | null, email: string): AuthUser {
  const roleid = row ? num(row[RESPONSE_FIELDS.roleid]) : 0;
  const issuperadmin = Boolean(row && row[RESPONSE_FIELDS.issuperadmin]);
  const applocation = row ? str(row[RESPONSE_FIELDS.applocation]).trim() : '';
  const locationname = row ? str(row[RESPONSE_FIELDS.locationname]).trim() : '';
  const contactno = row ? str(row[RESPONSE_FIELDS.contactno]).trim() : '';
  return {
    // issuperadmin is checked first: it's a server-derived flag from the
    // login row, not something the client asserts, so it takes priority
    // over the roleid-based admin/user split.
    role: issuperadmin ? 'super_admin' : roleFromRoleId(roleid),
    name: displayName(row ?? {}, email),
    email,
    userid: row && row[RESPONSE_FIELDS.userid] != null ? num(row[RESPONSE_FIELDS.userid]) : undefined,
    roleid,
    issuperadmin: issuperadmin || undefined,
    contactno: contactno || undefined,
    tenantid: row && row[RESPONSE_FIELDS.tenantid] != null ? num(row[RESPONSE_FIELDS.tenantid]) : undefined,
    applocationid:
      row && row[RESPONSE_FIELDS.applocationid] != null ? num(row[RESPONSE_FIELDS.applocationid]) : undefined,
    applocation: applocation || undefined,
    locationid: row && row[RESPONSE_FIELDS.locationid] != null ? num(row[RESPONSE_FIELDS.locationid]) : undefined,
    locationname: locationname || undefined,
  };
}
