import { z } from "zod"
import { Route } from "next"
import type { ReactNode } from "react"
import type { BetterAuthOptions } from "better-auth"
import type {
  ComponentConfig,
  Config,
  Data,
  DefaultComponentProps,
  DefaultComponents,
  SlotComponent,
} from "@puckeditor/core"
import type { StreamResponseChunk } from "@tohuhono/utils"
import type { NextRequest } from "next/server"

export class OberonError extends Error {}

export class ResponseError extends Error {}

export class NotImplementedError extends ResponseError {}

// TODO fix types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Transforms = Array<(props: any) => any>

export type PageData = Data

export type OberonConfig<
  Components extends DefaultComponents = DefaultComponents,
> = Config<{ components: Components }> & {
  version: 1
  components: Record<
    string,
    {
      transforms?: Transforms
    }
  >
}

export type OberonComponent<
  Props extends DefaultComponentProps = DefaultComponentProps,
> = ComponentConfig<{
  props: Props
}>

type OberonFieldMap = Record<string, { type: string }>

type InferOberonFieldProp<FieldConfig> = FieldConfig extends { type: "slot" }
  ? SlotComponent
  : FieldConfig extends { type: "richtext" }
    ? ReactNode
    : FieldConfig extends { type: "textarea"; contentEditable: true }
      ? ReactNode
      : FieldConfig extends { type: "text" | "textarea" }
        ? string
        : FieldConfig extends { type: "number" }
          ? number
          : unknown

type InferOberonRequiredFieldKeys<FieldMap extends OberonFieldMap> = {
  [Key in keyof FieldMap]: FieldMap[Key] extends { type: "slot" } ? Key : never
}[keyof FieldMap]

type InferOberonOptionalFieldKeys<FieldMap extends OberonFieldMap> = Exclude<
  keyof FieldMap,
  InferOberonRequiredFieldKeys<FieldMap>
>

type InferOberonFieldProps<FieldMap extends OberonFieldMap> = {
  [Key in InferOberonRequiredFieldKeys<FieldMap>]: InferOberonFieldProp<
    FieldMap[Key]
  >
} & {
  [Key in InferOberonOptionalFieldKeys<FieldMap>]?: InferOberonFieldProp<
    FieldMap[Key]
  >
}

type DefineOberonComponentConfig<
  Props extends DefaultComponentProps,
  FieldMap extends OberonFieldMap,
> = Omit<OberonComponent<Props>, "fields"> & {
  fields?: FieldMap & NonNullable<OberonComponent<Props>["fields"]>
}

export function defineOberonComponent<const FieldMap extends OberonFieldMap>(
  config: DefineOberonComponentConfig<
    InferOberonFieldProps<FieldMap>,
    FieldMap
  >,
): OberonComponent<InferOberonFieldProps<FieldMap>>

export function defineOberonComponent<
  Props extends DefaultComponentProps,
  const FieldMap extends OberonFieldMap = {},
>(config: DefineOberonComponentConfig<Props, FieldMap>): OberonComponent<Props>

export function defineOberonComponent(
  config: DefineOberonComponentConfig<DefaultComponentProps, OberonFieldMap>,
): OberonComponent<DefaultComponentProps> {
  return config
}

export const clientActions = [
  "edit",
  "preview",
  "users",
  "images",
  "pages",
  "site",
  "login",
] as const
export const actionPaths = clientActions.map((action) => ({
  path: [action],
}))
export type ClientAction = (typeof clientActions)[number]

export type AdapterActionGroup = "all" | "users" | "images" | "pages" | "site"
export type AdapterPermission = "unauthenticated" | "read" | "write"
export type OberonRole = "user" | "admin" | "unauthenticated" | (string & {})

export type OberonPermissions = Record<
  OberonRole,
  Partial<Record<AdapterActionGroup, AdapterPermission>>
>

export const INITIAL_DATA = {
  content: [],
  root: { props: { title: "" } },
} satisfies Data

export type MaybeOptimistic<T> = T & {
  pending?: boolean
}

export const JsonValueSchema = z.json()

export type JsonValue = z.infer<typeof JsonValueSchema>

/*
 * Pages
 */
export const PageSchema = z.object({
  key: z
    .string()
    .regex(/^[0-9a-zA-Z_.-/]+$/, "Valid characters: 0-9 a-z A-Z -_./")
    .regex(/^(\/|\/[^/]+(\/[^/]+)*)$/, "Route segments cannot be empty"),
  data: z.object({}).passthrough(),
  updatedAt: z.date(),
  updatedBy: z.string(),
})

export const AddPageSchema = PageSchema.pick({ key: true })

export const DeletePageSchema = PageSchema.pick({ key: true })

