export interface MockMatcherResult {
  params?: Record<string, string>
  query?: Record<string, string>
  searchParams?: URLSearchParams
}

export type MockMatcher = (url: URL) => boolean | MockMatcherResult | null | undefined

export interface MockResponse<TData = unknown> {
  data?: TData
  status?: number
  headers?: HeadersInit
  body?: BodyInit | null
  delay?: number
  response?: Response
}

export type MockHandlerResult<TData = unknown> = MockResponse<TData> | Response | TData | void

export interface MockHandlerContext {
  url: string
  method: string
  params: Record<string, string>
  query: Record<string, string>
  searchParams: URLSearchParams
  headers: Record<string, string>
  body?: unknown
  request: RequestInit
}

export type MockHandler<TData = unknown> = (ctx: MockHandlerContext) => Promise<MockHandlerResult<TData>> | MockHandlerResult<TData>

export type MockRouteMatcher = string | RegExp | MockMatcher

export interface MockRouteDefinition<TData = unknown> {
  method?: string | string[]
  url?: MockRouteMatcher
  once?: boolean
  priority?: number
  handler?: MockHandler<TData>
  response?: MockResponse<TData>
}

interface MockMatchInternal {
  route: InternalMockRoute
  params: Record<string, string>
  query: Record<string, string>
  searchParams: URLSearchParams
}

interface InternalMockRoute extends Required<Pick<MockRouteDefinition, 'once'>> {
  id: number
  methods: string[] | null
  priority: number
  matcher: (url: URL) => MatcherShape | null
  handler: MockHandler | undefined
  response: MockResponse | undefined
}

interface MatcherShape {
  params: Record<string, string>
  query: Record<string, string>
  searchParams: URLSearchParams
}

interface ExecuteContext {
  url: string
  method: string
  request: RequestInit
  bodySample?: unknown
  delay: (ms: number) => Promise<void>
}

interface NormalizedMockResult<T = unknown> {
  data: T
  response: Response
}

export class MockController {
  private routes: InternalMockRoute[] = []
  private enabled = true
  private counter = 0

  register<TData = unknown>(definition: MockRouteDefinition<TData>): () => void {
    const route: InternalMockRoute = {
      id: ++this.counter,
      once: Boolean(definition.once),
      priority: definition.priority ?? 0,
      methods: this.normalizeMethods(definition.method),
      matcher: this.createMatcher(definition.url),
      handler: definition.handler as MockHandler | undefined,
      response: definition.response
    }

    this.routes.push(route)
    this.routes.sort((a, b) => (b.priority - a.priority) || (a.id - b.id))

    return () => {
      this.routes = this.routes.filter(entry => entry.id !== route.id)
    }
  }

  clear(): void {
    this.routes = []
  }

  enable(): void {
    this.enabled = true
  }

  disable(): void {
    this.enabled = false
  }

  hasHandlers(): boolean {
    return this.routes.length > 0
  }

  match(method: string, url: string): MockMatchInternal | null {
    if (!this.enabled || !this.routes.length) {
      return null
    }

    const normalizedMethod = method.toUpperCase()
    const parsed = new URL(url)

    for (const route of this.routes) {
      if (route.methods && !route.methods.includes(normalizedMethod) && !route.methods.includes('ANY')) {
        continue
      }

      const match = route.matcher(parsed)
      if (match) {
        return {
          route,
          ...match
        }
      }
    }

    return null
  }

  async execute<T = unknown>(match: MockMatchInternal, context: ExecuteContext): Promise<NormalizedMockResult<T>> {
    if (match.route.once) {
      this.routes = this.routes.filter(route => route.id !== match.route.id)
    }

    const handler = match.route.handler
    const fallback = match.route.response
    const headersRecord = this.toHeaderRecord(context.request.headers)
    const body = context.bodySample !== undefined ? context.bodySample : this.tryDecodeBody(context.request.body)
    const handlerContext: MockHandlerContext = {
      url: context.url,
      method: context.method,
      params: match.params,
      query: match.query,
      searchParams: match.searchParams,
      headers: headersRecord,
      body,
      request: context.request
    }

    const output = handler ? await handler(handlerContext) : undefined
    const resolved = output ?? fallback

    if (!resolved) {
      return this.normalizeResult({ data: undefined }) as NormalizedMockResult<T>
    }

    if (typeof resolved === 'object' && resolved !== null && 'delay' in resolved && typeof resolved.delay === 'number') {
      await context.delay(resolved.delay)
    }

    return this.normalizeResult<T>(resolved)
  }

  private normalizeMethods(method?: string | string[]): string[] | null {
    if (!method) return null
    const list = Array.isArray(method) ? method : [method]
    if (!list.length) return null
    return list.map(m => m.toUpperCase())
  }

  private createMatcher(source?: MockRouteMatcher): (url: URL) => MatcherShape | null {
    if (!source) {
      return (url) => this.buildMatch(url)
    }

    if (typeof source === 'string') {
      return this.compileStringMatcher(source)
    }

    if (source instanceof RegExp) {
      return (url) => (source.test(url.href) ? this.buildMatch(url) : null)
    }

    return (url) => {
      const result = source(url)
      if (result === true) return this.buildMatch(url)
      if (!result) return null
      const params = result.params ?? {}
      const query = result.query ?? this.buildQuery(url)
      const searchParams = result.searchParams ?? new URLSearchParams(url.search)
      return {
        params,
        query,
        searchParams
      }
    }
  }

