type FakeHandler = (req: Request) => Response | Promise<Response>

export function createFakeRegistry(handlers: Record<string, FakeHandler>) {
  return async function fakeFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
    const path = new URL(url).pathname

    for (const [pattern, handler] of Object.entries(handlers)) {
      if (path === pattern || path.startsWith(pattern)) {
        return handler(new Request(url, init))
      }
    }

    return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
  }
}

// ---------------------------------------------------------------------------
// Failure injection
// ---------------------------------------------------------------------------

/**
 * Controls how a specific endpoint behaves when a failure is injected:
 *   'auth'         => 401 { code: 401, message: 'unauthorized' }
 *   'not_found'    => 404 { code: 404, message: 'not found' }
 *   'server_error' => 500 { code: 500, message: 'internal error' }
 *   'network'      => handler throws, causing fetch() to reject with a TypeError
 */
export type FailureMode = 'auth' | 'not_found' | 'server_error' | 'network'

function failureResponse(mode: FailureMode): Response {
  switch (mode) {
    case 'auth':
      return Response.json({ code: 401, message: 'unauthorized' }, { status: 401 })
    case 'not_found':
      return Response.json({ code: 404, message: 'not found' }, { status: 404 })
    case 'server_error':
      return Response.json({ code: 500, message: 'internal error' }, { status: 500 })
    case 'network':
      // Unreachable at runtime: startFakeRegistry intercepts 'network' failures
      // by returning a URL pointing at a TCP server that closes connections
      // immediately, so fetch() itself rejects. This case satisfies TypeScript.
      return new Response(null, { status: 503 })
  }
}

// ---------------------------------------------------------------------------
// Skill fixture shape
// ---------------------------------------------------------------------------

export interface FakeSkill {
  namespace: string
  slug: string
  /** Treated as the "latest" version string. Defaults to '1.0.0'. */
  version?: string
  /** Numeric version id returned in resolve. Defaults to 1. */
  versionId?: number
  /** SHA-256 fingerprint string. Defaults to 'deadbeef'. */
  fingerprint?: string
  /** Raw bytes served as the ZIP body. Defaults to a minimal valid ZIP. */
  zipBytes?: Uint8Array
}

// Minimal valid ZIP: local file header + end-of-central-directory record with
// zero entries. Enough for any consumer that just checks Content-Type / length.
const MINIMAL_ZIP = new Uint8Array([
  // End of central directory record (22 bytes, zero entries)
  0x50, 0x4b, 0x05, 0x06, // signature
  0x00, 0x00,             // disk number
  0x00, 0x00,             // disk with start of central directory
  0x00, 0x00,             // entries on this disk
  0x00, 0x00,             // total entries
  0x00, 0x00, 0x00, 0x00, // size of central directory
  0x00, 0x00, 0x00, 0x00, // offset of central directory
  0x00, 0x00,             // comment length
])

// ---------------------------------------------------------------------------
// Captured publish state
// ---------------------------------------------------------------------------

/**
 * Shape of the last publish request received by the fake registry.
 * Inspect via `registry.received.publish` in tests.
 */
export interface CapturedPublish {
  namespace: string
  /** Original file name from the multipart form field. */
  fileName: string
  /** Visibility string from the multipart form field. */
  visibility: string
}

/** Last resolve GET: useful for verifying --version is forwarded as ?version=. */
export interface CapturedResolve {
  namespace: string
  slug: string
  version: string | null
  token: string | null
}

/** Last DELETE: useful for verifying remote hard-delete actually hit the server. */
export interface CapturedDelete {
  namespace: string
  slug: string
  token: string | null
}

// ---------------------------------------------------------------------------
// Options
// ---------------------------------------------------------------------------

interface FakeRegistryOptions {
  token?: string
  user?: { handle: string; displayName: string; email?: string }
  searchItems?: Array<{ namespace: string; slug: string; latestVersion: string; summary: string }>
  /** Skills available for resolve / download / delete / publish. */
  skills?: FakeSkill[]
  /**
   * Per-endpoint failure injection. When set for an endpoint, that endpoint
   * ignores all other logic and returns the specified failure (or throws for
   * 'network').
   */
  failures?: {
    whoami?: FailureMode
    search?: FailureMode
    resolve?: FailureMode
    download?: FailureMode
    deleteRemote?: FailureMode
    publish?: FailureMode
  }
}

// ---------------------------------------------------------------------------
// startFakeRegistry
// ---------------------------------------------------------------------------

