import type { Team, TeamRole } from './ontology';
import { TEAMS } from './ontology';

export const API_KEY_SCOPES = ['read', 'write'] as const;
export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];

export const CAPABILITIES = [
  'deal:write',
  'commitment:write',
  'contract:sign',
  'activity:write',
  'data:import',
  'fact:review',
  'integration:connect',
  'motion:write',
  'motion:publish',
  'settings:admin',
  'book:read',
  'economics:read',
  'team:read',
] as const;
export type Capability = (typeof CAPABILITIES)[number];

/**
 * Writes, authorised per team.
 *
 * `data:import`, `fact:review` and `integration:connect` were one capability
 * until an audit pointed out they are three different authorities: rewriting
 * five thousand rows, accepting an agent's claim about a named person, and
 * handing PIG a third party's OAuth token. Someone trusted with the third is
 * not thereby trusted with the first.
 */
export const TEAM_CAPABILITIES = [
  'deal:write',
  'commitment:write',
  'contract:sign',
  'activity:write',
  'data:import',
  'fact:review',
  'integration:connect',
  'motion:write',
  'motion:publish',
] as const satisfies readonly Capability[];
export type TeamCapability = (typeof TEAM_CAPABILITIES)[number];

/**
 * Reads, authorised platform-wide.
 *
 * Deliberately NOT team-scoped, and the distinction is load-bearing. Every read
 * endpoint returns the whole book — every account, every contract, every
 * block — because no row-level team filter exists anywhere in the query layer.
 * A team-scoped read grant would therefore be a lie the guard could not
 * enforce: it would say "demand only" while the handler returned supply too.
 * The honest model is that a read capability is held or it is not, and the
 * *role* required to hold it is what separates the roster from the cost book.
 */
export const READ_CAPABILITIES = [
  'book:read',
  'economics:read',
  'team:read',
] as const satisfies readonly Capability[];
export type ReadCapability = (typeof READ_CAPABILITIES)[number];

export type GlobalCapability = Exclude<Capability, TeamCapability | ReadCapability>;
export type WriteCapability = TeamCapability | GlobalCapability;

export interface PermissionSubject {
  isPlatformAdmin: boolean;
  teams: readonly { team: Team; role: TeamRole }[];
}

export interface PermissionGrant {
  capability: Capability;
  /** Null is a platform-wide grant; ordinary role grants always name a team. */
  team: Team | null;
}

interface CapabilityRule {
  teams: readonly Team[];
  minimumRole: TeamRole;
}

/**
 * The role policy is shared by API and browser code so controls cannot drift
 * from server enforcement as new write paths are added.
 */
export const TEAM_CAPABILITY_RULES: Readonly<Record<TeamCapability, CapabilityRule>> = {
  'deal:write': { teams: ['supply', 'demand'], minimumRole: 'member' },
  'commitment:write': { teams: ['supply'], minimumRole: 'lead' },
  'contract:sign': { teams: ['supply', 'demand'], minimumRole: 'admin' },
  // Logging a call is the lightest write in the product and every member does
  // it, but it still moves `accounts.lastActivityAt`, which drives the account
  // list ordering — so a viewer must not be able to reorder someone's day.
  'activity:write': { teams: TEAMS, minimumRole: 'member' },
  'data:import': { teams: TEAMS, minimumRole: 'admin' },
  // Unchanged from when this was `data:import` on research: fact review is the
  // research team's judgement about evidence, not a commercial authority.
  'fact:review': { teams: ['research'], minimumRole: 'admin' },
  'integration:connect': { teams: TEAMS, minimumRole: 'admin' },
  // Every team runs a motion, which is why this is not scoped to the
  // commercial pair: research authors reference architectures and technical
  // narratives as much as demand authors proposals.
  'motion:write': { teams: TEAMS, minimumRole: 'member' },
  // Publishing is what everybody else copies next quarter — a lead's
  // judgement, not a member's. Authoring privately stays open to everyone
  // precisely so this gate can be a real one.
  'motion:publish': { teams: TEAMS, minimumRole: 'lead' },
};

