export interface OidcMetadata {
  issuer: string;
  authorization_endpoint: string;
  token_endpoint: string;
  userinfo_endpoint?: string;
  jwks_uri: string;
  scopes_supported?: string[];
}

function secureUrl(value: string, field: string): URL {
  const url = new URL(value);
  if (url.protocol !== "https:" && url.hostname !== "localhost")
    throw new Error(`WRN-IDENTITY-URL: ${field} must use HTTPS.`);
  return url;
}

export async function discoverOidc(
  issuer: string,
  options: { fetch?: typeof fetch } = {},
): Promise<OidcMetadata> {
  const expected = secureUrl(issuer, "issuer");
  const endpoint = new URL(
    ".well-known/openid-configuration",
    `${expected.href.replace(/\/$/, "")}/`,
  );
  const response = await (options.fetch ?? fetch)(endpoint, {
    headers: { accept: "application/json" },
  });
  if (!response.ok) throw new Error(`WRN-OIDC-DISCOVERY: provider returned ${response.status}.`);
  const metadata = (await response.json()) as OidcMetadata;
  if (metadata.issuer.replace(/\/$/, "") !== expected.href.replace(/\/$/, ""))
    throw new Error("WRN-OIDC-ISSUER: discovered issuer does not match configuration.");
  for (const field of ["authorization_endpoint", "token_endpoint", "jwks_uri"] as const)
    secureUrl(metadata[field], field);
  return metadata;
}

export function oidcAuthorizationUrl(
  metadata: OidcMetadata,
  input: {
    clientId: string;
    redirectUri: string;
    state: string;
    nonce: string;
    codeChallenge: string;
    scopes?: string[];
  },
): string {
  const url = secureUrl(metadata.authorization_endpoint, "authorization_endpoint");
  const values = {
    response_type: "code",
    client_id: input.clientId,
    redirect_uri: input.redirectUri,
    scope: (input.scopes ?? ["openid", "profile", "email"]).join(" "),
    state: input.state,
    nonce: input.nonce,
    code_challenge: input.codeChallenge,
    code_challenge_method: "S256",
  };
  for (const [key, value] of Object.entries(values)) url.searchParams.set(key, value);
  return url.href;
}

export interface EnterpriseIdentity {
  externalId: string;
  username: string;
  displayName?: string;
  email?: string;
  groups: string[];
  active: boolean;
  attributes?: Record<string, unknown>;
}

export interface SamlAssertion {
  id: string;
  issuer: string;
  audience: string;
  recipient: string;
  expiresAt: number;
  identity: EnterpriseIdentity;
}
export interface SamlAdapter {
  createLoginRequest(input: {
    requestId: string;
    callbackUrl: string;
    relayState: string;
  }): Promise<string> | string;
  verifySignedResponse(response: string): Promise<SamlAssertion>;
}
export interface ReplayStore {
  consume(id: string, expiresAt: number): Promise<boolean>;
}
export function memoryReplayStore(now: () => number = Date.now): ReplayStore {
  const ids = new Map<string, number>();
  return {
    async consume(id, expiresAt) {
      for (const [key, expiry] of ids) if (expiry <= now()) ids.delete(key);
      if (ids.has(id)) return false;
      ids.set(id, expiresAt);
      return true;
    },
  };
}
export function createSamlFederation(options: {
  adapter: SamlAdapter;
  issuer: string;
  audience: string;
  recipient: string;
  replayStore?: ReplayStore;
  now?: () => number;
}) {
  const replay = options.replayStore ?? memoryReplayStore(options.now);
  const now = options.now ?? Date.now;
  return {
    login: options.adapter.createLoginRequest.bind(options.adapter),
    async callback(encodedResponse: string): Promise<EnterpriseIdentity> {
      const assertion = await options.adapter.verifySignedResponse(encodedResponse);
      if (
        assertion.issuer !== options.issuer ||
        assertion.audience !== options.audience ||
        assertion.recipient !== options.recipient
      )
        throw new Error("WRN-SAML-BOUNDARY: issuer, audience, or recipient mismatch.");
      if (assertion.expiresAt <= now()) throw new Error("WRN-SAML-EXPIRED: assertion has expired.");
      if (!(await replay.consume(assertion.id, assertion.expiresAt)))
        throw new Error("WRN-SAML-REPLAY: assertion was already consumed.");
      return assertion.identity;
    },
  };
}

export interface DirectoryAdapter {
  kind: "ldap" | "active-directory";
  search(input: {
    baseDn: string;
    filter: string;
    attributes: string[];
    signal?: AbortSignal;
  }): Promise<EnterpriseIdentity[]>;
  authenticate?(
    username: string,
    password: string,
    signal?: AbortSignal,
  ): Promise<EnterpriseIdentity | null>;
}
export async function syncDirectory(
  adapter: DirectoryAdapter,
  options: {
    baseDn: string;
    filter?: string;
    attributes?: string[];
    signal?: AbortSignal;
    upsert: (identity: EnterpriseIdentity) => void | Promise<void>;
    disableMissing?: (externalIds: string[]) => void | Promise<void>;
  },
) {
  const identities = await adapter.search({
    baseDn: options.baseDn,
    filter: options.filter ?? "(objectClass=person)",
    attributes: options.attributes ?? ["uid", "mail", "displayName", "memberOf"],
    signal: options.signal,
  });
  for (const identity of identities) await options.upsert(identity);
  await options.disableMissing?.(identities.map((identity) => identity.externalId));
  return { provider: adapter.kind, synchronized: identities.length };
}

