/**
 * User identity upsert + stable per-member color assignment.
 *
 * Identity is keyed on oidc_iss + oidc_sub — never email (D-10).
 * Color is auto-assigned from a curated palette on first login, round-robin
 * by join order (D-06). Stable across sessions: re-upsert returns same row.
 *
 * Source: RESEARCH.md § "User upsert with color assignment"
 */

import { and, eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { users } from '../db/schema.js';

/**
 * Accessible, visually-distinct palette for per-member member-color assignment.
 * A new member is given the first entry not already in use (see upsertUser).
 *
 * Ordering matters: the SHARED-family calendar is reserved rose (#F25C7A, D-06),
 * so the warm near-rose hues (coral, amber) are placed LAST. Early members get
 * cool colors (blue, green, teal) that read clearly distinct from the shared
 * lane — otherwise a member's coral was mistaken for the shared rose.
 * Values are Claude's choice per D-06.
 */
export const COLOR_PALETTE: string[] = [
  '#4A90D9', // calm blue
  '#5BA85A', // forest green
  '#3AAFA9', // teal
  '#9B6DC5', // soft purple
  '#E8A840', // warm amber  (near shared rose — assigned only after cool colors)
  '#E8734A', // warm coral  (closest to shared rose — assigned last)
];

/** Coerce an OIDC claim to a trimmed non-empty string, else undefined. */
const claimStr = (v: unknown): string | undefined =>
  typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined;

/**
 * Derive the best available display name from OIDC claims, in preference order:
 *   1. name              — full name set by the IdP (most human-friendly)
 *   2. preferred_username — often the login handle; still readable
 *   3. email             — readable but reveals contact info; acceptable fallback
 *   4. null              — no usable claim; the UI degrades to a generic "Member"
 *
 * Returns null (NOT a synthetic `Member <sub>`) when no real claim is present so
 * we never persist an ugly sub-derived string. Whether Authelia emits
 * name/preferred_username/email in the ID TOKEN (not just at the userinfo
 * endpoint) is an operator config concern: Authelia 4.39+ requires a
 * `claims_policies` entry adding those claims to `id_token` for this client,
 * because @hono/oidc-auth reads ID-token claims only (no userinfo fetch). Until
 * that is set, every claim here is absent and the legend shows "Member".
 *
 * Shared by every call site that upserts a user (me.ts, events.ts resolveUserId)
 * so the write-path upsert agrees with /api/me.
 */
export function deriveDisplayName(claims: {
  name?: unknown;
  preferred_username?: unknown;
  email?: unknown;
}): string | null {
  return (
    claimStr(claims.name) ?? claimStr(claims.preferred_username) ?? claimStr(claims.email) ?? null
  );
}

/**
 * Upsert a user by their OIDC identity (iss + sub).
 *
 * - If a row with matching (oidc_iss, oidc_sub) exists, return it unchanged.
 * - Otherwise, count current users to pick the next palette color, insert a
 *   new row, then re-select and return it.
 *
 * Never keys on email or displayName for identity. displayName is stored as a
 * display hint only and may change without affecting identity.
 */
export async function upsertUser(oidcIss: string, oidcSub: string, displayName?: string | null) {
  // 1. Look up by composite identity key (iss + sub) — never email
  const existing = await db
    .select()
    .from(users)
    .where(and(eq(users.oidcIss, oidcIss), eq(users.oidcSub, oidcSub)))
    .limit(1);

  if (existing[0]) {
    // Track the IdP display name authoritatively: when the caller supplies a
    // real (non-null) name that differs from what's stored, update it. This both
    // corrects rows created before robust claim derivation (blank → name) and
    // self-heals once an operator adds the name/email claims to Authelia's ID
    // token (e.g. a stale "Member …"/email → the real name) — no DB surgery.
    // A null displayName (no usable claim this request) never overwrites a good
    // stored value.
    if (displayName != null && displayName !== existing[0].displayName) {
      await db.update(users).set({ displayName }).where(eq(users.id, existing[0].id));
      return { ...existing[0], displayName };
    }
    return existing[0];
  }

  // 2. Assign the first palette color NOT already in use by another member.
  //    A plain COUNT(*) % palette collides under deletions: a deleted user
  //    shifts the count so the next insert reuses an in-use slot (observed in
  //    Gate 2 — two members both got #E8734A). Selecting the first unused color
  //    guarantees distinct, stable colors for up to COLOR_PALETTE.length members
  //    (AUTH-03). Falls back to round-robin by count only once the palette is
  //    exhausted (more members than colors).
  const usedRows = await db.select({ color: users.color }).from(users);
  const usedColors = new Set(usedRows.map((r) => r.color));
  const color =
    COLOR_PALETTE.find((c) => !usedColors.has(c)) ??
    COLOR_PALETTE[usedColors.size % COLOR_PALETTE.length];

  // 3. Insert new user row
  // mysql2 has no RETURNING clause — use $returningId() then re-select
  const [inserted] = await db
    .insert(users)
    .values({
      oidcIss,
      oidcSub,
      displayName: displayName ?? null,
      color,
    })
    .$returningId();

  // 4. Re-select to return the full typed row
  const [newUser] = await db.select().from(users).where(eq(users.id, inserted.id)).limit(1);

  return newUser;
}
