import { createWriteStream } from 'node:fs'
import { mkdir, rename, stat, unlink } from 'node:fs/promises'
import { join } from 'node:path'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { config } from '../config.js'
import { catalogService } from './catalog_service.js'
import type { DownloadResource, ResourceType } from '../data/catalog.js'

export type DownloadStatus =
  | 'queued'
  | 'downloading'
  | 'completed'
  | 'failed'
  | 'canceled'

export interface DownloadTask {
  id: string
  resourceId: string
  type: ResourceType
  filename: string
  status: DownloadStatus
  totalBytes: number
  receivedBytes: number
  startedAt: number
  finishedAt?: number
  error?: string
}

export interface PublicDownloadTask extends DownloadTask {
  /** Užbaigtumas procentais (0–100). */
  percent: number
}

export interface CatalogItem {
  resource: DownloadResource
  downloaded: boolean
  task: PublicDownloadTask | null
}

/**
 * Realus atsisiuntimų variklis. Srautu parsiunčia viešus išteklius
 * (ZIM wiki, PMTiles žemėlapius) į lokalią saugyklą, seka progresą ir
 * leidžia atšaukti. Be Redis/BullMQ — užtenka vieno proceso.
 */
export class DownloadService {
  private tasks = new Map<string, DownloadTask>()
  private controllers = new Map<string, AbortController>()
  private counter = 0

  private pathFor(resource: DownloadResource): string {
    return join(config.storagePath, resource.type, resource.filename)
  }

  private percent(t: DownloadTask): number {
    if (t.status === 'completed') return 100
    if (!t.totalBytes) return 0
    return Math.min(99, Math.round((t.receivedBytes / t.totalBytes) * 100))
  }

  private toPublic(t: DownloadTask): PublicDownloadTask {
    return { ...t, percent: this.percent(t) }
  }

  private latestTask(resourceId: string): DownloadTask | undefined {
    let found: DownloadTask | undefined
    for (const t of this.tasks.values()) {
      if (t.resourceId === resourceId) found = t
    }
    return found
  }

  async isDownloaded(resource: DownloadResource): Promise<boolean> {
    try {
      const s = await stat(this.pathFor(resource))
      return s.size > 0
    } catch {
      return false
    }
  }

  /** Ar atsisiųstas bent vienas nurodyto tipo išteklius (wiki / map). */
  async hasDownloadedType(type: ResourceType): Promise<boolean> {
    for (const resource of await catalogService.list(type)) {
      if (await this.isDownloaded(resource)) return true
    }
    return false
  }

  /** Katalogas su atsisiuntimo būsena (UI). */
  async catalog(type?: ResourceType): Promise<CatalogItem[]> {
    const items: CatalogItem[] = []
    for (const resource of await catalogService.list(type)) {
      const task = this.latestTask(resource.id)
      const downloaded =
        task?.status === 'completed' || (await this.isDownloaded(resource))
      items.push({
        resource,
        downloaded,
        task: task ? this.toPublic(task) : null,
      })
    }
    return items
  }

  list(): PublicDownloadTask[] {
    return [...this.tasks.values()].map((t) => this.toPublic(t))
  }

  get(id: string): PublicDownloadTask | undefined {
    const t = this.tasks.get(id)
    return t ? this.toPublic(t) : undefined
  }

  /** Pradeda (arba grąžina jau vykstantį / užbaigtą) atsisiuntimą. */
  async start(resourceId: string): Promise<PublicDownloadTask> {
    const resource = await catalogService.get(resourceId)
    if (!resource) throw new Error('Išteklius nerastas / resource not found')

    // Jau atsisiųsta — nedubliuojam.
    if (await this.isDownloaded(resource)) {
      const t = this.makeTask(resource)
      t.status = 'completed'
      t.totalBytes = resource.sizeBytes
      t.receivedBytes = resource.sizeBytes
      t.finishedAt = Date.now()
      this.tasks.set(t.id, t)
      return this.toPublic(t)
    }

    // Jau vyksta — grąžinam tą patį.
    const active = [...this.tasks.values()].find(
      (t) =>
        t.resourceId === resourceId &&
        (t.status === 'downloading' || t.status === 'queued')
    )
    if (active) return this.toPublic(active)

    const task = this.makeTask(resource)
    this.tasks.set(task.id, task)
    // Paleidžiam fone — neblokuojam HTTP atsako.
    void this.run(task, resource)
    return this.toPublic(task)
  }

  cancel(id: string): boolean {
    const ctrl = this.controllers.get(id)
    if (!ctrl) return false
    ctrl.abort()
    return true
  }

  private makeTask(resource: DownloadResource): DownloadTask {
    return {
      id: `dl_${Date.now()}_${++this.counter}`,
      resourceId: resource.id,
      type: resource.type,
      filename: resource.filename,
      status: 'queued',
      totalBytes: resource.sizeBytes,
      receivedBytes: 0,
      startedAt: Date.now(),
    }
  }

  private async run(task: DownloadTask, resource: DownloadResource): Promise<void> {
    const dir = join(config.storagePath, resource.type)
    const finalPath = this.pathFor(resource)
    const partPath = finalPath + '.part'
    const controller = new AbortController()
    this.controllers.set(task.id, controller)

    try {
      await mkdir(dir, { recursive: true })
      task.status = 'downloading'

      const res = await fetch(resource.url, {
        signal: controller.signal,
        redirect: 'follow',
      })
      if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`)

      const len = Number(res.headers.get('content-length'))
      if (len > 0) task.totalBytes = len

      const nodeStream = Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0])
      nodeStream.on('data', (chunk: Buffer) => {
        task.receivedBytes += chunk.length
      })

      await pipeline(nodeStream, createWriteStream(partPath))
      await rename(partPath, finalPath)

      task.status = 'completed'
      task.finishedAt = Date.now()
    } catch (err) {
      task.status = controller.signal.aborted ? 'canceled' : 'failed'
      if (task.status === 'failed') {
        task.error = err instanceof Error ? err.message : String(err)
      }
      task.finishedAt = Date.now()
      await unlink(partPath).catch(() => {})
    } finally {
      this.controllers.delete(task.id)
    }
  }
}

export const downloadService = new DownloadService()