export interface ScimUser extends EnterpriseIdentity {
  id: string;
  /** RFC 7643 field accepted at the HTTP boundary. */
  userName?: string;
  schemas?: string[];
}
export interface ScimStore {
  list(): Promise<ScimUser[]>;
  get(id: string): Promise<ScimUser | null>;
  create(user: Omit<ScimUser, "id">): Promise<ScimUser>;
  update(id: string, user: Partial<ScimUser>): Promise<ScimUser | null>;
  delete(id: string): Promise<boolean>;
}
export function memoryScimStore(): ScimStore {
  const users = new Map<string, ScimUser>();
  return {
    async list() {
      return [...users.values()];
    },
    async get(id) {
      return users.get(id) ?? null;
    },
    async create(user) {
      const value = { ...user, id: crypto.randomUUID() };
      users.set(value.id, value);
      return value;
    },
    async update(id, patch) {
      const current = users.get(id);
      if (!current) return null;
      const value = { ...current, ...patch, id };
      users.set(id, value);
      return value;
    },
    async delete(id) {
      return users.delete(id);
    },
  };
}
function constantTimeText(left: string, right: string): boolean {
  const a = new TextEncoder().encode(left);
  const b = new TextEncoder().encode(right);
  let mismatch = a.length ^ b.length;
  for (let index = 0; index < Math.max(a.length, b.length); index++)
    mismatch |= (a[index] ?? 0) ^ (b[index] ?? 0);
  return mismatch === 0;
}
const SCIM_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
export function createScimHandler(options: {
  store: ScimStore;
  bearerToken: string;
  basePath?: string;
  maxBodyBytes?: number;
}) {
  if (options.bearerToken.length < 24)
    throw new Error("WRN-SCIM-TOKEN: bearer token must contain at least 24 characters.");
  const base = options.basePath ?? "/scim/v2";
  return async (request: Request): Promise<Response> => {
    if (
      !constantTimeText(request.headers.get("authorization") ?? "", `Bearer ${options.bearerToken}`)
    )
      return Response.json({ detail: "Unauthorized" }, { status: 401 });
    const url = new URL(request.url);
    const match = new RegExp(
      `^${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/Users(?:/([^/]+))?$`,
    ).exec(url.pathname);
    if (!match) return Response.json({ detail: "Not found" }, { status: 404 });
    const id = match[1];
    if (request.method === "GET" && !id) {
      const values = await options.store.list();
      return Response.json({
        schemas: [SCIM_SCHEMA],
        totalResults: values.length,
        startIndex: 1,
        itemsPerPage: values.length,
        Resources: values,
      });
    }
    if (request.method === "GET" && id) {
      const value = await options.store.get(id);
      return value ? Response.json(value) : Response.json({ detail: "Not found" }, { status: 404 });
    }
    if (["POST", "PUT", "PATCH"].includes(request.method)) {
      const text = await request.text();
      if (new TextEncoder().encode(text).byteLength > (options.maxBodyBytes ?? 64 * 1024))
        return Response.json({ detail: "Too large" }, { status: 413 });
      let body: ScimUser;
      try {
        body = JSON.parse(text) as ScimUser;
      } catch {
        return Response.json({ detail: "Invalid JSON" }, { status: 400 });
      }
      if (!body || typeof body.userName !== "string")
        return Response.json({ detail: "userName is required" }, { status: 400 });
      const normalized = {
        externalId: String(body.externalId ?? body.userName),
        username: body.userName,
        displayName: body.displayName,
        email: body.email,
        groups: Array.isArray(body.groups) ? body.groups : [],
        active: body.active !== false,
        attributes: body.attributes,
      };
      const value =
        request.method === "POST"
          ? await options.store.create(normalized)
          : id
            ? await options.store.update(id, normalized)
            : null;
      return value
        ? Response.json(value, { status: request.method === "POST" ? 201 : 200 })
        : Response.json({ detail: "Not found" }, { status: 404 });
    }
    if (request.method === "DELETE" && id)
      return new Response(null, { status: (await options.store.delete(id)) ? 204 : 404 });
    return new Response("Method Not Allowed", { status: 405 });
  };
}

export interface MachineCredential {
  id: string;
  ownerId: string;
  kind: "api-key" | "service-account";
  name: string;
  scopes: string[];
  secretHash: string;
  createdAt: number;
  expiresAt?: number;
  revokedAt?: number;
}
const hex = (bytes: Uint8Array) =>
  Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
