import { z } from "zod/v4";
import { Log } from "./utils/log.js";
import { Context } from "./context.js";
import { ErrorCodes, VisibleError } from "./error.js";

export namespace Actor {
  export const User = z.object({
    type: z.literal("user"),
    properties: z.object({
      userID: z.string(),
      email: z.string(),
    }),
  });

  export type User = z.Infer<typeof User>;

  export const Steam = z.object({
    type: z.literal("steam"),
    properties: z.object({
      userID: z.string(),
      steamID: z.string(),
    }),
  });

  export type Steam = z.Infer<typeof Steam>;

  export const System = z.object({
    type: z.literal("system"),
    properties: z.object({
      userID: z.string(),
    }),
  });

  export type System = z.Infer<typeof System>;

  export const Token = z.object({
    type: z.literal("token"),
    properties: z.object({
      teamID: z.string(),
      tokenID: z.string(),
    }),
  });

  export type Token = z.Infer<typeof Token>;

  export const Public = z.object({
    type: z.literal("public"),
    properties: z.object({}),
  });

  export type Public = z.Infer<typeof Public>;

  export const Info = z.discriminatedUnion("type", [
    User,
    Token,
    Steam,
    Public,
    System,
  ]);

  export type Info = z.Infer<typeof Info>;

  export const ctx = Context.create<Info>();

  export function userID() {
    const actor = ctx.use();
    if ("userID" in actor.properties) return actor.properties.userID;
    throw new VisibleError(
      "authentication",
      ErrorCodes.Authentication.UNAUTHORIZED,
      `You don't have permission to access this resource.`,
    );
  }

  export function use() {
    try {
      return ctx.use();
    } catch {
      return { type: "public", properties: {} } as Public;
    }
  }

  export function assert<T extends Info["type"]>(type: T) {
    const actor = use();
    if (actor.type !== type)
      throw new VisibleError(
        "authentication",
        ErrorCodes.Authentication.UNAUTHORIZED,
        `Actor is not "${type}"`,
      );
    return actor as Extract<Info, { type: T }>;
  }

  export function provide<
    T extends Info["type"],
    Next extends (...args: any) => any,
  >(type: T, properties: Extract<Info, { type: T }>["properties"], fn: Next) {
    return ctx.provide({ type, properties } as any, () =>
      Log.provide(
        {
          actor: type,
          ...properties,
        },
        fn,
      ),
    );
  }
}
