import type { IngestInteractionRequest } from '@insignia/iios-contracts';
import type { Message, InboxItem, InboxState, Ticket, TicketState, CallbackRequest, RouteBinding, RouteDecision, AiArtifact, AiJobResult, Meeting, MeetingActionItem, ThreadSummary, DiscoveredThread, SavedItem, LoginResult } from './types';

export interface RestConfig {
  serviceUrl: string;
  token?: string;
  /**
   * Extra headers for every request — e.g. `x-context-attestation` (the July 12 trust proof).
   * Pass a FUNCTION to mint fresh headers per request: a context attestation carries a single-use
   * nonce, so a static header would be replay-rejected on the 2nd call. The function is invoked
   * once per request.
   */
  headers?: Record<string, string> | (() => Record<string, string>);
}

/** REST/polling client for kernel reads and the native-send fallback. */
export class RestClient {
  constructor(private readonly config: RestConfig) {}

  private url(path: string): string {
    return `${this.config.serviceUrl.replace(/\/$/, '')}${path}`;
  }

  private headers(extra: Record<string, string> = {}): Record<string, string> {
    const custom = typeof this.config.headers === 'function' ? this.config.headers() : this.config.headers;
    const h: Record<string, string> = { 'content-type': 'application/json', ...custom, ...extra };
    if (this.config.token) h.authorization = `Bearer ${this.config.token}`;
    return h;
  }

  async getThreadMessages(threadId: string): Promise<{ threadId: string; messages: unknown[] }> {
    const r = await fetch(this.url(`/v1/threads/${threadId}/messages`), { headers: this.headers() });
    if (!r.ok) throw new Error(`getThreadMessages ${r.status}`);
    return (await r.json()) as { threadId: string; messages: unknown[] };
  }

  async sendMessage(
    threadId: string,
    content: string,
    opts?: { contentRef?: string; idempotencyKey?: string },
  ): Promise<Message> {
    const r = await fetch(this.url(`/v1/threads/${threadId}/messages`), {
      method: 'POST',
      headers: this.headers(opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {}),
      body: JSON.stringify({ content, contentRef: opts?.contentRef }),
    });
    if (!r.ok) throw new Error(`sendMessage ${r.status}`);
    return (await r.json()) as Message;
  }

