import { appConfig } from '@/config/env';
import { useSessionStore } from '@/stores/session-store';
import { ApiError } from '@/lib/errors';
import type { z } from 'zod';
import {
  BillingStatusSchema,
  CheckoutSessionResponseSchema,
  DepotObjectSchema,
  FirebaseTokenResponseSchema,
  GetLivekitTokenResponseSchema,
  HumanSchema,
  ListInvitationsResponseSchema,
  ListNetworksResponseSchema,
  NetworkSchema,
  NetworkUsageSchema,
  PortalSessionResponseSchema,
  PrepareUploadResponseSchema,
  SignInResponseSchema,
} from './types';
import type {
  AcceptInvitationRequest,
  AddMembersRequest,
  BillingCadence,
  CreateNetworkRequest,
  PrepareUploadRequest,
  RequestCodeRequest,
  RevokeInvitationRequest,
  SignInRequest,
} from './types';
import type { LinkMetadata } from '@/lib/link-metadata';

interface ApiClientConfig {
  baseUrl: string;
  getToken: () => string | null;
  onUnauthorized: () => void;
}

class ApiClient {
  private config: ApiClientConfig;

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

  private async send(
    method: string,
    path: string,
    init: { headers?: Record<string, string>; body?: BodyInit } = {},
  ): Promise<Response> {
    const headers: Record<string, string> = { ...init.headers };

    const token = this.config.getToken();
    if (token) {
      headers['Authorization'] = `Bearer ${token}`;
    }

    const response = await fetch(`${this.config.baseUrl}${path}`, {
      method,
      headers,
      body: init.body,
    });

    if (response.status === 401) {
      this.config.onUnauthorized();
      throw new ApiError(401, 'Unauthorized');
    }

    if (!response.ok) {
      const text = await response.text().catch(() => 'Unknown error');
      throw new ApiError(response.status, text);
    }

    return response;
  }

  private async fetch(
    method: string,
    path: string,
    body?: unknown,
  ): Promise<Response> {
    return this.send(method, path, {
      headers: body ? { 'Content-Type': 'application/json' } : undefined,
      body: body ? JSON.stringify(body) : undefined,
    });
  }

  private async request<T>(
    schema: z.ZodType<T>,
    method: string,
    path: string,
    body?: unknown,
  ): Promise<T> {
    const response = await this.fetch(method, path, body);
    const json = await response.json();
    return schema.parse(json);
  }

  private async requestVoid(
    method: string,
    path: string,
    body?: unknown,
  ): Promise<void> {
    await this.fetch(method, path, body);
  }

  // --- Auth ---

  async requestCode(data: RequestCodeRequest): Promise<void> {
    await this.requestVoid('POST', '/auth/request-code', data);
  }

  async signIn(data: SignInRequest) {
    return this.request(SignInResponseSchema, 'POST', '/auth/sign-in', data);
  }

  async me() {
    return this.request(HumanSchema, 'GET', '/auth/me');
  }

  async signOut(): Promise<void> {
    await this.requestVoid('POST', '/auth/sign-out');
  }

  async getFirebaseToken() {
    return this.request(
      FirebaseTokenResponseSchema,
      'POST',
      '/auth/firebase-token',
    );
  }

  // TODO: security: require passing in the particle id once api deprecates this
  async getParticleDownloadUrl(objectId: string): Promise<string> {
    const response = await this.fetch('GET', `/particles/${objectId}/download`);
    const data = await response.json();
    return data.url;
  }

  // --- Settings ---

  async updateSettings(data: {
    email_notifications_enabled?: boolean;
  }): Promise<void> {
    await this.requestVoid('PATCH', '/humans/me/settings', data);
  }

  // --- Avatar ---

  async updateAvatar(blob: Blob): Promise<void> {
    await this.send('PUT', '/humans/me/avatar', {
      headers: { 'Content-Type': blob.type || 'image/jpeg' },
      body: blob,
    });
  }

  async deleteAvatar(): Promise<void> {
    await this.requestVoid('DELETE', '/humans/me/avatar');
  }

  async getAvatarDownloadUrl(objectId: string): Promise<string> {
    const response = await this.fetch('GET', `/humans/avatar/${objectId}`);
    const data = await response.json();
    return data.url;
  }

  // --- Depot ---

  async prepareUpload(data: PrepareUploadRequest) {
    return this.request(
      PrepareUploadResponseSchema,
      'POST',
      '/depot/upload',
      data,
    );
  }

  async confirmUpload(objectId: string) {
    return this.request(
      DepotObjectSchema,
      'POST',
      `/depot/objects/${objectId}/confirm`,
    );
  }

  // --- Networks ---

  async listNetworks() {
    return this.request(ListNetworksResponseSchema, 'GET', '/networks');
  }

  async createNetwork(data: CreateNetworkRequest) {
    return this.request(NetworkSchema, 'POST', '/networks', data);
  }

  async getNetwork(id: string) {
    return this.request(NetworkSchema, 'GET', `/networks/${id}`);
  }

  async addMembers(networkId: string, data: AddMembersRequest): Promise<void> {
    await this.requestVoid('POST', `/networks/${networkId}/members`, data);
  }

  async removeMember(networkId: string, humanId: string): Promise<void> {
    await this.requestVoid(
      'DELETE',
      `/networks/${networkId}/members/${humanId}`,
    );
  }

  // --- Invitations ---

  async listNetworkInvitations(networkId: string) {
    return this.request(
      ListInvitationsResponseSchema,
      'GET',
      `/networks/${networkId}/invitations`,
    );
  }

  async listMyInvitations() {
    return this.request(ListInvitationsResponseSchema, 'GET', '/invitations');
  }

  async acceptInvitation(data: AcceptInvitationRequest): Promise<void> {
    await this.requestVoid('POST', '/invitations/accept', data);
  }

  async revokeInvitation(
    networkId: string,
    data: RevokeInvitationRequest,
  ): Promise<void> {
    await this.requestVoid(
      'DELETE',
      `/networks/${networkId}/invitations`,
      data,
    );
  }

  // --- LiveKit ---

  async getLivekitToken(networkId: string, streamId: string) {
    return this.request(
      GetLivekitTokenResponseSchema,
      'POST',
      '/livekit/token',
      { network_id: networkId, stream_id: streamId },
    );
  }

  // --- Billing (network admin only) ---

  async getNetworkBilling(networkId: string) {
    return this.request(
      BillingStatusSchema,
      'GET',
      `/networks/${networkId}/billing`,
    );
  }

  async createCheckoutSession(networkId: string, cadence: BillingCadence) {
    return this.request(
      CheckoutSessionResponseSchema,
      'POST',
      `/networks/${networkId}/billing/checkout-session`,
      { cadence },
    );
  }

  async createPortalSession(networkId: string) {
    return this.request(
      PortalSessionResponseSchema,
      'POST',
      `/networks/${networkId}/billing/portal-session`,
    );
  }

  async getNetworkUsage(networkId: string) {
    return this.request(
      NetworkUsageSchema,
      'GET',
      `/networks/${networkId}/usage`,
    );
  }

  // --- Link metadata ---

  async getLinkMetadata(url: string): Promise<LinkMetadata | null> {
    const response = await this.fetch(
      'GET',
      `/metadata?url=${encodeURIComponent(url)}`,
    );
    return (await response.json()) as LinkMetadata | null;
  }
}

export const apiClient = new ApiClient({
  baseUrl: appConfig.orionUrl,
  getToken: () => useSessionStore.getState().token,
  onUnauthorized: () => useSessionStore.getState().clearToken(),
});
