import type {
  AdminUser,
  AudioStatus,
  DiscordGuild,
  GridMap,
  ListedMap,
  MapAccessRequest,
  MapPermission,
  MapRole,
  MapState,
  PublicAccess,
  UserInfo,
} from "./types";

const GRID_BASE = "/api/grid";
const AUTH_BASE = "/api/auth";

async function request<T>(url: string, init?: RequestInit): Promise<T> {
  const res = await fetch(url, {
    ...init,
    credentials: "include",
    headers: {
      ...(init?.headers as Record<string, string>),
    },
  });

  if (!res.ok) {
    const text = await res.text().catch(() => res.statusText);
    throw new Error(`${res.status}: ${text}`);
  }

  // Read the bdoy if it exists
  const text = await res.text();
  if (!text) return undefined as T;

  // Parse JSON if it exists
  const contentType = res.headers.get("content-type") ?? "";
  if (contentType.includes("application/json")) {
    return JSON.parse(text) as T;
  } else if (contentType.includes("text/plain")) {
    return text as unknown as T;
  }

  throw new Error(`Expected JSON or text but got: ${contentType}`);
}

export const api = {
  /** List maps where the authenticated user has a direct role or has favorited. */
  listMaps: (): Promise<ListedMap[]> =>
    request<ListedMap[]>(`${GRID_BASE}/maps`),

  /** Create a new map (authenticated). */
  createMap: (
    name: string,
    public_access: PublicAccess = "private",
  ): Promise<GridMap> =>
    request<GridMap>(`${GRID_BASE}/maps`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, public_access }),
    }),

  /** Get full map state (cells, tokens, colors). */
  getMap: (id: string): Promise<MapState> =>
    request<MapState>(`${GRID_BASE}/maps/${id}`),

  /** Update map name and/or public_access (owner only). */
  updateMap: (
    id: string,
    payload: { name?: string; public_access?: PublicAccess },
  ): Promise<GridMap> =>
    request<GridMap>(`${GRID_BASE}/maps/${id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }),

  /** Delete a map (owner only). */
  deleteMap: (id: string): Promise<void> =>
    request<void>(`${GRID_BASE}/maps/${id}`, { method: "DELETE" }),

  // ---- Permissions ----

  /** List all permissions for a map including usernames (owner only). */
  listPermissions: (mapId: string): Promise<MapPermission[]> =>
    request<MapPermission[]>(`${GRID_BASE}/maps/${mapId}/permissions`),

  /**
   * Add or update a user's role by username.
   * Pass `role: null` to remove the user's permission entirely.
   */
  updatePermission: (
    mapId: string,
    username: string,
    role: MapRole | null,
  ): Promise<void> =>
    request<void>(`${GRID_BASE}/maps/${mapId}/permissions`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, role }),
    }),

  /** Favorite a map (adds it to the user's map list). */
  favoriteMap: (id: string): Promise<void> =>
    request<void>(`${GRID_BASE}/maps/${id}/favorite`, { method: "POST" }),

  /** Un-favorite a map. */
  unfavoriteMap: (id: string): Promise<void> =>
    request<void>(`${GRID_BASE}/maps/${id}/favorite`, { method: "DELETE" }),

  /** Request viewer or editor access to a map. */
  requestAccess: (mapId: string, role: "editor" | "viewer"): Promise<void> =>
    request<void>(`${GRID_BASE}/maps/${mapId}/access-requests`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ role }),
    }),

  /** List pending access requests for a map (owner only). */
  listAccessRequests: (mapId: string): Promise<MapAccessRequest[]> =>
    request<MapAccessRequest[]>(`${GRID_BASE}/maps/${mapId}/access-requests`),

  /** Approve or deny a pending access request (owner only). */
  resolveAccessRequest: (
    mapId: string,
    requestId: string,
    action: "approve" | "deny",
  ): Promise<void> =>
    request<void>(`${GRID_BASE}/maps/${mapId}/access-requests/${requestId}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action }),
    }),
};

