/** HuggingFace Hub client — search, browse, download models */

import { Effect } from "effect"
import * as Log from "@saeeol/core/util/log"
import { iife } from "@/util/iife"
import type { HFModelSearch, HFModelInfo, HFSibling, ModelArtifact, Quantization, ModelFormat, RAGAsset, RAGAssetType } from "./types"

const log = Log.create({ service: "local/hub" })
const API = "https://huggingface.co/api"

/** Search HuggingFace Hub for models */
export async function search(query: string, opts?: { limit?: number; tags?: string[] }): Promise<HFModelSearch[]> {
  const params = new URLSearchParams({
    search: query,
    limit: String(opts?.limit ?? 20),
    ...(opts?.tags?.length ? { tags: opts.tags.join(",") } : {}),
  })

  const res = await fetch(`${API}/models?${params}`)
  if (!res.ok) throw new Error(`HF search failed: ${res.status} ${res.statusText}`)

  const items = (await res.json()) as any[]
  return items.map((item) => ({
    id: item.id ?? item.modelId ?? "",
    name: item.id ?? "",
    author: item.author ?? "",
    downloads: item.downloads ?? 0,
    likes: item.likes ?? 0,
    tags: item.tags ?? [],
    pipelineTag: item.pipeline_tag ?? undefined,
    libraryName: item.library_name ?? undefined,
  }))
}

/** Get detailed model info from HF Hub */
export async function info(repo: string): Promise<HFModelInfo> {
  const res = await fetch(`${API}/models/${repo}`)
  if (!res.ok) throw new Error(`HF model info failed: ${res.status} ${res.statusText}`)

  const data = (await res.json()) as any
  return {
    id: data.id ?? data.modelId ?? repo,
    modelId: data.modelId ?? repo,
    sha: data.sha ?? "",
    siblings: (data.siblings ?? []) as HFSibling[],
    tags: data.tags ?? [],
    downloads: data.downloads ?? 0,
    likes: data.likes ?? 0,
    private: data.private ?? false,
  }
}

/** List GGUF files in a repo */
export function listGGUF(siblings: HFSibling[]): string[] {
  return siblings
    .map((s) => s.rfilename)
    .filter((f) => f.endsWith(".gguf"))
}

/** Parse quantization from GGUF filename */
export function parseQuant(filename: string): Quantization | undefined {
  const lower = filename.toLowerCase()
  const quants: Quantization[] = [
    "q2_k", "q3_k_s", "q3_k_m", "q3_k_l",
    "q4_0", "q4_1", "q4_k_s", "q4_k_m",
    "q5_0", "q5_1", "q5_k_s", "q5_k_m",
    "q6_k", "q8_0", "fp16", "bf16", "fp32",
  ]
  for (const q of quants) {
    if (lower.includes(q)) return q
  }
  return undefined
}

/** Resolve download URL for a file in a repo */
export function downloadURL(repo: string, filename: string): string {
  return `https://huggingface.co/${repo}/resolve/main/${filename}`
}

/** Build a ModelArtifact from repo + sibling info */
export function buildArtifact(
  repo: string,
  filename: string,
  sizeBytes: number,
  sha256?: string,
): ModelArtifact {
  const q = parseQuant(filename)
  const ext = filename.split(".").pop() ?? ""
  const formatMap: Record<string, ModelFormat> = {
    gguf: "gguf",
    safetensors: "safetensors",
    bin: "pytorch",
    pt: "pytorch",
    onnx: "onnx",
  }

  return {
    id: `${repo}/${filename}`,
    repo,
    filename,
    format: formatMap[ext] ?? "pytorch",
    quantization: q ?? "fp16",
    sizeBytes,
    sha256,
  }
}

/** Download a file from HF Hub with progress callback */
export async function download(
  repo: string,
  filename: string,
  destPath: string,
  opts?: { onProgress?: (downloaded: number, total: number) => void; signal?: AbortSignal },
): Promise<void> {
  const url = downloadURL(repo, filename)

  const res = await fetch(url, { signal: opts?.signal })
  if (!res.ok) throw new Error(`HF download failed: ${res.status} ${res.statusText}`)
  if (!res.body) throw new Error("No response body")

  const total = Number(res.headers.get("content-length") ?? "0")
  const file = Bun.file(destPath)
  const writer = file.writer()

  let downloaded = 0
  const reader = res.body.getReader()

  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      writer.write(value)
      downloaded += value.length
      opts?.onProgress?.(downloaded, total)
    }
  } finally {
    writer.end()
  }

  log.info("downloaded", { repo, filename, sizeMB: Math.round(downloaded / 1024 / 1024) })
}

/** Search for embedding/reranker models */
export async function searchRAG(
  type: RAGAssetType,
  query?: string,
  limit?: number,
): Promise<RAGAsset[]> {
  const tagMap: Record<RAGAssetType, string> = {
    embedding: "sentence-transformers",
    reranker: "reranker",
    vectordb: "vector",
  }

  const params = new URLSearchParams({
    limit: String(limit ?? 20),
    tags: tagMap[type],
    ...(query ? { search: query } : {}),
  })

  const res = await fetch(`${API}/models?${params}`)
  if (!res.ok) return []

  const items = (await res.json()) as any[]
  return items.map((item) => ({
    id: item.id ?? "",
    name: item.id ?? "",
    type,
    repo: item.id ?? "",
    format: "safetensors" as ModelFormat,
    sizeBytes: 0,
    dimensions: undefined,
  }))
}
