import { ApiException } from "@components/api-exceptions"
import type { FlowcoreAuthenticatedUser } from "@components/jwks-guard"
import type { Logger } from "@components/logger"
import type * as xxHash from "@jabr/xxhash64"
import { type Static, Type } from "@sinclair/typebox"
import { BunSqliteKeyValue } from "bun-sqlite-key-value"

/**
 * Represents an API forbidden exception. This exception is thrown when the server refuses to authorize the request.
 *
 * @property {number} status - The HTTP status code for the exception, which is 403.
 * @property {string} code - The error code for the exception, which is "FORBIDDEN".
 */
export class IamForbiddenException extends ApiException {
  public status = 403
  public code = "FORBIDDEN"

  constructor(
    message: string,
    public readonly validPolicies?: { policyFrn: string; statementId: string }[],
    public readonly invalidRequest?: RequestedAccess,
  ) {
    super(message)
  }
}

/**
 * Defines the structure of requested access for IAM validation.
 *
 * @property {action} - The action or actions to validate against.
 * @property {resource} - The resource to validate, in order of granularity. First match wins.
 */
export const requestedAccess = Type.Array(
  Type.Object({
    action: Type.Union([Type.String(), Type.Array(Type.String())], {
      description: "The action or actions to validate against",
      examples: ["read", "write", "fetch", "ingest"],
    }),
    resource: Type.Array(Type.String({ pattern: "^frn::" }), {
      description: "The resource to validate, in order of granularity. First match wins.",
      examples: [
        [
          "frn::tenant",
          "frn::tenant:data-core/24005dde-7476-4692-aec4-175d2e5a9b9e",
          "frn::tenant:flow-type/6a394228-e1a1-4491-b280-0117f481be32",
          "frn::tenant:event-type/ffff7fe3-45c7-4a96-afe2-2519ee342d69",
        ],
        [
          "frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda",
          "frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda:data-core/24005dde-7476-4692-aec4-175d2e5a9b9e",
          "frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda:flow-type/6a394228-e1a1-4491-b280-0117f481be32",
          "frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda:event-type/ffff7fe3-45c7-4a96-afe2-2519ee342d69",
        ],
      ],
    }),
  }),
)

/**
 * Represents the type of requested access for IAM validation.
 */
export type RequestedAccess = Static<typeof requestedAccess>

/**
 * Represents the response from IAM validation.
 *
 * @property {valid} - Indicates if the validation was successful.
 * @property {validPolicies} - The policies that were validated.
 * @property {checksum} - The checksum of the validation request.
 * @property {invalidRequest} - The invalid request details if validation failed.
 */
export type IamValidationResponse =
  | {
      valid: true
      validPolicies: { policyFrn: string; statementId: string }[]
      checksum: string
    }
  | {
      valid: false
      invalidRequest: RequestedAccess
      validPolicies: { policyFrn: string; statementId: string }[]
    }

const HASH_KEY_TTL_IN_MS = 300_000

export class IamValidateBuilder {
  private hash!: xxHash.Hasher
  private type: "users" | "keys" = "users"
  private mode: "tenant" | "organization" = "tenant"
  private logger: Logger | undefined
  private localCache: BunSqliteKeyValue = new BunSqliteKeyValue(":memory:", {
    ttlMs: 300_000,
  })

  constructor(private readonly iamApiUrl: string) {}

  /**
   * Sets the hash function for generating checksums.
   *
   * @param {xxHash.Hasher} hash - The hash function instance.
   * @returns {IamValidateBuilder} - This instance for method chaining.
   */
  public withHash(hash: xxHash.Hasher): IamValidateBuilder {
    this.hash = hash
    return this
  }

  /**
   * Sets the type of validation to perform.
   *
   * @param {"users" | "keys"} type - The type of validation.
   * @returns {IamValidateBuilder} - This instance for method chaining.
   */
  public withType(type: "users" | "keys"): IamValidateBuilder {
    this.type = type
    return this
  }