  private compileStringMatcher(pattern: string): (url: URL) => MatcherShape | null {
    const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(pattern)
    const parsedPattern = isAbsolute ? new URL(pattern) : null
    const [rawPath, rawQuery] = pattern.split('?')
    const targetPath = parsedPattern ? parsedPattern.pathname : (rawPath || '/')
    const patternSegments = this.splitSegments(targetPath)
    const wildcardIndex = patternSegments.indexOf('*')
    const hasWildcard = wildcardIndex !== -1 && wildcardIndex === patternSegments.length - 1
    const queryConstraints = parsedPattern
      ? parsedPattern.searchParams
      : rawQuery
        ? new URLSearchParams(rawQuery)
        : new URLSearchParams()

    return (url) => {
      if (parsedPattern && parsedPattern.origin !== url.origin) {
        return null
      }

      const urlSegments = this.splitSegments(url.pathname)

      if (!hasWildcard && patternSegments.length !== urlSegments.length) {
        return null
      }

      if (hasWildcard && urlSegments.length < wildcardIndex) {
        return null
      }

      const params: Record<string, string> = {}
      for (let i = 0; i < patternSegments.length; i++) {
        const segment = patternSegments[i]
        if (!segment) {
          return null
        }
        if (segment === '*') {
          break
        }
        const value = urlSegments[i]
        if (segment.startsWith(':')) {
          if (value === undefined) return null
          params[segment.slice(1)] = decodeURIComponent(value)
          continue
        }
        if (segment !== value) {
          return null
        }
      }

      for (const [key, value] of queryConstraints.entries()) {
        if (url.searchParams.get(key) !== value) {
          return null
        }
      }

      return this.buildMatch(url, params)
    }
  }

  private buildMatch(url: URL, params: Record<string, string> = {}): MatcherShape {
    return {
      params,
      query: this.buildQuery(url),
      searchParams: new URLSearchParams(url.search)
    }
  }

  private buildQuery(url: URL): Record<string, string> {
    const query: Record<string, string> = {}
    for (const [key, value] of url.searchParams.entries()) {
      query[key] = value
    }
    return query
  }

  private splitSegments(path: string): string[] {
    return path.split('/').filter(Boolean)
  }

  private normalizeResult<T>(output: MockHandlerResult<T>): NormalizedMockResult<T> {
    if (output instanceof Response) {
      return { data: undefined as T, response: output }
    }

    const candidate = typeof output === 'object' && output !== null && (
      'data' in output || 'status' in output || 'headers' in output || 'body' in output || 'response' in output
    )
      ? output as MockResponse<T>
      : { data: output } as MockResponse<T>

    if (candidate.response instanceof Response) {
      return { data: candidate.data as T, response: candidate.response }
    }

    const bodySource = candidate.body ?? candidate.data
    const serialized = this.serializeBody(bodySource)
    const headers = new Headers(candidate.headers || {})
    if (serialized.kind === 'json' && !headers.has('Content-Type')) {
      headers.set('Content-Type', 'application/json')
    }

    const response = new Response(serialized.body ?? '', {
      status: candidate.status ?? 200,
      headers
    })

    return {
      data: candidate.data as T,
      response
    }
  }

  private serializeBody(source: unknown): { body: BodyInit | null; kind: 'json' | 'raw' } {
    if (source === undefined || source === null) {
      return { body: null, kind: 'raw' }
    }

    if (typeof source === 'string') {
      return { body: source, kind: 'raw' }
    }

    if (typeof Blob !== 'undefined' && source instanceof Blob) {
      return { body: source, kind: 'raw' }
    }

    if (typeof FormData !== 'undefined' && source instanceof FormData) {
      return { body: source, kind: 'raw' }
    }

    if (source instanceof URLSearchParams) {
      return { body: source, kind: 'raw' }
    }

    if (typeof ReadableStream !== 'undefined' && source instanceof ReadableStream) {
      return { body: source, kind: 'raw' }
    }

    if (typeof ArrayBuffer !== 'undefined' && source instanceof ArrayBuffer) {
      return { body: source, kind: 'raw' }
    }

    if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(source)) {
      return { body: source as unknown as BodyInit, kind: 'raw' }
    }

    return { body: JSON.stringify(source), kind: 'json' }
  }

  private tryDecodeBody(body: RequestInit['body']): unknown {
    if (!body) return undefined

    if (typeof body === 'string') {
      try {
        return JSON.parse(body)
      } catch {
        return body
      }
    }

    if (body instanceof URLSearchParams) {
      return Object.fromEntries(body.entries())
    }

    if (typeof FormData !== 'undefined' && body instanceof FormData) {
      const result: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {}
      for (const [key, raw] of body.entries()) {
        const value = raw as FormDataEntryValue
        if (key in result) {
          const existing = result[key]
          if (Array.isArray(existing)) {
            existing.push(value)
          } else {
            result[key] = [existing as FormDataEntryValue, value]
          }
        } else {
          result[key] = value
        }
      }
      return result
    }

    if (
      (typeof Blob !== 'undefined' && body instanceof Blob) ||
      (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) ||
      (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body)) ||
      (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream)
    ) {
      return body
    }

    return body
  }

  private toHeaderRecord(headers?: HeadersInit): Record<string, string> {
    const record: Record<string, string> = {}
    if (!headers) return record
    const normalized = new Headers(headers)
    normalized.forEach((value, key) => {
      record[key] = value
    })
    return record
  }
}
