/**
 * Typed API client
 * @countermeasure/web-components/services/api
 */

/** Methods that modify server state and should include CSRF tokens */
const STATE_CHANGING_METHODS = new Set(['POST', 'PUT', 'DELETE', 'PATCH'])

export interface ApiClientConfig {
  baseUrl?: string
  headers?: Record<string, string>
  getAuthToken?: () => string | null
  onUnauthorized?: () => void
  /** Optional CSRF token getter — token is auto-injected on POST/PUT/DELETE/PATCH */
  getCsrfToken?: () => string | null
  /** Header name for the CSRF token (default: 'X-CSRF-Token') */
  csrfHeaderName?: string
}

export interface ApiError {
  status: number
  message: string
  body?: unknown
}

class ApiErrorImpl extends Error implements ApiError {
  public readonly status: number
  public readonly body?: unknown

  constructor(status: number, message: string, body?: unknown) {
    super(message)
    this.name = 'ApiError'
    this.status = status
    this.body = body
  }
}

function buildUrl(baseUrl: string, path: string): string {
  const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl
  const normalizedPath = path.startsWith('/') ? path : `/${path}`
  return `${base}${normalizedPath}`
}

export class ApiClient {
  private readonly config: ApiClientConfig

  constructor(config: ApiClientConfig) {
    this.config = config
  }

  async get<T>(path: string, options?: RequestInit): Promise<T> {
    return this.request<T>(path, { ...options, method: 'GET' })
  }

  async post<T>(path: string, body?: unknown, options?: RequestInit): Promise<T> {
    const mergedHeaders: Record<string, string> = {
      'Content-Type': 'application/json',
    }
    if (options?.headers) {
      const optHeaders = options.headers as Record<string, string>
      for (const hKey of Object.keys(optHeaders)) {
        const hVal = optHeaders[hKey]
        if (hVal !== undefined) {
          mergedHeaders[hKey] = hVal
        }
      }
    }
    const init: RequestInit = {
      method: 'POST',
      headers: mergedHeaders,
    }
    if (body !== undefined) {
      init.body = JSON.stringify(body)
    }
    return this.request<T>(path, init)
  }

  async put<T>(path: string, body?: unknown, options?: RequestInit): Promise<T> {
    const mergedHeaders: Record<string, string> = {
      'Content-Type': 'application/json',
    }
    if (options?.headers) {
      const optHeaders = options.headers as Record<string, string>
      for (const hKey of Object.keys(optHeaders)) {
        const hVal = optHeaders[hKey]
        if (hVal !== undefined) {
          mergedHeaders[hKey] = hVal
        }
      }
    }
    const init: RequestInit = {
      method: 'PUT',
      headers: mergedHeaders,
    }
    if (body !== undefined) {
      init.body = JSON.stringify(body)
    }
    return this.request<T>(path, init)
  }

  async delete<T>(path: string, options?: RequestInit): Promise<T> {
    return this.request<T>(path, { ...options, method: 'DELETE' })
  }

  async request<T>(path: string, options?: RequestInit): Promise<T> {
    const baseUrl = this.config.baseUrl ?? ''
    const url = buildUrl(baseUrl, path)

    const headers: Record<string, string> = {
      ...this.config.headers,
    }

    if (this.config.getAuthToken) {
      const token = this.config.getAuthToken()
      if (token) {
        headers.Authorization = `Bearer ${token}`
      }
    }

    // Inject CSRF token on state-changing requests
    const method = (options?.method ?? 'GET').toUpperCase()
    if (this.config.getCsrfToken && STATE_CHANGING_METHODS.has(method)) {
      const csrfToken = this.config.getCsrfToken()
      if (csrfToken) {
        headers[this.config.csrfHeaderName ?? 'X-CSRF-Token'] = csrfToken
      }
    }

    const mergedHeaders: Record<string, string> = {
      ...headers,
    }

    const requestOptions = options ?? {}
    if (requestOptions.headers) {
      const optHeaders = requestOptions.headers as Record<string, string>
      for (const key of Object.keys(optHeaders)) {
        const value = optHeaders[key]
        if (value !== undefined) {
          mergedHeaders[key] = value
        }
      }
    }

    const response = await fetch(url, {
      ...requestOptions,
      headers: mergedHeaders,
    })

    if (response.status === 401) {
      if (this.config.onUnauthorized) {
        this.config.onUnauthorized()
      }
    }

    if (!response.ok) {
      let body: unknown
      try {
        body = await response.json()
      } catch {
        // Response body is not JSON
      }

      let message = `Request failed with status ${String(response.status)}`
      if (body !== null && body !== undefined && typeof body === 'object' && 'message' in body) {
        const bodyMessage = (body as Record<string, unknown>).message
        if (typeof bodyMessage === 'string') {
          message = bodyMessage
        }
      }

      throw new ApiErrorImpl(response.status, message, body)
    }

    const json: T = (await response.json()) as T
    return json
  }
}

export function createApiClient(config?: ApiClientConfig): ApiClient {
  return new ApiClient(config ?? {})
}
