// Per-section rights for developer API tokens (2026-10-12).
// Each section: "none" | "read" | "write" (write includes read).
// Tokens without stored scopes keep their old behaviour (see legacyScopes).

export const API_SECTIONS = [
  { id: "tasks", label: "Tasks", writable: true },
  { id: "projects", label: "Projects", writable: true },
  { id: "events", label: "Calendar", writable: true },
  { id: "journal", label: "Journal", writable: true },
  { id: "notes", label: "Notes", writable: true },
  { id: "leads", label: "Leads", writable: true },
  { id: "reminders", label: "Reminders", writable: true },
  { id: "finances", label: "Finances", writable: true },
  // Vault (documents) is read-only through the API: list of documents, no upload/delete
  { id: "vault", label: "Vault (documents)", writable: false },
] as const;

export type ApiSection = (typeof API_SECTIONS)[number]["id"];
export type ScopeLevel = "none" | "read" | "write";
export type TokenScopes = Record<ApiSection, ScopeLevel>;

const SECTION_IDS = API_SECTIONS.map((s) => s.id) as ApiSection[];
const WRITABLE = new Set<string>(API_SECTIONS.filter((s) => s.writable).map((s) => s.id));

export function isApiSection(value: unknown): value is ApiSection {
  return typeof value === "string" && (SECTION_IDS as string[]).includes(value);
}

/** Old tokens (before per-section rights): everything except finances/vault follows access_level. */
export function legacyScopes(accessLevel: string, financeAccess?: string | null): TokenScopes {
  const base: ScopeLevel = accessLevel === "read_write" ? "write" : "read";
  const out = Object.fromEntries(SECTION_IDS.map((id) => [id, base])) as TokenScopes;
  out.finances =
    financeAccess === "read_write" && accessLevel === "read_write"
      ? "write"
      : financeAccess === "read" || financeAccess === "read_write"
        ? "read"
        : "none";
  out.vault = "none";
  return out;
}

/** Validate scopes from a request body. Unknown sections are rejected, missing ones become "none". */
export function parseScopes(input: unknown): { ok: true; value: TokenScopes } | { ok: false; error: string } {
  if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, error: "scopes must be an object" };
  const raw = input as Record<string, unknown>;
  for (const key of Object.keys(raw)) {
    if (!isApiSection(key)) return { ok: false, error: `unknown section: ${key}` };
  }
  const out = {} as TokenScopes;
  for (const id of SECTION_IDS) {
    const level = raw[id] ?? "none";
    if (level !== "none" && level !== "read" && level !== "write") return { ok: false, error: `invalid level for ${id}` };
    if (level === "write" && !WRITABLE.has(id)) return { ok: false, error: `${id} can only be read through the API` };
    out[id] = level;
  }
  return { ok: true, value: out };
}

/** Stored scopes win; otherwise legacy behaviour. Stored values are re-validated (defensive). */
export function effectiveScopes(token: { access_level: string; finance_access?: string | null; scopes?: unknown }): TokenScopes {
  if (token.scopes) {
    const parsed = parseScopes(token.scopes);
    if (parsed.ok) return parsed.value;
    return Object.fromEntries(SECTION_IDS.map((id) => [id, "none"])) as TokenScopes; // corrupt → no access
  }
  return legacyScopes(token.access_level, token.finance_access);
}

export function allows(scopes: TokenScopes, section: ApiSection, need: "read" | "write") {
  const level = scopes[section];
  return need === "read" ? level === "read" || level === "write" : level === "write";
}

/** access_level kept in sync for old code paths: read_write if any section can write. */
export function accessLevelFor(scopes: TokenScopes): "read_only" | "read_write" {
  return SECTION_IDS.some((id) => scopes[id] === "write") ? "read_write" : "read_only";
}

export function presetScopes(kind: "none" | "read" | "write"): TokenScopes {
  return Object.fromEntries(
    API_SECTIONS.map((s) => [s.id, kind === "write" && !s.writable ? "read" : kind])
  ) as TokenScopes;
}
