import { prepareBodyPayload } from './body-utils.js'

export type HttpMethod =
  | 'GET'
  | 'POST'
  | 'PUT'
  | 'PATCH'
  | 'DELETE'
  | 'OPTIONS'
  | 'HEAD'

export interface ResourceMethodConfig<
  TResponse = unknown,
  TBody = undefined,
  TParams extends Record<string, string | number> = Record<string, string | number>,
  TQuery extends Record<string, unknown> = Record<string, unknown>
> {
  method?: HttpMethod
  path?: string
  query?: Partial<TQuery>
  headers?: Record<string, string>
  json?: boolean
  serializeBody?: (body: TBody) => BodyInit | undefined
  transformResponse?: (payload: any, response: Response) => TResponse
  description?: string
  summary?: string
}

export type ResourceDefinition = Record<string, ResourceMethodConfig>

export interface ResourceCallArgs<
  TBody,
  TParams extends Record<string, string | number>,
  TQuery extends Record<string, unknown>
> {
  body?: TBody
  params?: TParams
  query?: TQuery
  init?: RequestInit
}

export type ResourceMethodInvoker<Config extends ResourceMethodConfig> = (
  args?: ResourceCallArgs<
    Config extends ResourceMethodConfig<any, infer TBody, any, any> ? TBody : never,
    Config extends ResourceMethodConfig<any, any, infer TParams, any>
      ? TParams
      : Record<string, string | number>,
    Config extends ResourceMethodConfig<any, any, any, infer TQuery>
      ? TQuery
      : Record<string, unknown>
  >
) => Promise<{
  data: Config extends ResourceMethodConfig<infer TResponse, any, any, any> ? TResponse : unknown
  response: Response
}>

export type ResourceInstance<TDefinition extends ResourceDefinition> = {
  [K in keyof TDefinition]: ResourceMethodInvoker<TDefinition[K]>
} & {
  /** Build the full URL for this resource */
  $path(
    params?: Record<string, string | number>,
    query?: Record<string, unknown>
  ): string
}

export interface ResourceBuilderOptions {
  trailingSlash?: boolean
  query?: Record<string, unknown>
}

interface ResourceRequester {
  request<T>(method: string, url: string, init?: RequestInit, bodySample?: unknown): Promise<{
    data: T
    response: Response
  }>
}

export function createResource<TDefinition extends ResourceDefinition>(
  requester: ResourceRequester,
  basePath: string,
  definition: TDefinition,
  options: ResourceBuilderOptions = {}
): ResourceInstance<TDefinition> {
  const normalizedBase = normalizePath(basePath, options.trailingSlash)
  const resource: Record<string, any> = {}

  for (const name of Object.keys(definition) as Array<keyof TDefinition>) {
    type Config = TDefinition[typeof name]
    type BodyType = Config extends ResourceMethodConfig<any, infer TBody, any, any> ? TBody : never
    type ParamsType = Config extends ResourceMethodConfig<any, any, infer TParams, any>
      ? TParams
      : Record<string, string | number>
    type QueryType = Config extends ResourceMethodConfig<any, any, any, infer TQuery>
      ? TQuery
      : Record<string, unknown>
    type ResponseType = Config extends ResourceMethodConfig<infer TResponse, any, any, any>
      ? TResponse
      : unknown
    const config = definition[name] as Config

    resource[name as string] = async (args: ResourceCallArgs<BodyType, ParamsType, QueryType> = {}) => {
      const method = (config.method || 'GET').toUpperCase()
      const path = joinPaths(normalizedBase, config.path, options.trailingSlash)
      const resolvedPath = applyParams(path, args.params)
      const query = buildQuery({
        ...(options.query ?? {}),
        ...(config.query ?? {}),
        ...(args.query ?? {})
      })
      const finalUrl = `${resolvedPath}${query}`

      const shouldUseJson = config.json ?? true
      const serializer = config.serializeBody as ((body: BodyType) => BodyInit | undefined) | undefined
      const hasBody = args.body !== undefined
      const prepared = hasBody
        ? serializer
          ? { bodyInit: serializer(args.body as BodyType), sample: args.body }
          : prepareBodyPayload(args.body as BodyType, { json: shouldUseJson })
        : undefined

      const init: RequestInit = {
        ...(args.init || {}),
        headers: {
          ...config.headers,
          ...(args.init?.headers || {})
        }
      }

      if (hasBody) {
        init.body = prepared?.bodyInit ?? null
      }

      const result = await requester.request<ResponseType>(
        method,
        finalUrl,
        init,
        hasBody ? prepared?.sample : undefined
      )
      if (config.transformResponse) {
        return {
          data: config.transformResponse(result.data, result.response),
          response: result.response
        }
      }
      return result
    }
  }

  resource.$path = (
    params?: Record<string, string | number>,
    query?: Record<string, unknown>
  ) => {
    const resolved = maybeApplyTrailing(applyParams(normalizedBase, params), options.trailingSlash)
    return `${resolved}${buildQuery({ ...(options.query ?? {}), ...(query ?? {}) })}`
  }

  return resource as ResourceInstance<TDefinition>
}

function normalizePath(path: string, trailingSlash?: boolean): string {
  if (!path) return trailingSlash ? '/' : '/'
  if (path.startsWith('http')) return maybeApplyTrailing(path, trailingSlash)
  const normalized = path.startsWith('/') ? path : `/${path}`
  return maybeApplyTrailing(normalized, trailingSlash)
}

function joinPaths(base: string, extra?: string, trailingSlash?: boolean): string {
  if (!extra) return maybeApplyTrailing(base, trailingSlash)
  if (extra.startsWith('http')) return maybeApplyTrailing(extra, trailingSlash)
  const combined = `${base.replace(/\/$/, '')}/${extra.replace(/^\//, '')}`
  return maybeApplyTrailing(combined.replace(/\/+/g, '/'), trailingSlash)
}

function maybeApplyTrailing(path: string, trailingSlash?: boolean): string {
  if (!path) return trailingSlash ? '/' : ''
  if (trailingSlash === undefined) return path
  return trailingSlash ? path.replace(/\/?$/, '/') : path.replace(/\/$/, '')
}

function applyParams(path: string, params?: Record<string, string | number>): string {
  if (!params) return path
  return path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
    const value = params[key]
    if (value === undefined) {
      throw new Error(`Missing value for URL parameter :${key}`)
    }
    return encodeURIComponent(String(value))
  })
}

function buildQuery(query?: Record<string, unknown>): string {
  if (!query) return ''
  const search = new URLSearchParams()
  for (const [key, value] of Object.entries(query)) {
    if (value === undefined || value === null) continue
    if (Array.isArray(value)) {
      value.forEach(item => search.append(key, String(item)))
    } else {
      search.append(key, String(value))
    }
  }
  const result = search.toString()
  return result ? `?${result}` : ''
}
