export class ApiError extends Error {
  status: number;
  body: Record<string, unknown>;

  constructor(status: number, body: Record<string, unknown>) {
    const msg = (body.error as string) || `API error: ${status}`;
    super(msg);
    this.name = "ApiError";
    this.status = status;
    this.body = body;
  }
}

async function handleResponse<T>(res: Response, path: string): Promise<T> {
  if (!res.ok) {
    let body: Record<string, unknown> = {};
    try {
      body = await res.json();
    } catch {
      body = { error: `API error: ${res.status}` };
    }

    // Redirect to login on 401 (except for auth endpoints)
    if (res.status === 401 && !path.startsWith("/api/auth/")) {
      const { default: router } = await import("@/router/index");
      const currentPath = window.location.pathname;
      if (currentPath !== "/login" && currentPath !== "/register") {
        router.push({ name: "login", query: { redirect: currentPath } });
      }
    }

    throw new ApiError(res.status, body);
  }
  // Handle 204 No Content
  if (res.status === 204) {
    return undefined as T;
  }
  return res.json() as Promise<T>;
}

/**
 * The server's `{"error": "..."}` message from a failed call, or `fallback`
 * when the failure carried none (network error, non-JSON body). The one place
 * the error envelope is unpacked on the client — views used to restate this
 * as a six-line `"body" in e` branch at every catch site.
 */
export function apiErrorMessage(e: unknown, fallback: string): string {
  if (e && typeof e === "object" && "body" in e) {
    const body = (e as { body?: { error?: unknown } }).body;
    if (body && typeof body.error === "string" && body.error) return body.error;
  }
  return fallback;
}

export async function apiGet<T>(path: string): Promise<T> {
  const res = await fetch(path);
  return handleResponse<T>(res, path);
}

export async function apiPost<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  return handleResponse<T>(res, path);
}

export async function apiPut<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(path, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  return handleResponse<T>(res, path);
}

export async function apiPatch<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(path, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  return handleResponse<T>(res, path);
}

export async function apiDelete(path: string): Promise<void> {
  const res = await fetch(path, { method: "DELETE" });
  return handleResponse<void>(res, path);
}

// ---------------------------------------------------------------------------
// Sharing, Groups, Notifications, User search
// ---------------------------------------------------------------------------

export interface ShareEntry {
  id: number
  permission: string
  shared_with_user_id: number | null
  shared_with_group_id: number | null
  username?: string
  group_name?: string
  invited_by: number
  created_at: string
}

export interface GroupEntry {
  id: number
  name: string
  description: string | null
  created_by: number | null
  member_count: number
  is_member: boolean
  created_at: string
  updated_at: string
}

export interface GroupMember {
  id: number
  group_id: number
  user_id: number
  role: string
  username: string
  email: string | null
  created_at: string
}

export interface NotificationEntry {
  id: number
  user_id: number
  type: string
  payload: Record<string, unknown>
  read_at: string | null
  created_at: string
}

export interface UserSearchResult {
  id: number
  username: string
}

// --- User search ---
export const searchUsers = (q: string) =>
  apiGet<{ users: UserSearchResult[] }>(`/api/users/search?q=${encodeURIComponent(q)}`).then(r => r.users)

// --- Groups ---
export const listGroups = () => apiGet<{ groups: GroupEntry[] }>('/api/groups').then(r => r.groups)
export const createGroup = (name: string, description?: string) =>
  apiPost<GroupEntry>('/api/groups', { name, description })
export const updateGroup = (id: number, data: { name?: string; description?: string }) =>
  apiPatch<GroupEntry>(`/api/groups/${id}`, data)
export const deleteGroup = (id: number) => apiDelete(`/api/groups/${id}`)
export const getGroupDetail = (id: number) =>
  apiGet<GroupEntry & { members: GroupMember[] }>(`/api/groups/${id}`)
export const listGroupMembers = (id: number) =>
  apiGet<{ members: GroupMember[] }>(`/api/groups/${id}/members`).then(r => r.members)
export const addGroupMember = (groupId: number, userId: number, role: string) =>
  apiPost<GroupMember>(`/api/groups/${groupId}/members`, { user_id: userId, role })
export const updateGroupMember = (groupId: number, userId: number, role: string) =>
  apiPatch<GroupMember>(`/api/groups/${groupId}/members/${userId}`, { role })