  /**
   * Sets the mode of validation to perform.
   *
   * @param {"tenant" | "organization"} mode - The mode of validation.
   * @returns {IamValidateBuilder} - This instance for method chaining.
   */
  public withMode(mode: "tenant" | "organization"): IamValidateBuilder {
    this.mode = mode
    return this
  }

  /**
   * Sets the logger for logging validation events.
   *
   * @param {Logger} logger - The logger instance.
   * @returns {IamValidateBuilder} - This instance for method chaining.
   */
  public withLogger(logger: Logger): IamValidateBuilder {
    this.logger = logger
    return this
  }

  /**
   * Builds the IAM validation process.
   *
   * @param {RequestedAccess} requestedAccess - The requested access details.
   * @returns {(flowcoreUser: FlowcoreAuthenticatedUser) => Promise<void>} - A promise that resolves to void.
   */
  public build(
    requestedAccess: RequestedAccess,
  ): (options: { flowcoreUser: FlowcoreAuthenticatedUser }) => Promise<void> {
    const checksum = this.hash.hash(JSON.stringify(requestedAccess), "hex").toString()
    return async ({ flowcoreUser }: { flowcoreUser: FlowcoreAuthenticatedUser }): Promise<void> => {
      const { id } = flowcoreUser
      const localChecksum = this.localCache.get<boolean>(`${id}-${checksum}`)
      if (localChecksum === true) {
        return
      }

      const validation = await fetch(`${this.iamApiUrl}/api/v1/validate/${this.type}/${id}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          mode: this.mode,
          requestedAccess,
        }),
      })

      const data = (await validation.json()) as IamValidationResponse

      if (data.valid) {
        this.localCache.set(`${id}-${data.checksum}`, true, HASH_KEY_TTL_IN_MS)
        this.logger?.debug(`IAM validation passed for ${this.type} ${id} with checksum ${data.checksum}`)
      } else {
        this.logger?.error("IAM validation failed", data)
        throw new IamForbiddenException("IAM validation failed", data.validPolicies, data.invalidRequest)
      }
    }
  }
}

/**
 * Enum for defining the types of FRNs.
 *
 * @enum {string}
 */
export enum FrnType {
  USER = "user",
  KEY = "key",
  ROLE = "role",
  POLICY = "policy",
  ORGANIZATION = "organization",
  DATA_CORE = "data-core",
  FLOW_TYPE = "flow-type",
  EVENT_TYPE = "event-type",
  ALL = "*",
}

/**
 * Enum for defining the actions that can be performed on FRNs.
 *
 * @enum {string}
 */
export enum FrnAction {
  READ = "read",
  WRITE = "write",
  INGEST = "ingest",
  FETCH = "fetch",
  ALL = "*",
}

/**
 * Builds a fully qualified FRN string.
 *
 * @param {string} tenantOrOrganizationId - The tenant or organization ID.
 * @param {FrnType} type - The type of FRN.
 * @param {string} [id] - The ID of the FRN.
 * @returns {string} - The fully qualified FRN string.
 */
export const buildFrn = (tenantOrOrganizationId: string, type: FrnType, id?: string): string => {
  return `frn::${tenantOrOrganizationId}:${type}${id ? `/${id}` : ""}`
}

/**
 * Builds a list of fully qualified FRN strings.
 *
 * @param {{ type: FrnType; id?: string }[]} items - The list of items to build FRNs for.
 * @param {string} tenantOrOrganizationId - The tenant or organization ID.
 * @returns {string[]} - A list of fully qualified FRN strings.
 */
export const buildListFrn = (items: { type: FrnType; id?: string }[], tenantOrOrganizationId: string): string[] => {
  return [
    buildFrn(tenantOrOrganizationId, FrnType.ALL),
    ...items.map((item) => buildFrn(tenantOrOrganizationId, item.type, item.id)),
  ]
}
