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>;
}

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 };
}

// ---------------------------------------------------------------------------
// Journal
// ---------------------------------------------------------------------------

export interface JournalLocation {
  label: string;
  address: string;
  lat?: number;
  lon?: number;
}

export interface JournalConfig {
  prep_enabled: boolean;
  prep_hour: number;
  prep_minute: number;
  day_rollover_hour: number;
  morning_end_hour?: number;
  midday_end_hour?: number;
  closeout_enabled?: boolean;
  // Ambient-context fields (carried forward from the briefing config schema)
  locations?: { home?: JournalLocation; work?: JournalLocation };
  temp_unit?: 'C' | 'F';
  use_caldav_event_locations?: boolean;
  enabled?: boolean;
  [key: string]: unknown;
}


export interface JournalConversation {
  id: number;
  title: string;
  model: string;
  conversation_type: string;
  day_date: string | null;
  rag_project_id: number | null;
  message_count: number;
  created_at: string;
  updated_at: string;
}

export interface JournalMessage {
  id: number;
  conversation_id: number;
  role: 'user' | 'assistant' | 'system';
  content: string;
  status: string;
  context_note_id: number | null;
  tool_calls: unknown[] | null;
  metadata: Record<string, unknown> | null;
  created_at: string;
}

export interface JournalDayPayload {
  day_date: string;
  conversation: JournalConversation | null;
  messages: JournalMessage[];
}

export interface JournalMoment {
  id: number;
  user_id: number;
  conversation_id: number | null;
  source_message_id: number | null;
  day_date: string;
  occurred_at: string;
  recorded_at: string;
  content: string;
  raw_excerpt: string | null;
  tags: string[];
  pinned: boolean;
  people: { id: number; title: string }[];
  places: { id: number; title: string }[];
  task_ids: number[];
  note_ids: number[];
  score?: number;
}

export async function getJournalConfig(): Promise<JournalConfig> {
  return apiGet<JournalConfig>('/api/journal/config');
}

export async function saveJournalConfig(config: JournalConfig): Promise<void> {
  await apiPut('/api/journal/config', config);
}

export async function getJournalToday(): Promise<JournalDayPayload> {
  return apiGet<JournalDayPayload>('/api/journal/today');
}

export async function getJournalDay(isoDate: string): Promise<JournalDayPayload> {
  return apiGet<JournalDayPayload>(`/api/journal/day/${isoDate}`);
}

export async function getJournalDays(): Promise<string[]> {
  const data = await apiGet<{ days: string[] }>('/api/journal/days');
  return data.days;
}

export async function triggerJournalPrep(date?: string): Promise<{ ok: boolean; message_id: number }> {
  return apiPost('/api/journal/trigger-prep', date ? { date } : {});
}

export interface CuratorRunResult {
  conv_id: number;
  user_id: number;
  model: string;
  messages_examined: number;
  tool_calls: Array<{
    name: string;
    arguments: Record<string, unknown>;
    status: 'success' | 'error' | 'pending';
    error: string | null;
  }>;
  tools_attempted: number;
  tools_succeeded: number;
  summary: string;
  duration_ms: number;
  error: string | null;
}

export async function runJournalCurator(convId: number): Promise<CuratorRunResult> {
  return apiPost<CuratorRunResult>(`/api/journal/curator/run/${convId}`, {});
}

export async function listJournalMoments(params: Record<string, string | number | boolean> = {}): Promise<JournalMoment[]> {
  const qs = new URLSearchParams();
  for (const [k, v] of Object.entries(params)) qs.set(k, String(v));
  const data = await apiGet<{ moments: JournalMoment[] }>(`/api/journal/moments?${qs}`);
  return data.moments;
}

export async function updateJournalMoment(id: number, patch: Partial<JournalMoment>): Promise<JournalMoment> {
  return apiPatch<JournalMoment>(`/api/journal/moments/${id}`, patch);
}

export async function deleteJournalMoment(id: number): Promise<void> {
  await apiDelete(`/api/journal/moments/${id}`);
}

export async function geocodeAddress(address: string): Promise<{ lat: number; lon: number; display_name: string } | null> {
  try {
    const r = await apiPost<{ lat: number; lon: number; label: string }>('/api/journal/weather/geocode', { query: address });
    return { lat: r.lat, lon: r.lon, display_name: r.label };
  } catch {
    return null;
  }
}

export async function getFableMcpInfo(): Promise<{ available: boolean; filename: string | null }> {
  return apiGet('/api/fable-mcp/info');
}

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);
    }
  }
}

// ---------------------------------------------------------------------------
// Calendar events
// ---------------------------------------------------------------------------

export interface EventEntry {
  id: number;
  uid: string;
  title: string;
  start_dt: string;
  end_dt: string | null;
  all_day: boolean;
  description: string;
  location: string;
  color: string;
  recurrence: string | null;
  caldav_uid: string;
  project_id: number | null;
  user_id: number;
  created_at: string | null;
  updated_at: string | null;
}

export interface EventCreatePayload {
  title: string;
  start_dt: string;
  end_dt?: string;
  all_day?: boolean;
  description?: string;
  location?: string;
  color?: string;
  recurrence?: string;
  project_id?: number;
}

