import { z } from 'zod';

type QueryValue = string | number | boolean | undefined | null;

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

export type RequestQuery = Record<string, QueryValue | QueryValue[] | undefined>;

export interface HttpRequestOptions extends Omit<RequestInit, 'body' | 'method'> {
  query?: RequestQuery;
  body?: Record<string, unknown> | FormData;
}

const API_BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8080';
const ADMIN_TOKEN_STORAGE_KEY = 'admin_token';
const ADMIN_API_PREFIX = '/api/v1/admin';
const isBrowserEnvironment = typeof window !== 'undefined';

let adminAuthToken: string | null = null;

const loadAdminTokenFromStorage = () => {
  if (!isBrowserEnvironment) {
    return null;
  }

  const storedToken = window.localStorage.getItem(ADMIN_TOKEN_STORAGE_KEY);
  adminAuthToken = storedToken;
  return storedToken;
};

loadAdminTokenFromStorage();

export const setAdminAuthToken = (token: string | null) => {
  adminAuthToken = token;

  if (!isBrowserEnvironment) {
    return;
  }

  if (token) {
    window.localStorage.setItem(ADMIN_TOKEN_STORAGE_KEY, token);
  } else {
    window.localStorage.removeItem(ADMIN_TOKEN_STORAGE_KEY);
  }
};

export const getAdminAuthToken = () => adminAuthToken;

export class ApiError extends Error {
  status: number;
  details?: unknown;

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

const buildQueryString = (query?: RequestQuery) => {
  if (!query) {
    return '';
  }

  const params = new URLSearchParams();

  Object.entries(query).forEach(([key, value]) => {
    if (value === undefined || value === null) {
      return;
    }

    if (Array.isArray(value)) {
      value.forEach((item) => {
        if (item === undefined || item === null) {
          return;
        }
        params.append(key, String(item));
      });
      return;
    }

    params.append(key, String(value));
  });

  const serialized = params.toString();
  return serialized ? `?${serialized}` : '';
};

const buildUrl = (path: string, query?: RequestQuery) => {
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
  return `${API_BASE_URL}${normalizedPath}${buildQueryString(query)}`;
};

const shouldAttachAdminAuth = (path: string) => path.startsWith(ADMIN_API_PREFIX);

const buildHeaders = (base: Record<string, string>, extra?: HeadersInit) => {
  const headers = new Headers(base);

  if (extra) {
    const additional = new Headers(extra);
    additional.forEach((value, key) => {
      headers.set(key, value);
    });
  }

  return headers;
};

const send = async <T>(
  method: HttpMethod,
  path: string,
  schema: z.ZodType<T>,
  options?: HttpRequestOptions,
): Promise<T> => {
  const { query, body, headers, ...rest } = options ?? {};
  const url = buildUrl(path, query);
  const hasJsonBody = body && !(body instanceof FormData);
  const shouldSendBody = method !== 'GET' && method !== 'DELETE' ? true : Boolean(body);
  const baseHeaders: Record<string, string> = {
    Accept: 'application/json',
    ...(hasJsonBody ? { 'Content-Type': 'application/json' } : {}),
  };

  const requestHeaders = buildHeaders(baseHeaders, headers);

  if (shouldAttachAdminAuth(path) && adminAuthToken) {
    requestHeaders.set('Authorization', `Bearer ${adminAuthToken}`);
  }

  const response = await fetch(url, {
    method,
    ...rest,
    headers: requestHeaders,
    body: shouldSendBody ? (hasJsonBody ? JSON.stringify(body) : (body ?? null)) : undefined,
  });

  const contentType = response.headers.get('content-type');
  const contentLength = response.headers.get('content-length');
  const isNoContentStatus = response.status === 204 || response.status === 205;
  const payloadHasBody = !isNoContentStatus && contentLength !== '0';

  let payload: unknown = undefined;

  if (payloadHasBody) {
    const rawBody = await response.text();
    if (rawBody.length > 0) {
      if (contentType && contentType.includes('application/json')) {
        try {
          payload = JSON.parse(rawBody);
        } catch {
          payload = rawBody;
        }
      } else {
        payload = rawBody;
      }
    }
  }

  if (!response.ok) {
    const serverMessage =
      typeof payload === 'object' && payload !== null && 'error' in payload && typeof (payload as Record<string, unknown>).error === 'string'
        ? (payload as { error: string }).error
        : null;
    if (response.status === 401) {
      setAdminAuthToken(null);
      if (isBrowserEnvironment) {
        window.location.replace('/admin');
      }
    }
    throw new ApiError(serverMessage ?? 'Request failed', response.status, payload);
  }

  return schema.parse(payload);
};

export const request = async <T>(
  path: string,
  schema: z.ZodType<T>,
  options?: HttpRequestOptions,
) => send('GET', path, schema, options);

export const post = async <T>(
  path: string,
  schema: z.ZodType<T>,
  options?: HttpRequestOptions,
) => send('POST', path, schema, options);

export const put = async <T>(
  path: string,
  schema: z.ZodType<T>,
  options?: HttpRequestOptions,
) => send('PUT', path, schema, options);

export const del = async <T>(
  path: string,
  schema: z.ZodType<T>,
  options?: HttpRequestOptions,
) => send('DELETE', path, schema, options);