export const auth = {
  /** Fetch the currently authenticated user's info. Returns null if not logged in. */
  async me(): Promise<UserInfo | null> {
    try {
      return await request<UserInfo>(`${AUTH_BASE}/me`);
    } catch {
      return null;
    }
  },

  /** Register a new local account. */
  async register(username: string, password: string): Promise<void> {
    await request<void>(`${AUTH_BASE}/register`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password }),
    });
  },

  /** Login with username and password. */
  async loginLocal(username: string, password: string): Promise<void> {
    await request<void>(`${AUTH_BASE}/login`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password }),
    });
  },

  /** Start Discord OAuth login flow (anonymous). */
  async loginDiscord(redirectUri?: string): Promise<void> {
    const target = encodeURIComponent(redirectUri ?? window.location.href);
    const response = await request<string>(
      `${AUTH_BASE}/discord/authorize?redirect_uri=${target}`,
    );
    window.location.href = JSON.parse(response);
  },

  /** Start Discord OAuth connect flow (authenticated users only). */
  async connectDiscord(redirectUri?: string): Promise<void> {
    const target = encodeURIComponent(
      redirectUri ?? window.location.origin + "/account",
    );
    const response = await request<string>(
      `${AUTH_BASE}/discord/connect?redirect_uri=${target}`,
    );
    window.location.href = JSON.parse(response);
  },

  /** Clear the session cookie server-side and reload. */
  async logout(): Promise<void> {
    try {
      await request<void>(`${AUTH_BASE}/logout`, { method: "POST" });
    } catch {
      // Ignore errors
    }
    window.location.href = "/map";
  },

  /**
   * Update the user's first and/or last name.
   * Pass an empty string to clear a field; pass undefined to leave it unchanged.
   */
  async updateProfile(
    firstName?: string,
    lastName?: string,
  ): Promise<UserInfo> {
    return request<UserInfo>(`${AUTH_BASE}/profile`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ first_name: firstName, last_name: lastName }),
    });
  },

  /**
   * Change or set the account password.
   * `currentPassword` is required when the account already has a password,
   * and can be omitted (null/undefined) for OAuth-only accounts setting a
   * password for the first time.
   */
  async changePassword(
    currentPassword: string | null,
    newPassword: string,
  ): Promise<void> {
    await request<void>(`${AUTH_BASE}/change-password`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        current_password: currentPassword ?? undefined,
        new_password: newPassword,
      }),
    });
  },

  /** Disconnect an OAuth provider (requires a password to be set first). */
  async disconnectProvider(provider: string): Promise<void> {
    await request<void>(`${AUTH_BASE}/connections/${provider}`, {
      method: "DELETE",
    });
  },
};

const ADMIN_BASE = "/api/admin";

export const adminApi = {
  /** List all user accounts (admin only). */
  listUsers: (): Promise<AdminUser[]> =>
    request<AdminUser[]>(`${ADMIN_BASE}/users`),

  /** Change a user's site role (admin only). */
  setUserRole: (id: string, role: "admin" | "user"): Promise<void> =>
    request<void>(`${ADMIN_BASE}/users/${id}/role`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ role }),
    }),

  /** Ban a user account (admin only). */
  banUser: (id: string): Promise<void> =>
    request<void>(`${ADMIN_BASE}/users/${id}/ban`, { method: "PUT" }),

  /** Unban a user account (admin only). */
  unbanUser: (id: string): Promise<void> =>
    request<void>(`${ADMIN_BASE}/users/${id}/unban`, { method: "PUT" }),

  /** Permanently delete a user account (admin only). */
  deleteUser: (id: string): Promise<void> =>
    request<void>(`${ADMIN_BASE}/users/${id}`, { method: "DELETE" }),
};

const AUDIO_BASE = "/api/audio";

export const audioApi = {
  /** List all Discord guilds the bot is currently in. */
  listGuilds: (): Promise<DiscordGuild[]> =>
    request<DiscordGuild[]>(`${AUDIO_BASE}/guilds`),

  /** Get the current audio status for a guild (voice channel, track, queue). */
  getStatus: (guildId: string): Promise<AudioStatus> =>
    request<AudioStatus>(`${AUDIO_BASE}/${guildId}/status`),

  /** Enqueue a track URL for playback (bot joins the caller's voice channel). */
  play: (guildId: string, url: string): Promise<void> =>
    request<void>(`${AUDIO_BASE}/${guildId}/play`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ url }),
    }),

  /** Pause the currently playing track. */
  pause: (guildId: string): Promise<void> =>
    request<void>(`${AUDIO_BASE}/${guildId}/pause`, { method: "POST" }),

  /** Resume a paused track. */
  resume: (guildId: string): Promise<void> =>
    request<void>(`${AUDIO_BASE}/${guildId}/resume`, { method: "POST" }),

  /** Stop playback and clear the queue. */
  stop: (guildId: string): Promise<void> =>
    request<void>(`${AUDIO_BASE}/${guildId}/stop`, { method: "POST" }),

  /** Skip the current track. */
  skip: (guildId: string): Promise<void> =>
    request<void>(`${AUDIO_BASE}/${guildId}/skip`, { method: "POST" }),
};