async function hash(value: string): Promise<string> {
  return hex(
    new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))),
  );
}
export function createMachineIdentityManager(now: () => number = Date.now) {
  const records = new Map<string, MachineCredential>();
  return {
    async issue(input: {
      ownerId: string;
      name: string;
      scopes: string[];
      kind?: MachineCredential["kind"];
      expiresAt?: number;
    }) {
      const id = crypto.randomUUID();
      const secret = `wrn_${id.replaceAll("-", "")}_${hex(crypto.getRandomValues(new Uint8Array(24)))}`;
      const record: MachineCredential = {
        id,
        ownerId: input.ownerId,
        kind: input.kind ?? "api-key",
        name: input.name,
        scopes: [...new Set(input.scopes)].sort(),
        secretHash: await hash(secret),
        createdAt: now(),
        expiresAt: input.expiresAt,
      };
      records.set(id, record);
      return { secret, credential: { ...record, secretHash: "[REDACTED]" } };
    },
    async authenticate(secret: string, requiredScope?: string) {
      const digest = await hash(secret);
      for (const record of records.values())
        if (
          constantTimeText(record.secretHash, digest) &&
          !record.revokedAt &&
          (!record.expiresAt || record.expiresAt > now()) &&
          (!requiredScope || record.scopes.includes(requiredScope) || record.scopes.includes("*"))
        )
          return { ...record, secretHash: "[REDACTED]" };
      return null;
    },
    revoke(id: string) {
      const record = records.get(id);
      if (!record) return false;
      record.revokedAt = now();
      return true;
    },
    list(ownerId: string) {
      return [...records.values()]
        .filter((record) => record.ownerId === ownerId)
        .map((record) => ({ ...record, secretHash: "[REDACTED]" }));
    },
  };
}

export interface GovernanceEvent {
  id: string;
  type: string;
  subjectId: string;
  actorId?: string;
  createdAt: number;
  data?: Record<string, unknown>;
}
export function createGovernance(
  options: {
    now?: () => number;
    audit?: (event: GovernanceEvent) => void | Promise<void>;
    exportSubject?: (subjectId: string) => unknown | Promise<unknown>;
    deleteSubject?: (subjectId: string) => void | Promise<void>;
  } = {},
) {
  const now = options.now ?? Date.now;
  const consents = new Map<
    string,
    Map<string, { granted: boolean; version: string; at: number }>
  >();
  const approvals = new Map<
    string,
    {
      id: string;
      subjectId: string;
      action: "export" | "delete";
      status: "pending" | "approved" | "rejected";
      requestedAt: number;
      decidedAt?: number;
      decidedBy?: string;
    }
  >();
  const emit = async (
    type: string,
    subjectId: string,
    actorId?: string,
    data?: Record<string, unknown>,
  ) =>
    options.audit?.({ id: crypto.randomUUID(), type, subjectId, actorId, createdAt: now(), data });
  return {
    async consent(subjectId: string, purpose: string, granted: boolean, version: string) {
      const values = consents.get(subjectId) ?? new Map();
      const value = { granted, version, at: now() };
      values.set(purpose, value);
      consents.set(subjectId, values);
      await emit("consent.changed", subjectId, subjectId, { purpose, granted, version });
      return value;
    },
    consents(subjectId: string) {
      return Object.fromEntries(consents.get(subjectId) ?? []);
    },
    async request(subjectId: string, action: "export" | "delete") {
      const value = {
        id: crypto.randomUUID(),
        subjectId,
        action,
        status: "pending" as const,
        requestedAt: now(),
      };
      approvals.set(value.id, value);
      await emit(`privacy.${action}.requested`, subjectId);
      return value;
    },
    async decide(id: string, actorId: string, approved: boolean) {
      const request = approvals.get(id);
      if (!request || request.status !== "pending")
        throw new Error("WRN-GOVERNANCE-APPROVAL: request is missing or already decided.");
      const decision = {
        ...request,
        status: approved ? ("approved" as const) : ("rejected" as const),
        decidedAt: now(),
        decidedBy: actorId,
      };
      approvals.set(id, decision);
      let result: unknown;
      if (approved && request.action === "export")
        result = await options.exportSubject?.(request.subjectId);
      if (approved && request.action === "delete") await options.deleteSubject?.(request.subjectId);
      await emit(`privacy.${request.action}.${decision.status}`, request.subjectId, actorId);
      return { decision, result };
    },
    async enforceRetention(
      records: Array<{ subjectId: string; createdAt: number }>,
      maxAgeMs: number,
      remove: (record: { subjectId: string; createdAt: number }) => void | Promise<void>,
    ) {
      if (maxAgeMs < 0) throw new RangeError("retention duration must not be negative");
      const expired = records.filter((record) => record.createdAt + maxAgeMs <= now());
      for (const record of expired) {
        await remove(record);
        await emit("retention.deleted", record.subjectId, undefined, {
          createdAt: record.createdAt,
        });
      }
      return expired.length;
    },
  };
}