export const PublishPageSchema = PageSchema.pick({ key: true, data: true })

export const PageMetaSchema = PageSchema.pick({
  key: true,
  updatedAt: true,
  updatedBy: true,
})

export type OberonPage = z.infer<typeof PageSchema> & {
  data: PageData
  key: Route
}

// Cannot infer from zod because we need nextjs to understand key is a valid Route
export type OberonPageMeta = MaybeOptimistic<
  z.infer<typeof PageMetaSchema> & {
    key: Route
  }
>

/*
 * Images
 */

export const ImageSchema = z.object({
  key: z.string(),
  url: z.string(),
  size: z.number(),
  width: z.number().gt(0),
  height: z.number().gt(0),
  alt: z.string(),
  updatedAt: z.date(),
  updatedBy: z.string(),
})

export const AddImageSchema = ImageSchema

export const DeleteImageSchema = ImageSchema.pick({ key: true })

export type OberonImage = MaybeOptimistic<z.infer<typeof ImageSchema>>

/*
 * Users
 */
export const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  role: z.union([z.literal("user"), z.literal("admin"), z.string()]),
})

export const AddUserSchema = UserSchema.pick({ email: true, role: true })
export const ChangeRoleSchema = UserSchema.pick({ id: true, role: true })
export const DeleteUserSchema = UserSchema.pick({ id: true })

export type OberonUser = MaybeOptimistic<z.infer<typeof UserSchema>> & {
  role: "user" | "admin" | (string & {})
}

export const roles: OberonRole[] = ["user", "admin"] as const

/*
 * Context
 */

type DescriminatedContext =
  | { action: "edit" | "preview"; data: Data | null }
  | { action: "users"; data: OberonUser[] }
  | { action: "images"; data: OberonImage[] }
  | { action: "pages"; data: OberonPageMeta[] }
  | { action: "site"; data: OberonSiteConfig }
  | {
      action: "login"
      data: { callbackUrl: string; email: string; token: string }
    }

export type OberonClientContext = DescriminatedContext & {
  slug: string
}

/*
 * Site
 */
type TransformStatus = "error" | "success"

export type TransformResult = {
  type: "transform"
  key: string
  status: "success" | "error"
}

export type MigrationResult = {
  type: "summary"
  total: number
} & {
  [key in TransformStatus]: string[]
}

export type TransformVersions = Record<string, number>

export type PluginVersion = Pick<
  ReturnType<OberonPlugin>,
  "name" | "version" | "disabled"
>

export type OberonSiteConfig = MaybeOptimistic<{
  version: string
  plugins: PluginVersion[]
  components: TransformVersions
  pendingMigrations: string[] | false
}>

export const SiteSchema = z.object({
  version: z.number(),
  components: z.record(z.string(), z.number()),
  updatedAt: z.date(),
  updatedBy: z.string(),
})

export type OberonSite = z.infer<typeof SiteSchema>

/*
 * Adapter
 */

export type OberonCanAdapter = {
  getCurrentUser: () => Promise<OberonUser | null>
  hasPermission: (props: {
    user?: OberonUser | null
    action: AdapterActionGroup
    permission: AdapterPermission
  }) => boolean
  signIn: (data: { email: string }) => Promise<void>
  signOut: () => Promise<void>
}

export type OberonBetterAuthAdapter = Pick<BetterAuthOptions, "database">

export type OberonAuthAdapter = {
  betterAuth?: OberonBetterAuthAdapter
  addUser: (data: z.infer<typeof AddUserSchema>) => Promise<OberonUser>
  deleteUser: (id: OberonUser["id"]) => Promise<void>
  changeRole: (data: z.infer<typeof ChangeRoleSchema>) => Promise<void>
  getAllUsers: () => Promise<OberonUser[]>
}

export type OberonBaseAdapter = {
  addPage: (page: OberonPage) => Promise<void>
  addImage: (data: z.infer<typeof ImageSchema>) => Promise<void>
  deletePage: (key: OberonPageMeta["key"]) => Promise<void>
  deleteImage: (key: OberonImage["key"]) => Promise<void> // TODO uploadthing
  deleteKV: (namespace: string, key: string) => Promise<void>
  getAllImages: () => Promise<OberonImage[]>
  getAllPages: () => Promise<OberonPageMeta[]>
  getPageData: (key: OberonPageMeta["key"]) => Promise<Data | null>
  getKV: (namespace: string, key: string) => Promise<JsonValue | null>
  getSite: () => Promise<OberonSite | undefined>
  putKV: (namespace: string, key: string, value: JsonValue) => Promise<void>
  updatePageData: (data: OberonPage) => Promise<void>
  updateSite: (data: z.infer<typeof SiteSchema>) => Promise<void>
}