  async listInboxItems(state?: InboxState): Promise<InboxItem[]> {
    const q = state ? `?state=${encodeURIComponent(state)}` : '';
    const r = await fetch(this.url(`/v1/inbox/items${q}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`listInboxItems ${r.status}`);
    return (await r.json()) as InboxItem[];
  }

  async patchInboxItem(id: string, body: { state: InboxState; reason?: string }): Promise<InboxItem> {
    const r = await fetch(this.url(`/v1/inbox/items/${id}`), {
      method: 'PATCH',
      headers: this.headers(),
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(`patchInboxItem ${r.status}`);
    return (await r.json()) as InboxItem;
  }

  // ─── auth + threads (app surface) ─────────────────────────────
  /** Dev IdP login (POST /v1/dev/login). A real IdP issues the same JWT — this is the swap point. */
  async login(username: string, password: string): Promise<LoginResult> {
    const r = await fetch(this.url('/v1/dev/login'), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ username, password }),
    });
    if (r.status === 401) throw new Error('invalid username or password');
    if (!r.ok) throw new Error(`login failed (${r.status}) — is the service running with IIOS_DEV_TOKENS=1?`);
    return (await r.json()) as LoginResult;
  }

  /** Dev directory: known usernames. A real app validates against its user store / MDM. */
  async listUsers(): Promise<string[]> {
    const r = await fetch(this.url('/v1/dev/users'), { headers: this.headers() });
    if (!r.ok) return [];
    return ((await r.json()) as { users: string[] }).users;
  }

  /**
   * Server-authoritative conversation list (works cross-device, shows unread + members).
   * `filter.metadata` narrows to threads whose opaque attribute bag matches every key/value.
   */
  async listThreads(filter?: { metadata?: Record<string, string> }): Promise<ThreadSummary[]> {
    const qs = filter?.metadata
      ? '?' + Object.entries(filter.metadata).map(([k, v]) => `metadata[${encodeURIComponent(k)}]=${encodeURIComponent(v)}`).join('&')
      : '';
    const r = await fetch(this.url(`/v1/threads${qs}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`listThreads ${r.status}`);
    return (await r.json()) as ThreadSummary[];
  }

  /** Create a thread with generic app attributes (membership/creator role/subject + an opaque metadata bag). */
  async createThread(opts: { membership?: string; creatorRole?: string; subject?: string; metadata?: Record<string, unknown> }): Promise<{ threadId: string }> {
    return this.post<{ threadId: string }>('/v1/threads', opts);
  }

  /**
   * Discover threads across your scope matching an opaque metadata filter (e.g. browse public
   * channels: `{ membership: 'channel', visibility: 'public' }`). Returns ones you have NOT joined
   * too, each flagged `joined`.
   */
  async discoverThreads(filter?: { metadata?: Record<string, string> }): Promise<DiscoveredThread[]> {
    const qs = filter?.metadata
      ? '?' + Object.entries(filter.metadata).map(([k, v]) => `metadata[${encodeURIComponent(k)}]=${encodeURIComponent(v)}`).join('&')
      : '';
    const r = await fetch(this.url(`/v1/threads/discover${qs}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`discoverThreads ${r.status}`);
    return (await r.json()) as DiscoveredThread[];
  }

  /** Self-leave a thread (e.g. leave a channel). */
  async leaveThread(threadId: string): Promise<{ threadId: string; participantCount: number }> {
    const r = await fetch(this.url(`/v1/threads/${threadId}/me`), { method: 'DELETE', headers: this.headers() });
    if (!r.ok) throw new Error(`leaveThread ${r.status}`);
    return (await r.json()) as { threadId: string; participantCount: number };
  }

  /** My saved messages (personal bookmarks), newest first, with thread context. */
  async listSaved(): Promise<SavedItem[]> {
    const r = await fetch(this.url('/v1/threads/my-annotations?type=save'), { headers: this.headers() });
    if (!r.ok) throw new Error(`listSaved ${r.status}`);
    return (await r.json()) as SavedItem[];
  }

  /** Governed add-participant. A policy 403 (e.g. DM cap) surfaces its reason. */
  async addParticipant(threadId: string, userId: string): Promise<void> {
    const r = await fetch(this.url(`/v1/threads/${threadId}/participants`), {
      method: 'POST',
      headers: this.headers(),
      body: JSON.stringify({ userId }),
    });
    if (r.ok) return;
    const body = (await r.json().catch(() => ({}))) as { message?: string };
    throw new Error(body.message ?? `could not add member (${r.status})`);
  }

  // ─── support ──────────────────────────────────────────────────
  async createTicket(body: { subject: string; priority?: string; threadId?: string; metadata?: Record<string, unknown> }): Promise<Ticket> {
    return this.post<Ticket>('/v1/support/tickets', body);
  }
  async escalate(threadId: string, subject?: string, metadata?: Record<string, unknown>): Promise<Ticket> {
    return this.post<Ticket>('/v1/support/escalate', { threadId, subject, metadata });
  }
  /** Manually assign a ticket to a specific user (generic assignment override). */
  async assignTicket(id: string, userId: string): Promise<Ticket> {
    return this.post<Ticket>(`/v1/support/tickets/${id}/assignee`, { userId });
  }
  async listTickets(scope: 'mine' | 'assigned' = 'mine'): Promise<Ticket[]> {
    const r = await fetch(this.url(`/v1/support/tickets?scope=${scope}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`listTickets ${r.status}`);
    return (await r.json()) as Ticket[];
  }
  async patchTicket(id: string, state: TicketState): Promise<Ticket> {
    const r = await fetch(this.url(`/v1/support/tickets/${id}`), {
      method: 'PATCH',
      headers: this.headers(),
      body: JSON.stringify({ state }),
    });
    if (!r.ok) throw new Error(`patchTicket ${r.status}`);
    return (await r.json()) as Ticket;
  }
  async requestCallback(body: { preferChannel: string; preferredTime?: string; notes?: string; ticketId?: string }): Promise<CallbackRequest> {
    return this.post<CallbackRequest>('/v1/support/callbacks', body);
  }
  async createQueue(name: string): Promise<{ id: string }> {
    return this.post<{ id: string }>('/v1/support/queues', { name });
  }
  async joinQueue(queueId: string): Promise<unknown> {
    return this.post('/v1/support/queues/' + queueId + '/members', {});
  }
  async joinDefaultQueue(): Promise<unknown> {
    return this.post('/v1/support/agents/me/join', {});
  }
  async setAvailability(state: string): Promise<unknown> {
    const r = await fetch(this.url('/v1/support/members/me/availability'), {
      method: 'PATCH',
      headers: this.headers(),
      body: JSON.stringify({ state }),
    });
    if (!r.ok) throw new Error(`setAvailability ${r.status}`);
    return r.json();
  }

  // ─── routing (P6) ─────────────────────────────────────────────
  async createBinding(input: Partial<RouteBinding>): Promise<RouteBinding> {
    return this.post<RouteBinding>('/v1/routes/bindings', input);
  }
  async listBindings(): Promise<RouteBinding[]> {
    const r = await fetch(this.url('/v1/routes/bindings'), { headers: this.headers() });
    if (!r.ok) throw new Error(`listBindings ${r.status}`);
    return (await r.json()) as RouteBinding[];
  }
  async simulateRoute(input: { interactionId: string; originChannelType: string; originRef?: string }): Promise<{ simulationId: string; decisions: RouteDecision[] }> {
    return this.post('/v1/routes/simulate', input);
  }
  async listRouteDecisions(state?: string): Promise<RouteDecision[]> {
    const r = await fetch(this.url(`/v1/routes/decisions${state ? `?state=${state}` : ''}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`listRouteDecisions ${r.status}`);
    return (await r.json()) as RouteDecision[];
  }
  async approveDecision(id: string): Promise<RouteDecision> {
    return this.post<RouteDecision>(`/v1/routes/decisions/${id}/approve`, {});
  }
  async denyDecision(id: string): Promise<RouteDecision> {
    return this.post<RouteDecision>(`/v1/routes/decisions/${id}/deny`, {});
  }

  // ─── AI proposal layer (P7) ───────────────────────────────────
  async runAiJob(input: { interactionId: string; jobType: 'CLASSIFY' | 'SUMMARIZE' | 'EXTRACT' }): Promise<AiJobResult> {
    return this.post<AiJobResult>('/v1/ai/jobs', input);
  }
  async listArtifacts(opts?: { interactionId?: string; status?: string }): Promise<AiArtifact[]> {
    const q = new URLSearchParams();
    if (opts?.interactionId) q.set('interactionId', opts.interactionId);
    if (opts?.status) q.set('status', opts.status);
    const qs = q.toString();
    const r = await fetch(this.url(`/v1/ai/artifacts${qs ? `?${qs}` : ''}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`listArtifacts ${r.status}`);
    return (await r.json()) as AiArtifact[];
  }
  async getArtifact(id: string): Promise<AiArtifact> {
    const r = await fetch(this.url(`/v1/ai/artifacts/${id}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`getArtifact ${r.status}`);
    return (await r.json()) as AiArtifact;
  }
  async acceptArtifact(id: string): Promise<AiArtifact> {
    return this.post<AiArtifact>(`/v1/ai/artifacts/${id}/accept`, {});
  }
  async rejectArtifact(id: string): Promise<AiArtifact> {
    return this.post<AiArtifact>(`/v1/ai/artifacts/${id}/reject`, {});
  }

  // ─── Calendar & Meeting (P8) ──────────────────────────────────
  async scheduleMeeting(input: {
    meetingType: string;
    title: string;
    startAt: string;
    endAt?: string;
    timezone?: string;
    attendees?: { userId: string; displayName?: string; role?: string; visibility?: string }[];
  }): Promise<Meeting> {
    return this.post<Meeting>('/v1/calendar/meetings', input);
  }
  async requestMeeting(input: { requestedWindow: Record<string, unknown>; meetingType?: string }, opts?: { fromCallback?: string; fromClaim?: string }): Promise<unknown> {
    const q = new URLSearchParams();
    if (opts?.fromCallback) q.set('fromCallback', opts.fromCallback);
    if (opts?.fromClaim) q.set('fromClaim', opts.fromClaim);
    const qs = q.toString();
    return this.post(`/v1/calendar/requests${qs ? `?${qs}` : ''}`, input);
  }
  async listMeetings(): Promise<Meeting[]> {
    const r = await fetch(this.url('/v1/calendar/meetings'), { headers: this.headers() });
    if (!r.ok) throw new Error(`listMeetings ${r.status}`);
    return (await r.json()) as Meeting[];
  }
  async getMeeting(id: string): Promise<Meeting> {
    const r = await fetch(this.url(`/v1/calendar/meetings/${id}`), { headers: this.headers() });
    if (!r.ok) throw new Error(`getMeeting ${r.status}`);
    return (await r.json()) as Meeting;
  }
  async setConsent(meetingId: string, actorRefId: string, status: string): Promise<unknown> {
    return this.post(`/v1/calendar/meetings/${meetingId}/consent`, { actorRefId, status });
  }
  async generateTranscript(meetingId: string): Promise<unknown> {
    return this.post(`/v1/calendar/meetings/${meetingId}/transcript`, {});
  }
  async summarizeMeeting(meetingId: string): Promise<Meeting> {
    return this.post<Meeting>(`/v1/calendar/meetings/${meetingId}/summarize`, {});
  }
  async listActionItems(meetingId: string): Promise<MeetingActionItem[]> {
    const r = await fetch(this.url(`/v1/calendar/meetings/${meetingId}/action-items`), { headers: this.headers() });
    if (!r.ok) throw new Error(`listActionItems ${r.status}`);
    return (await r.json()) as MeetingActionItem[];
  }
  async connectCalendarProvider(): Promise<{ id: string }> {
    return this.post<{ id: string }>('/v1/calendar/providers', {});
  }
  async syncCalendarProvider(id: string): Promise<{ pulled: number; cursor: string }> {
    return this.post(`/v1/calendar/providers/${id}/sync`, {});
  }

  private async post<T>(path: string, body: unknown): Promise<T> {
    const r = await fetch(this.url(path), { method: 'POST', headers: this.headers(), body: JSON.stringify(body) });
    if (!r.ok) throw new Error(`POST ${path} ${r.status}`);
    return (await r.json()) as T;
  }

  async ingest(body: IngestInteractionRequest, idempotencyKey: string): Promise<unknown> {
    const r = await fetch(this.url('/v1/interactions/ingest'), {
      method: 'POST',
      headers: this.headers({ 'idempotency-key': idempotencyKey }),
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(`ingest ${r.status}`);
    return r.json();
  }
}