export interface EventUpdatePayload {
  title?: string;
  start_dt?: string;
  end_dt?: string;
  all_day?: boolean;
  description?: string;
  location?: string;
  color?: string;
  recurrence?: string | null;
  project_id?: number;
}

export async function listEvents(from: string, to: string): Promise<EventEntry[]> {
  return apiGet<EventEntry[]>(`/api/events?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`);
}

export async function createEvent(payload: EventCreatePayload): Promise<EventEntry> {
  return apiPost<EventEntry>('/api/events', payload);
}

export async function getEvent(id: number): Promise<EventEntry> {
  return apiGet<EventEntry>(`/api/events/${id}`);
}

export async function updateEvent(id: number, payload: EventUpdatePayload): Promise<EventEntry> {
  return apiPatch<EventEntry>(`/api/events/${id}`, payload);
}

export async function deleteEvent(id: number): Promise<void> {
  return apiDelete(`/api/events/${id}`);
}

// ─── 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}`)

// ─── News ─────────────────────────────────────────────────────────────────────

import type { NewsItem } from '@/types/news'

export interface GetNewsItemsParams {
  days?: number
  limit?: number
  offset?: number
  feed_id?: number | null
}

export function getNewsItems(params: GetNewsItemsParams = {}) {
  const p = new URLSearchParams()
  if (params.days != null) p.set('days', String(params.days))
  if (params.limit != null) p.set('limit', String(params.limit))
  if (params.offset != null) p.set('offset', String(params.offset))
  if (params.feed_id != null) p.set('feed_id', String(params.feed_id))
  return apiGet<{ items: NewsItem[]; offset: number; limit: number }>(
    `/api/briefing/news?${p}`
  )
}

// ─── Voice ────────────────────────────────────────────────────────────────────

export interface VoiceStatusResult {
  enabled: boolean
  stt: boolean
  tts: boolean
  stt_model?: string
  tts_backend?: string
}

export interface VoiceEntry {
  id: string
  label: string
  language?: string
  quality?: string
  sample_rate?: number
}

export interface VoiceLibraryEntry {
  id: string
  name: string
  language_code: string
  language_name: string
  country: string
  quality: string
  num_speakers: number
  size_bytes: number
  installed: boolean
  installed_source: 'user' | 'bundled' | null
}

export const getVoiceStatus = () => apiGet<VoiceStatusResult>('/api/voice/status')

export const getVoiceList = () =>
  apiGet<{ voices: VoiceEntry[] }>('/api/voice/voices').then(r => r.voices)

export const getVoiceLibrary = (refresh = false) =>
  apiGet<{ voices: VoiceLibraryEntry[]; count: number }>(
    refresh ? '/api/voice/voices/library?refresh=1' : '/api/voice/voices/library'
  )

export const installVoice = (voiceId: string) =>
  apiPost<{ id: string; size_bytes: number; skipped: boolean }>(
    '/api/voice/voices/install',
    { voice_id: voiceId }
  )

export const uninstallVoice = (voiceId: string) =>
  apiDelete(`/api/voice/voices/${encodeURIComponent(voiceId)}`)

export async function transcribeAudio(blob: Blob, context?: string): Promise<{ transcript: string; duration_ms: number }> {
  const form = new FormData()
  form.append('audio', blob, 'audio.webm')
  if (context) form.append('context', context)
  const res = await fetch('/api/voice/transcribe', { method: 'POST', body: form })
  if (!res.ok) {
    const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
    throw new ApiError(res.status, err)
  }
  return res.json()
}

export async function synthesiseSpeech(
  text: string,
  voice?: string,
  speed?: number,
): Promise<Blob> {
  // Only send voice/speed when explicitly provided — omitting them lets the
  // server auto-load the user's saved voice settings.
  // Voice blending was removed with the kokoro → piper migration (piper has
  // no blend equivalent); callers that previously passed `voiceBlend` should
  // pass the first voice's id as `voice` instead.
  const body: Record<string, unknown> = { text }
  if (voice !== undefined || speed !== undefined) {
    body.voice = voice ?? 'en_US-amy-medium'
    body.speed = speed ?? 1.0
  }
  const res = await fetch('/api/voice/synthesise', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
    throw new ApiError(res.status, err)
  }
  return res.blob()
}

// ── 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 }
  learned_summary: string
  observations_count: number
  observations_updated_at: string | null
}

export const getProfile = () => apiGet<UserProfile>('/api/profile')
export const updateProfile = (data: Partial<UserProfile>) =>
  apiPut<UserProfile>('/api/profile', data)
export const consolidateProfile = () =>
  apiPost<{ status: string; learned_summary: string }>('/api/profile/consolidate', {})
export const clearProfileObservations = () => apiDelete('/api/profile/observations')

export interface ProfileObservationEntry {
  date: string
  bullets: string
}

export const listProfileObservations = () =>
  apiGet<{ observations: ProfileObservationEntry[] }>('/api/profile/observations')


// ── Tasks ────────────────────────────────────────────────────────────────────

import type { Note as Task } from '../types/note'

/** Manually trigger a consolidation pass for a task. Returns the freshly-
 *  updated task with new body + consolidated_at. Bypasses the user's
 *  auto_consolidate_tasks setting. */
export const consolidateTask = (id: number) =>
  apiPost<Task>(`/api/tasks/${id}/consolidate`, {})


// ── 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`)