export type OberonSendAdapter = {
  sendVerificationRequest: (props: {
    email: string
    token: string
    url: string
  }) => Promise<void>
}

export type OberonInitAdapter = {
  prebuild: () => Promise<void>
}

export type OberonDatabaseAdapter = Partial<OberonInitAdapter> &
  OberonBaseAdapter &
  OberonAuthAdapter

export type OberonPluginAdapter = OberonInitAdapter &
  OberonDatabaseAdapter &
  OberonCanAdapter &
  OberonSendAdapter

export type OberonMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"

export type OberonHandler<Params = undefined> = Params extends undefined
  ? {
      [key in OberonMethod]?: (req: NextRequest) => Promise<Response> | Response
    }
  : {
      [key in OberonMethod]: (
        req: NextRequest,
        context: { params: Promise<Params> },
      ) => Promise<Response>
    }

export type OberonAdapter = {
  prebuild: () => Promise<void>
  getSetting: (namespace: string, key: string) => Promise<JsonValue | null>
  addPage: (page: z.infer<typeof AddPageSchema>) => Promise<void>
  addImage: (data: OberonImage) => Promise<OberonImage[]>
  addUser: (data: z.infer<typeof AddUserSchema>) => Promise<OberonUser | null>
  deletePage: (data: z.infer<typeof DeletePageSchema>) => Promise<void>
  deleteImage: (key: OberonImage["key"]) => Promise<void> // TODO uploadthing
  deleteUser: (
    data: z.infer<typeof DeleteUserSchema>,
  ) => Promise<Pick<OberonUser, "id"> | null>
  can: (
    action: AdapterActionGroup,
    permission?: AdapterPermission,
  ) => Promise<boolean>
  changeRole: (
    data: z.infer<typeof ChangeRoleSchema>,
  ) => Promise<Pick<OberonUser, "role" | "id"> | null>
  getAllImages: () => Promise<OberonImage[]>
  getAllPages: () => Promise<OberonPageMeta[]>
  getAllPaths: () => Promise<Array<{ path: string[] }>>
  getAllUsers: () => Promise<OberonUser[]>
  getConfig: () => Promise<OberonSiteConfig>
  getPageData: (key: OberonPageMeta["key"]) => Promise<Data | null>
  migrateData: () => Promise<
    StreamResponseChunk<TransformResult | MigrationResult>
  >
  publishPageData: (
    data: z.infer<typeof PublishPageSchema>,
  ) => Promise<{ message: string }>
  signOut: () => Promise<void>
  signIn: (data: { email: string }) => Promise<void>
}

export type OberonPlugin = (adapter: OberonPluginAdapter) => {
  name: string
  version?: string
  disabled?: boolean
  handlers?: Record<string, (adapter: OberonAdapter) => OberonHandler>
  adapter?: Partial<OberonPluginAdapter>
}

export type OberonResponse<T = unknown> = Promise<
  | {
      status: "success"
      result: T
      message?: string
    }
  | {
      status: "error"
      result?: T
      message?: string
    }
>

export type OberonServerActions = {
  addPage: (page: z.infer<typeof AddPageSchema>) => OberonResponse<void>
  addImage: (data: OberonImage) => OberonResponse<OberonImage[]>
  addUser: (
    data: z.infer<typeof AddUserSchema>,
  ) => OberonResponse<OberonUser | null>
  deletePage: (data: z.infer<typeof DeletePageSchema>) => OberonResponse
  deleteImage: (key: OberonImage["key"]) => OberonResponse
  deleteUser: (
    data: z.infer<typeof DeleteUserSchema>,
  ) => OberonResponse<Pick<OberonUser, "id"> | null>
  can: (
    action: AdapterActionGroup,
    permission?: AdapterPermission,
  ) => OberonResponse<boolean>
  changeRole: (
    data: z.infer<typeof ChangeRoleSchema>,
  ) => OberonResponse<Pick<OberonUser, "role" | "id"> | null>
  getAllImages: () => OberonResponse<OberonImage[]>
  getAllPages: () => OberonResponse<OberonPageMeta[]>
  getAllPaths: () => OberonResponse<Array<{ path: string[] }>>
  getAllUsers: () => OberonResponse<OberonUser[]>
  getConfig: () => OberonResponse<OberonSiteConfig>
  getPageData: (key: OberonPageMeta["key"]) => OberonResponse<Data | null>
  migrateData: () => OberonResponse<
    StreamResponseChunk<TransformResult | MigrationResult>
  >
  publishPageData: (data: z.infer<typeof PublishPageSchema>) => OberonResponse
  signIn: (data: { email: string }) => OberonResponse
  signOut: () => OberonResponse
}
