import type { ApiError } from "./types"
import { ensureAccessToken } from "@/lib/auth/ensure-access-token"
import { handleUnauthorized } from "@/lib/auth/handle-unauthorized"
import { isSessionExpired } from "@/lib/auth/session-guard-store"

export class OfflineError extends Error {
  constructor() {
    super("Device is offline")
    this.name = "OfflineError"
  }
}

export class ApiRequestError extends Error {
  code: string
  details?: unknown
  status: number

  constructor(status: number, code: string, message: string, details?: unknown) {
    super(message)
    this.name = "ApiRequestError"
    this.status = status
    this.code = code
    this.details = details
  }
}

const DEFAULT_TIMEOUT = 10_000
const DEFAULT_RETRIES = 3
const BASE_DELAY = 1000

class ApiClient {
  constructor(private baseUrl: string) {}

  private resolveUrl(path: string): URL {
    const base = this.baseUrl.startsWith("http")
      ? this.baseUrl
      : `${typeof window !== "undefined" ? window.location.origin : "http://localhost"}${this.baseUrl}`
    // Absolute path (leading /) would replace /api/v1 — keep base path segment.
    const normalizedBase = base.endsWith("/") ? base : `${base}/`
    const normalizedPath = path.startsWith("/") ? path.slice(1) : path
    return new URL(normalizedPath, normalizedBase)
  }

  private async getHeaders(): Promise<HeadersInit> {
    const headers: Record<string, string> = {
      "Content-Type": "application/json",
    }
    const token = await ensureAccessToken()
    if (token) {
      headers["Authorization"] = `Bearer ${token}`
    }
    return headers
  }

  private async request<T>(
    method: string,
    path: string,
    opts?: {
      body?: unknown
      params?: Record<string, string | undefined>
      headers?: Record<string, string>
      timeout?: number
      retries?: number
    }
  ): Promise<T> {
    if (typeof navigator !== "undefined" && !navigator.onLine) {
      throw new OfflineError()
    }

    const url = this.resolveUrl(path)
    if (opts?.params) {
      for (const [key, value] of Object.entries(opts.params)) {
        if (value !== undefined) {
          url.searchParams.set(key, value)
        }
      }
    }

    const timeout = opts?.timeout ?? DEFAULT_TIMEOUT
    const maxRetries = opts?.retries ?? DEFAULT_RETRIES

    let lastError: Error | null = null
    let authRetried = false

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      if (attempt > 0) {
        const delay = BASE_DELAY * Math.pow(2, attempt - 1)
        await new Promise((resolve) => setTimeout(resolve, delay))
      }

      const controller = new AbortController()
      const timer = setTimeout(() => controller.abort(), timeout)

      try {
        const response = await fetch(url.toString(), {
          method,
          headers: { ...(await this.getHeaders()), ...opts?.headers },
          body: opts?.body ? JSON.stringify(opts.body) : undefined,
          signal: controller.signal,
        })

        clearTimeout(timer)

        if (!response.ok) {
          let errorBody: ApiError | undefined
          try {
            errorBody = await response.json()
          } catch {}

          const err = new ApiRequestError(
            response.status,
            errorBody?.code ?? "UNKNOWN",
            errorBody?.message ?? response.statusText,
            errorBody?.details
          )

          if (response.status === 401) {
            if (isSessionExpired()) {
              throw err
            }
            if (!authRetried) {
              authRetried = true
              const resolution = await handleUnauthorized()
              if (resolution === "refreshed") {
                continue
              }
              if (resolution === "offline") {
                throw new OfflineError()
              }
            } else {
              await handleUnauthorized({ forceExpired: true })
            }
          }

          if (response.status >= 400 && response.status < 500) {
            throw err
          }

          lastError = err
          continue
        }

        if (response.status === 204) {
          return undefined as T
        }

        const text = await response.text()
        if (!text.trim()) {
          return undefined as T
        }

        return JSON.parse(text) as T
      } catch (err) {
        clearTimeout(timer)

        if (err instanceof ApiRequestError && err.status >= 400 && err.status < 500) {
          throw err
        }

        lastError = err instanceof Error ? err : new Error(String(err))

        if (err instanceof DOMException && err.name === "AbortError") {
          lastError = new Error("Request timed out")
        }
      }
    }

    throw lastError ?? new Error("Request failed")
  }

  async get<T>(path: string, params?: Record<string, string | undefined>): Promise<T> {
    return this.request<T>("GET", path, { params })
  }

  /** GET binary body (inline attachments, exports). */
  async getBlob(path: string, authRetried = false): Promise<Blob> {
    if (typeof navigator !== "undefined" && !navigator.onLine) {
      throw new OfflineError()
    }
    const url = this.resolveUrl(path)
    const headers: Record<string, string> = {}
    const token = await ensureAccessToken()
    if (token) {
      headers["Authorization"] = `Bearer ${token}`
    }
    const response = await fetch(url.toString(), { method: "GET", headers })
    if (!response.ok) {
      if (response.status === 401) {
        if (isSessionExpired()) {
          throw new ApiRequestError(response.status, "UNKNOWN", response.statusText)
        }
        if (!authRetried) {
          const resolution = await handleUnauthorized()
          if (resolution === "refreshed") {
            return this.getBlob(path, true)
          }
          if (resolution === "offline") {
            throw new OfflineError()
          }
        } else {
          await handleUnauthorized({ forceExpired: true })
        }
      }
      throw new ApiRequestError(
        response.status,
        "UNKNOWN",
        response.statusText
      )
    }
    return response.blob()
  }

  async post<T>(path: string, body?: unknown): Promise<T> {
    return this.request<T>("POST", path, { body })
  }

  async put<T>(
    path: string,
    body?: unknown,
    headers?: Record<string, string>,
  ): Promise<T> {
    return this.request<T>("PUT", path, { body, headers })
  }

  async patch<T>(path: string, body?: unknown): Promise<T> {
    return this.request<T>("PATCH", path, { body })
  }

  async delete(path: string): Promise<void> {
    await this.request<void>("DELETE", path)
  }
}

export const apiClient = new ApiClient(process.env.NEXT_PUBLIC_API_URL ?? "/api/v1")