export const removeGroupMember = (groupId: number, userId: number) =>
  apiDelete(`/api/groups/${groupId}/members/${userId}`)

// --- Project shares ---
export const listProjectShares = (projectId: number) =>
  apiGet<{ shares: ShareEntry[] }>(`/api/projects/${projectId}/shares`).then(r => r.shares)
export const createProjectShare = (projectId: number, body: { user_id?: number; group_id?: number; permission: string }) =>
  apiPost<ShareEntry>(`/api/projects/${projectId}/shares`, body)
export const updateProjectShare = (projectId: number, shareId: number, permission: string) =>
  apiPatch<ShareEntry>(`/api/projects/${projectId}/shares/${shareId}`, { permission })
export const deleteProjectShare = (projectId: number, shareId: number) =>
  apiDelete(`/api/projects/${projectId}/shares/${shareId}`)

// --- Note / task shares ---
export const listNoteShares = (noteId: number) =>
  apiGet<{ shares: ShareEntry[] }>(`/api/notes/${noteId}/shares`).then(r => r.shares)
export const createNoteShare = (noteId: number, body: { user_id?: number; group_id?: number; permission: string }) =>
  apiPost<ShareEntry>(`/api/notes/${noteId}/shares`, body)
export const updateNoteShare = (noteId: number, shareId: number, permission: string) =>
  apiPatch<ShareEntry>(`/api/notes/${noteId}/shares/${shareId}`, { permission })
export const deleteNoteShare = (noteId: number, shareId: number) =>
  apiDelete(`/api/notes/${noteId}/shares/${shareId}`)

// --- Shared-with-me ---
export const getSharedWithMe = () =>
  apiGet<{ projects: Record<string, unknown>[]; notes: Record<string, unknown>[] }>('/api/shared-with-me')

// --- In-app notifications ---
export const getNotifications = (all = false) =>
  apiGet<{ notifications: NotificationEntry[] }>(`/api/notifications${all ? '?all=true' : ''}`).then(r => r.notifications)
export const getNotificationCount = () =>
  apiGet<{ count: number }>('/api/notifications/count').then(r => r.count)
export const markNotificationRead = (id: number) => apiPost<void>(`/api/notifications/${id}/read`, {})
export const markAllNotificationsRead = () => apiPost<{ marked: number }>('/api/notifications/read-all', {})

export interface SSEStreamHandle {
  close(): void;
  /** Resolves when the stream closes (normally or via error/abort). */
  done: Promise<void>;
}

export interface SSENamedEvent {
  id: number;
  event: string;
  data: Record<string, unknown>;
}

export function apiSSEStream(
  path: string,
  onEvent: (event: SSENamedEvent) => void,
  options?: { lastEventId?: number; signal?: AbortSignal },
): SSEStreamHandle {
  const controller = new AbortController();
  const combinedSignal = options?.signal
    ? AbortSignal.any([controller.signal, options.signal])
    : controller.signal;

  const headers: Record<string, string> = {};
  if (options?.lastEventId !== undefined) {
    headers["Last-Event-ID"] = String(options.lastEventId);
  }

  const done = (async () => {
    const res = await fetch(path, { headers, signal: combinedSignal });
    if (!res.ok) {
      let body: Record<string, unknown> = {};
      try {
        body = await res.json();
      } catch {
        body = { error: `API error: ${res.status}` };
      }
      if (res.status === 401 && !path.startsWith("/api/auth/")) {
        const { default: router } = await import("@/router/index");
        router.push({ name: "login" });
      }
      throw new ApiError(res.status, body);
    }
    const reader = res.body?.getReader();
    if (!reader) throw new Error("No response body");

    const decoder = new TextDecoder();
    let buffer = "";

    // SSE field parsing state
    let currentId = -1;
    let currentEvent = "message";
    let currentData = "";

    function dispatch() {
      if (!currentData) return;
      let parsed: Record<string, unknown>;
      try {
        parsed = JSON.parse(currentData);
      } catch {
        return;
      }
      onEvent({ id: currentId, event: currentEvent, data: parsed });
      // Reset for next event
      currentEvent = "message";
      currentData = "";
    }

    function processLine(line: string) {
      if (line === "") {
        // Empty line = end of event
        dispatch();
        return;
      }
      if (line.startsWith(":")) return; // comment / keepalive
      const colonIdx = line.indexOf(":");
      let field: string;
      let value: string;
      if (colonIdx === -1) {
        field = line;
        value = "";
      } else {
        field = line.slice(0, colonIdx);
        value = line.slice(colonIdx + 1);
        if (value.startsWith(" ")) value = value.slice(1);
      }
      switch (field) {
        case "id":
          currentId = parseInt(value, 10);
          break;
        case "event":
          currentEvent = value;
          break;
        case "data":
          currentData += value;
          break;
      }
    }

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";
      for (const line of lines) {
        processLine(line);
      }
    }
    // Process remaining buffer
    if (buffer) {
      processLine(buffer);
    }
    dispatch();
  })().catch(() => {
    // Stream closed or aborted — handled by caller via reconnection
  });

  return { close: () => controller.abort(), done };
}