/**
 * `economics:read` is the one that matters. Supplier cost per GPU-hour and the
 * break-even price ARE the business; a research contractor consuming capacity
 * internally has no reason to see what we pay for it, and neither has an
 * analyst hired to read the book. Commercial members do: you cannot price a
 * deal without knowing what the block cost.
 */
export const READ_CAPABILITY_RULES: Readonly<Record<ReadCapability, CapabilityRule>> = {
  'book:read': { teams: TEAMS, minimumRole: 'viewer' },
  'economics:read': { teams: ['supply', 'demand'], minimumRole: 'member' },
  'team:read': { teams: TEAMS, minimumRole: 'viewer' },
};

/**
 * Rank, not identity: every rule is "at or above". `viewer` is deliberately
 * below `member` so that adding it grants nothing that was not already
 * granted — every existing rule starts at `member` or higher.
 */
const ROLE_RANK: Readonly<Record<TeamRole, number>> = {
  viewer: 0,
  member: 1,
  lead: 2,
  admin: 3,
};

/**
 * Rank comparison, exported because it was duplicated in `hasTeamAccess` and
 * the copy went stale the moment `viewer` was added — silently ranking an
 * unknown role as `undefined >= n`, which is `false` for every threshold and
 * would have locked viewers out of nothing while looking correct.
 */
export function roleMeets(role: TeamRole, minimumRole: TeamRole): boolean {
  return ROLE_RANK[role] >= ROLE_RANK[minimumRole];
}

export function isTeamCapability(capability: Capability): capability is TeamCapability {
  return (TEAM_CAPABILITIES as readonly Capability[]).includes(capability);
}

export function isReadCapability(capability: Capability): capability is ReadCapability {
  return (READ_CAPABILITIES as readonly Capability[]).includes(capability);
}

/** Capabilities that are neither team-scoped nor reads: platform administration. */
export const GLOBAL_CAPABILITIES = CAPABILITIES.filter(
  (capability): capability is GlobalCapability =>
    !isTeamCapability(capability) && !isReadCapability(capability),
);

function meetsRule(subject: PermissionSubject, rule: CapabilityRule): boolean {
  return subject.teams.some(
    (membership) =>
      rule.teams.includes(membership.team) &&
      ROLE_RANK[membership.role] >= ROLE_RANK[rule.minimumRole],
  );
}

/** Team-scoped write grants. One entry per (capability, team) that qualifies. */
export function resolveWritePermissionGrants(subject: PermissionSubject): PermissionGrant[] {
  if (subject.isPlatformAdmin) {
    return [...TEAM_CAPABILITIES, ...GLOBAL_CAPABILITIES].map((capability) => ({
      capability,
      team: null,
    }));
  }

  const grants: PermissionGrant[] = [];
  for (const capability of TEAM_CAPABILITIES) {
    const rule = TEAM_CAPABILITY_RULES[capability];
    for (const membership of subject.teams) {
      if (
        rule.teams.includes(membership.team) &&
        ROLE_RANK[membership.role] >= ROLE_RANK[rule.minimumRole]
      ) {
        grants.push({ capability, team: membership.team });
      }
    }
  }
  return grants;
}

/** Read grants, always platform-wide — see `READ_CAPABILITIES`. */
export function resolveReadPermissionGrants(subject: PermissionSubject): PermissionGrant[] {
  return READ_CAPABILITIES.filter(
    (capability) =>
      subject.isPlatformAdmin || meetsRule(subject, READ_CAPABILITY_RULES[capability]),
  ).map((capability) => ({ capability, team: null }));
}

export function resolvePermissionGrants(subject: PermissionSubject): PermissionGrant[] {
  return [...resolveReadPermissionGrants(subject), ...resolveWritePermissionGrants(subject)];
}

export function permissionGranted(
  grants: readonly PermissionGrant[],
  capability: Capability,
  team?: Team,
): boolean {
  return grants.some(
    (grant) =>
      grant.capability === capability &&
      (grant.team === null || team === undefined || grant.team === team),
  );
}