/**
 * Allocate a port that terminates every accepted connection. We keep the
 * listener alive so the OS cannot reassign the port to another process
 * between bind and fetch — otherwise the client might connect to someone
 * else instead of failing. Any fetch reaches open() and gets terminated,
 * which surfaces as a socket error and maps to
 * CliError('registry unreachable', EXIT.network) in the SkillHub client.
 */
async function startNetworkFailureServer(): Promise<{ url: string; stop: () => void }> {
  const listener = Bun.listen<unknown>({
    hostname: "127.0.0.1",
    port: 0,
    socket: {
      open(socket) { socket.terminate() },
      data() {},
      error() {},
      close() {},
    }
  })
  const port = listener.port
  return {
    url: `http://127.0.0.1:${port}`,
    stop: () => { try { listener.stop(true) } catch { /* already stopped */ } }
  }
}

export async function startFakeRegistry(options: FakeRegistryOptions = {}) {
  // Mutable state captured from incoming requests — readable by tests.
  const state: {
    publish: CapturedPublish | null
    resolve: CapturedResolve | null
    delete: CapturedDelete | null
  } = { publish: null, resolve: null, delete: null }

  // If any endpoint is configured with 'network' failure mode, we need a real
  // TCP-level failure. Start a connection-dropping server and return its URL
  // so that fetch() itself rejects (ECONNRESET/ECONNREFUSED).
  const hasNetworkFailure = options.failures &&
    Object.values(options.failures).some(m => m === 'network')

  if (hasNetworkFailure) {
    const { url, stop } = await startNetworkFailureServer()
    return {
      url,
      stop,
      received: state
    }
  }

  // Helper: check bearer token. Returns a 401 Response if auth fails, null if ok.
  function checkAuth(req: Request): Response | null {
    if (options.token) {
      const auth = req.headers.get('Authorization')
      if (auth !== `Bearer ${options.token}`) {
        return Response.json({ code: 401, message: 'unauthorized' }, { status: 401 })
      }
    }
    return null
  }

  // Helper: look up a skill by namespace + slug.
  function findSkill(namespace: string, slug: string): FakeSkill | undefined {
    return options.skills?.find(s => s.namespace === namespace && s.slug === slug)
  }

  // Helper: build the download URL that resolve returns.
  function buildDownloadUrl(baseUrl: string, namespace: string, slug: string, version: string): string {
    return `${baseUrl}/api/cli/v1/skills/${namespace}/${slug}/versions/${version}/download`
  }

  const server = Bun.serve({
    port: 0,
    fetch(req) {
      const url = new URL(req.url)
      const path = url.pathname
      const baseUrl = `${url.protocol}//${url.host}`

      // ------------------------------------------------------------------ //
      // GET /api/cli/v1/auth/whoami
      // ------------------------------------------------------------------ //
      if (path === '/api/cli/v1/auth/whoami') {
        if (options.failures?.whoami) return failureResponse(options.failures.whoami)
        const authErr = checkAuth(req)
        if (authErr) return authErr
        return Response.json({
          code: 0,
          data: options.user ?? { handle: 'test-user', displayName: 'Test User', email: 'test@example.com' }
        })
      }

      // ------------------------------------------------------------------ //
      // GET /api/cli/v1/skills/search
      // ------------------------------------------------------------------ //
      if (path === '/api/cli/v1/skills/search') {
        if (options.failures?.search) return failureResponse(options.failures.search)
        return Response.json({
          code: 0,
          data: {
            items: options.searchItems ?? [],
            total: options.searchItems?.length ?? 0,
            limit: 20
          }
        })
      }

      // ------------------------------------------------------------------ //
      // Route: /api/cli/v1/skills/:namespace/:slug/...
      // ------------------------------------------------------------------ //

      // Resolve: GET /api/cli/v1/skills/:namespace/:slug/resolve
      const resolveMatch = path.match(/^\/api\/cli\/v1\/skills\/([^/]+)\/([^/]+)\/resolve$/)
      if (resolveMatch && req.method === 'GET') {
        if (options.failures?.resolve) return failureResponse(options.failures.resolve)
        const namespace = resolveMatch[1]!
        const slug = resolveMatch[2]!
        state.resolve = {
          namespace,
          slug,
          version: url.searchParams.get('version'),
          token: req.headers.get('authorization') ?? null
        }
        const skill = findSkill(namespace, slug)
        if (!skill) {
          return Response.json({ code: 404, message: 'not found' }, { status: 404 })
        }
        const version = skill.version ?? '1.0.0'
        return Response.json({
          code: 0,
          data: {
            namespace,
            slug,
            version,
            versionId: skill.versionId ?? 1,
            fingerprint: skill.fingerprint ?? 'deadbeef',
            downloadUrl: buildDownloadUrl(baseUrl, namespace, slug, version)
          }
        })
      }

      // Download (latest): GET /api/cli/v1/skills/:namespace/:slug/download
      const downloadLatestMatch = path.match(/^\/api\/cli\/v1\/skills\/([^/]+)\/([^/]+)\/download$/)
      if (downloadLatestMatch && req.method === 'GET') {
        if (options.failures?.download) return failureResponse(options.failures.download)
        const namespace = downloadLatestMatch[1]!
        const slug = downloadLatestMatch[2]!
        const skill = findSkill(namespace, slug)
        if (!skill) {
          return Response.json({ code: 404, message: 'not found' }, { status: 404 })
        }
        const bytes = skill.zipBytes ?? MINIMAL_ZIP
        return new Response(bytes as BodyInit, {
          status: 200,
          headers: { 'Content-Type': 'application/zip' }
        })
      }

      // Download (versioned): GET /api/cli/v1/skills/:namespace/:slug/versions/:version/download
      const downloadVersionedMatch = path.match(
        /^\/api\/cli\/v1\/skills\/([^/]+)\/([^/]+)\/versions\/([^/]+)\/download$/
      )
      if (downloadVersionedMatch && req.method === 'GET') {
        if (options.failures?.download) return failureResponse(options.failures.download)
        const namespace = downloadVersionedMatch[1]!
        const slug = downloadVersionedMatch[2]!
        const skill = findSkill(namespace, slug)
        if (!skill) {
          return Response.json({ code: 404, message: 'not found' }, { status: 404 })
        }
        const bytes = skill.zipBytes ?? MINIMAL_ZIP
        return new Response(bytes as BodyInit, {
          status: 200,
          headers: { 'Content-Type': 'application/zip' }
        })
      }

      // Delete: DELETE /api/cli/v1/skills/:namespace/:slug
      const deleteMatch = path.match(/^\/api\/cli\/v1\/skills\/([^/]+)\/([^/]+)$/)
      if (deleteMatch && req.method === 'DELETE') {
        if (options.failures?.deleteRemote) return failureResponse(options.failures.deleteRemote)
        const authErr = checkAuth(req)
        if (authErr) return authErr
        const namespace = deleteMatch[1]!
        const slug = deleteMatch[2]!
        state.delete = {
          namespace,
          slug,
          token: req.headers.get('authorization') ?? null
        }
        const skill = findSkill(namespace, slug)
        if (!skill) {
          return Response.json({ code: 404, message: 'not found' }, { status: 404 })
        }
        return Response.json({
          code: 0,
          data: {
            ok: true,
            scope: namespace,
            action: 'deleted',
            namespace,
            slug
          }
        })
      }

      // Publish: POST /api/cli/v1/skills/:namespace/publish
      const publishMatch = path.match(/^\/api\/cli\/v1\/skills\/([^/]+)\/publish$/)
      if (publishMatch && req.method === 'POST') {
        if (options.failures?.publish) return failureResponse(options.failures.publish)
        const authErr = checkAuth(req)
        if (authErr) return authErr
        const namespace = publishMatch[1]!

        // Parse multipart form data asynchronously — return a Promise<Response>.
        return req.formData().then(form => {
          const fileField = form.get('file')
          const visibility = (form.get('visibility') as string | null) ?? 'PUBLIC'

          // Capture file name from the File object if available, else fallback.
          let fileName = 'skill.zip'
          if (fileField instanceof File) {
            fileName = fileField.name || fileName
          }

          // Record for test assertions.
          state.publish = { namespace, fileName, visibility }

          return Response.json({
            code: 0,
            data: {
              namespace,
              slug: fileName.replace(/\.zip$/, ''),
              version: '1.0.0',
              visibility
            }
          })
        })
      }

      // ------------------------------------------------------------------ //
      // Fallthrough
      // ------------------------------------------------------------------ //
      return Response.json({ code: 404, message: 'not found' }, { status: 404 })
    }
  })

  return {
    url: `http://localhost:${server.port}`,
    stop: () => server.stop(),
    /**
     * Inspect state captured from incoming requests.
     *
     * `received.publish` holds the last publish request's parsed fields:
     *   { namespace, fileName, visibility }
     * It is null until a publish request has been received.
     */
    received: state
  }
}