export async function apiStreamPost(
  path: string,
  body: unknown,
  onChunk: (data: Record<string, unknown>) => void
): Promise<void> {
  const res = await fetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    let errBody: Record<string, unknown> = {};
    try {
      errBody = await res.json();
    } catch {
      errBody = { error: `API error: ${res.status}` };
    }

    if (res.status === 401 && !path.startsWith("/api/auth/")) {
      const { default: router } = await import("@/router/index");
      router.push({ name: "login" });
    }

    throw new ApiError(res.status, errBody);
  }
  const reader = res.body?.getReader();
  if (!reader) throw new Error("No response body");

  const decoder = new TextDecoder();
  let buffer = "";

  function processLine(line: string) {
    const trimmed = line.trim();
    if (trimmed.startsWith("data: ")) {
      let data;
      try {
        data = JSON.parse(trimmed.slice(6));
      } catch {
        return; // Skip malformed JSON lines
      }
      onChunk(data);
    }
  }

  function processBuffer() {
    const lines = buffer.split("\n");
    // Keep the last (possibly incomplete) line in the buffer
    buffer = lines.pop() || "";
    for (const line of lines) {
      processLine(line);
    }
  }

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      processBuffer();
    }
  } catch {
    // Stream may close with a network error after all data was sent.
  }

  // Flush any remaining complete lines in the buffer
  if (buffer.trim()) {
    // The buffer may contain one or more unprocessed lines
    const remaining = buffer;
    buffer = "";
    for (const line of remaining.split("\n")) {
      processLine(line);
    }
  }
}

// ─── API Keys ─────────────────────────────────────────────────────────────────

export interface ApiKeyEntry {
  id: number
  name: string
  scope: string
  key_prefix: string
  last_used_at: string | null
}

export const listApiKeys = () =>
  apiGet<{ api_keys: ApiKeyEntry[] }>('/api/api-keys').then(r => r.api_keys)

export const createApiKey = (name: string, scope: 'read' | 'write') =>
  apiPost<{ key: string; api_key: ApiKeyEntry }>('/api/api-keys', { name, scope })

export const revokeApiKey = (id: number) => apiDelete(`/api/api-keys/${id}`)

// ── User Profile ─────────────────────────────────────────────────────────────

export interface UserProfile {
  display_name: string
  job_title: string
  industry: string
  expertise_level: 'novice' | 'intermediate' | 'expert'
  response_style: 'concise' | 'balanced' | 'detailed'
  tone: 'casual' | 'professional' | 'technical'
  interests: string[]
  work_schedule: { days?: string[]; start?: string; end?: string }
}

export const getProfile = () => apiGet<UserProfile>('/api/profile')
export const updateProfile = (data: Partial<UserProfile>) =>
  apiPut<UserProfile>('/api/profile', data)


// ── Note Versions (pinning) ──────────────────────────────────────────────────

import type { NoteVersion } from '../types/task'

/** Mark a note version as manually pinned, optionally with a commit-note
 *  label. Re-calling with a different label updates the label. */
export const pinNoteVersion = (noteId: number, versionId: number, label?: string | null) =>
  apiPost<NoteVersion>(
    `/api/notes/${noteId}/versions/${versionId}/pin`,
    { label: label ?? null },
  )

/** Downgrade a manually-pinned version back to rolling. Does NOT delete
 *  the row — older rows may be FIFO-pruned by the next autosave. */
export const unpinNoteVersion = (noteId: number, versionId: number) =>
  apiDelete(`/api/notes/${noteId}/versions/${versionId}/pin`)
