/**
 * Eye ingester — public entry point.
 *
 * Orchestrates: crawl Awwwards tier galleries → filter to award winners only
 * → capture each site (static + motion + keyframes) → derive motion
 * descriptors → persist to ~/.sandcode/eye/awwwards/<slug>/.
 *
 * Called either from SANDCODE's scheduled task surface (weekly run) or
 * from a manual CLI entrypoint for testing.
 */

import { captureSite } from './capturer.js'
import { fetchGalleryPage, discoverPageCount } from './crawler.js'
import { deriveDescriptors } from './descriptors.js'
import {
  buildInitialRecord,
  getAwwwardsSiteDir,
  getSiteAgeDays,
  writeMotionDescriptors,
  writeSiteRecord,
} from './storage.js'
import {
  AWARD_TIERS,
  DEFAULT_INGEST_CONFIG,
  type AwardTier,
  type AwwwardsSite,
  type IngestRunConfig,
  type SiteRecord,
} from './types.js'

export interface IngestProgressEvent {
  phase: 'discover' | 'capture' | 'persist' | 'skip' | 'error'
  tier?: AwardTier
  page?: number
  slug?: string
  message: string
}

export interface IngestSummary {
  tiersScanned: AwardTier[]
  sitesDiscovered: number
  sitesCaptured: number
  sitesSkipped: number
  sitesFailed: number
  durationMs: number
}

export type IngestProgressHandler = (event: IngestProgressEvent) => void

export async function runIngest(
  configOverrides: Partial<IngestRunConfig> = {},
  onProgress?: IngestProgressHandler,
): Promise<IngestSummary> {
  const config: IngestRunConfig = { ...DEFAULT_INGEST_CONFIG, ...configOverrides }
  const startedAt = Date.now()

  const discovered: AwwwardsSite[] = []

  for (const tier of config.tiers) {
    const pageCap =
      config.maxPagesPerTier === 0 ? await discoverPageCount(tier, config) : config.maxPagesPerTier

    for (let page = 1; page <= pageCap; page++) {
      emit(onProgress, {
        phase: 'discover',
        tier,
        page,
        message: `fetching ${tier} page ${page}/${pageCap}`,
      })

      try {
        const sites = await fetchGalleryPage(tier, page, config)
        if (sites.length === 0) break
        discovered.push(...sites)
      } catch (error: unknown) {
        emit(onProgress, {
          phase: 'error',
          tier,
          page,
          message: `gallery-fetch failed: ${errorMessage(error)}`,
        })
        break
      }

      await sleep(config.requestDelayMs)
    }
  }

  let sitesCaptured = 0
  let sitesSkipped = 0
  let sitesFailed = 0

  const unique = dedupeBySlug(discovered)

  // Concurrency-bounded worker pool. Keeps memory modest and Awwwards polite.
  const workers: Promise<void>[] = []
  const queue = [...unique]

  const worker = async (): Promise<void> => {
    while (queue.length > 0) {
      const site = queue.shift()
      if (!site) return

      const ageDays = await getSiteAgeDays(site.slug)
      if (ageDays !== null && ageDays < config.reingestAfterDays) {
        sitesSkipped++
        emit(onProgress, {
          phase: 'skip',
          slug: site.slug,
          message: `already captured ${Math.round(ageDays)}d ago`,
        })
        continue
      }

      try {
        const result = await captureSite(site, config)

        const descriptors = deriveDescriptors({
          durationMs: result.durationMs,
          frames: result.frames,
          hover: result.hover,
          canvases: result.canvases,
        })

        const record: SiteRecord = {
          ...buildInitialRecord(site),
          captureStatus: result.errors.length === 0 ? 'ok' : 'partial',
          captureErrors: result.errors,
          files: {
            static: result.staticFile ? relativize(result.staticFile, site.slug) : undefined,
            motion: result.motionFile ? relativize(result.motionFile, site.slug) : undefined,
            keyframes: result.keyframeFiles.map((f) => relativize(f, site.slug)),
          },
        }

        await writeSiteRecord(record)
        await writeMotionDescriptors(site.slug, descriptors)

        sitesCaptured++
        emit(onProgress, {
          phase: 'persist',
          slug: site.slug,
          message: `captured ${site.tier}: ${site.title}`,
        })
      } catch (error: unknown) {
        sitesFailed++
        emit(onProgress, {
          phase: 'error',
          slug: site.slug,
          message: `capture-failed: ${errorMessage(error)}`,
        })
      }

      await sleep(config.requestDelayMs)
    }
  }

  for (let i = 0; i < config.concurrency; i++) {
    workers.push(worker())
  }
  await Promise.all(workers)

  return {
    tiersScanned: [...config.tiers],
    sitesDiscovered: unique.length,
    sitesCaptured,
    sitesSkipped,
    sitesFailed,
    durationMs: Date.now() - startedAt,
  }
}

export { AWARD_TIERS }
export { captureSite, deriveDescriptors, fetchGalleryPage, discoverPageCount }
export type { AwardTier, AwwwardsSite, IngestRunConfig }

function dedupeBySlug(sites: readonly AwwwardsSite[]): AwwwardsSite[] {
  // When a site won multiple tiers (e.g., SOTD → SOTM → SOTY over time),
  // we keep only the highest-weight record so the library isn't inflated
  // by duplicates with conflicting tier data.
  const byPriority: Record<AwardTier, number> = { SOTY: 3, SOTM: 2, SOTD: 1 }
  const best = new Map<string, AwwwardsSite>()
  for (const s of sites) {
    const existing = best.get(s.slug)
    if (!existing || byPriority[s.tier] > byPriority[existing.tier]) {
      best.set(s.slug, s)
    }
  }
  return [...best.values()]
}

function relativize(absolutePath: string, slug: string): string {
  const marker = getAwwwardsSiteDir(slug)
  return absolutePath.startsWith(marker)
    ? absolutePath
        .slice(marker.length + 1)
        .split('\\')
        .join('/')
    : absolutePath
}

function emit(handler: IngestProgressHandler | undefined, event: IngestProgressEvent): void {
  if (handler) handler(event)
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

function errorMessage(error: unknown): string {
  if (error instanceof Error) return error.message
  return String(error)
}
