/**
 * Federation grant scope schema and validator.
 *
 * Source of truth: docs/federation/PRD.md §8.1
 *
 * This module is intentionally pure — no DB, no NestJS, no CA wiring.
 * It is reusable from grant CRUD (M2-06) and scope enforcement (M3+).
 */

import { z } from 'zod';

// ---------------------------------------------------------------------------
// Allowlist of federation resources (canonical — M3+ will extend this list)
// ---------------------------------------------------------------------------
export const FEDERATION_RESOURCE_VALUES = [
  'tasks',
  'notes',
  'memory',
  'credentials',
  'api_keys',
] as const;

export type FederationResource = (typeof FEDERATION_RESOURCE_VALUES)[number];

/**
 * Sensitive resources require explicit admin approval (PRD §8.4).
 * The parser warns when these appear in `resources`; M2-06 grant CRUD
 * will add a hard gate on top of this warning.
 */
const SENSITIVE_RESOURCES: ReadonlySet<FederationResource> = new Set(['credentials', 'api_keys']);

// ---------------------------------------------------------------------------
// Sub-schemas
// ---------------------------------------------------------------------------

const ResourceArraySchema = z
  .array(z.enum(FEDERATION_RESOURCE_VALUES))
  .nonempty({ message: 'resources must contain at least one value' })
  .refine((arr) => new Set(arr).size === arr.length, {
    message: 'resources must not contain duplicate values',
  });

const ResourceFilterSchema = z.object({
  include_teams: z.array(z.string()).optional(),
  include_personal: z.boolean().default(true),
});

// ---------------------------------------------------------------------------
// Top-level schema
// ---------------------------------------------------------------------------

export const FederationScopeSchema = z
  .object({
    resources: ResourceArraySchema,

    excluded_resources: z
      .array(z.enum(FEDERATION_RESOURCE_VALUES))
      .default([])
      .refine((arr) => new Set(arr).size === arr.length, {
        message: 'excluded_resources must not contain duplicate values',
      }),

    filters: z.record(z.string(), ResourceFilterSchema).optional(),

    max_rows_per_query: z
      .number()
      .int({ message: 'max_rows_per_query must be an integer' })
      .min(1, { message: 'max_rows_per_query must be at least 1' })
      .max(10000, { message: 'max_rows_per_query must be at most 10000' }),
  })
  .superRefine((data, ctx) => {
    const resourceSet = new Set(data.resources);

    // Intersection guard: a resource cannot be both granted and excluded
    for (const r of data.excluded_resources) {
      if (resourceSet.has(r)) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: `Resource "${r}" appears in both resources and excluded_resources`,
          path: ['excluded_resources'],
        });
      }
    }

    // Filter keys must be a subset of resources
    if (data.filters) {
      for (const key of Object.keys(data.filters)) {
        if (!resourceSet.has(key as FederationResource)) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: `filters key "${key}" references a resource not present in resources`,
            path: ['filters', key],
          });
        }
      }
    }
  });

export type FederationScope = z.infer<typeof FederationScopeSchema>;

// ---------------------------------------------------------------------------
// Error class
// ---------------------------------------------------------------------------

export class FederationScopeError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'FederationScopeError';
  }
}

// ---------------------------------------------------------------------------
// Typed parser
// ---------------------------------------------------------------------------

/**
 * Parse and validate an unknown value as a FederationScope.
 *
 * Throws `FederationScopeError` with aggregated Zod issues on failure.
 *
 * Emits `console.warn` when sensitive resources (`credentials`, `api_keys`)
 * are present in `resources` — per PRD §8.4, these require explicit admin
 * approval. M2-06 grant CRUD will add a hard gate on top of this warning.
 */
export function parseFederationScope(input: unknown): FederationScope {
  const result = FederationScopeSchema.safeParse(input);

  if (!result.success) {
    const issues = result.error.issues
      .map((e) => `  - [${e.path.join('.') || 'root'}] ${e.message}`)
      .join('\n');
    throw new FederationScopeError(`Invalid federation scope:\n${issues}`);
  }

  const scope = result.data;

  // Sentinel warning for sensitive resources (PRD §8.4)
  for (const resource of scope.resources) {
    if (SENSITIVE_RESOURCES.has(resource)) {
      console.warn(
        `[FederationScope] WARNING: scope grants sensitive resource "${resource}". Per PRD §8.4 this requires explicit admin approval and is logged.`,
      );
    }
  }

  return scope;
}
