/**
 * HTTP client for the imajin-identity face-embedding service.
 *
 * The service is reached through the Model Boss coordinator (see
 * shared/model-boss.ts for the same pattern with embeddings). The coordinator
 * routes service-name + path tuples to the right backing process and manages
 * VRAM scheduling.
 *
 * Endpoints used:
 *   POST /imajin-identity/embed/from-url
 *     → { success, faces: [{bbox: [x,y,w,h], confidence, embedding: number[]}] }
 *   POST /imajin-identity/identities/from-urls
 *     → { success, identity_id, namespace, image_count }
 *   POST /imajin-identity/identities/search-in-namespace
 *     → { identity_id, namespace, similarity, confidence, face_detected }
 *
 * The service fetches images via HTTP. It must be able to reach our MinIO
 * bucket — pass an absolute URL the service can resolve.
 */

import { loadConfig } from '@/app/config';

export interface FaceEmbedding {
  /** [x, y, w, h] in source-image pixels. */
  readonly bbox: readonly [number, number, number, number];
  readonly confidence: number;
  readonly embedding: readonly number[];
}

export interface EmbedFromUrlResponse {
  readonly success: boolean;
  readonly faces: readonly FaceEmbedding[];
  readonly message?: string | null;
}

export interface IdentityBuildResponse {
  readonly success: boolean;
  readonly identity_id: string;
  readonly namespace: string;
  readonly image_count: number;
  readonly message?: string | null;
}

export interface NamespacedSearchResponse {
  readonly identity_id: string;
  readonly namespace: string;
  readonly similarity: number;
  readonly confidence: 'high' | 'medium' | 'low';
  readonly face_detected: boolean;
  readonly message?: string | null;
}

const DEFAULT_TIMEOUT_MS = 30_000;

async function postJson<TReq, TRes>(path: string, body: TReq, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<TRes> {
  const { IMAJIN_IDENTITY_URL } = loadConfig();
  const url = `${IMAJIN_IDENTITY_URL.replace(/\/$/, '')}${path}`;
  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Client-Id': 'mac-sync-server' },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(timeoutMs),
    });
    if (!res.ok) {
      const text = await res.text().catch(() => '');
      throw new Error(`imajin-identity ${path} -> ${res.status}: ${text.slice(0, 200)} @ ${url}`);
    }
    return (await res.json()) as TRes;
  } catch (err) {
    if (err instanceof Error) throw err;
    throw new Error(`imajin-identity ${path} failed: ${String(err)}`);
  }
}

export async function embedFromUrl(params: { imageUrl: string; extractAll?: boolean }): Promise<EmbedFromUrlResponse> {
  return postJson<{ image_url: string; extract_all: boolean }, EmbedFromUrlResponse>('/embed/from-url', {
    image_url: params.imageUrl,
    extract_all: params.extractAll ?? true,
  });
}

export async function buildIdentityFromUrls(params: {
  namespace: string;
  identityId: string;
  displayName: string;
  imageUrls: string[];
  upsert?: boolean;
}): Promise<IdentityBuildResponse> {
  return postJson<
    {
      namespace: string;
      identity_id: string;
      display_name: string;
      image_urls: string[];
      upsert: boolean;
    },
    IdentityBuildResponse
  >('/identities/from-urls', {
    namespace: params.namespace,
    identity_id: params.identityId,
    display_name: params.displayName,
    image_urls: params.imageUrls,
    upsert: params.upsert ?? false,
  });
}

export async function searchInNamespace(params: {
  namespace: string;
  identityId: string;
  imageUrl: string;
  minSimilarity?: number;
}): Promise<NamespacedSearchResponse> {
  return postJson<
    {
      namespace: string;
      identity_id: string;
      image_url: string;
      min_similarity: number;
    },
    NamespacedSearchResponse
  >('/identities/search-in-namespace', {
    namespace: params.namespace,
    identity_id: params.identityId,
    image_url: params.imageUrl,
    min_similarity: params.minSimilarity ?? 0.35,
  });
}
