// src/api-client.ts
// Typed HTTP client for all CMS transcoder API endpoints.

import * as fssync from "fs";
import * as fs from "fs/promises";
import { createReadStream, createWriteStream } from "fs";
import { Readable } from "stream";
import { pipeline } from "stream/promises";

export interface JobInfo {
  videoId: string;
  downloadUrl: string;
}

export class TranscoderApiClient {
  constructor(
    private readonly cmsUrl: string,
    private readonly secret: string
  ) {}

  private get authHeader(): Record<string, string> {
    return { Authorization: `Bearer ${this.secret}` };
  }

  /**
   * Atomically claim the next available job.
   * Returns null when the queue is empty.
   */
  async claimJob(): Promise<JobInfo | null> {
    const res = await fetch(`${this.cmsUrl}/api/transcoder/claim`, {
      method: "POST",
      headers: this.authHeader,
    });

    if (!res.ok) {
      const body = await res.text();
      throw new Error(`Claim failed: HTTP ${res.status} – ${body}`);
    }

    return res.json() as Promise<JobInfo | null>;
  }

  /**
   * Stream the source MP4 from the CMS directly to a local file path.
   * Calls onProgress with the total bytes received so far (optional).
   * Returns the total bytes downloaded.
   */
  async downloadVideo(
    downloadUrl: string,
    destPath: string,
    onProgress?: (bytesReceived: number) => void
  ): Promise<number> {
    const res = await fetch(downloadUrl, { headers: this.authHeader });

    if (!res.ok) {
      const body = await res.text();
      throw new Error(`Download failed: HTTP ${res.status} – ${body}`);
    }

    if (!res.body) {
      throw new Error("Download response had no body");
    }

    const writeStream = createWriteStream(destPath);
    let totalBytes = 0;

    const nodeReadable = Readable.fromWeb(
      res.body as unknown as import("stream/web").ReadableStream<Uint8Array>
    );

    nodeReadable.on("data", (chunk: Buffer) => {
      totalBytes += chunk.length;
      onProgress?.(totalBytes);
    });

    await pipeline(nodeReadable, writeStream);
    return totalBytes;
  }

  /**
   * Stream a ZIP archive to the CMS upload endpoint.
   * The CMS extracts the archive, validates it, and marks the video transcoded.
   */
  async uploadHls(videoId: string, zipPath: string): Promise<void> {
    const stat = await fs.stat(zipPath);
    const readStream = createReadStream(zipPath);

    // Node.js native fetch requires duplex: 'half' when body is a stream.
    const res = await (fetch as typeof fetch)(
      `${this.cmsUrl}/api/transcoder/upload/${videoId}`,
      {
        method: "POST",
        headers: {
          ...this.authHeader,
          "Content-Type": "application/zip",
          "Content-Length": stat.size.toString(),
        },
        body: Readable.toWeb(
          readStream
        ) as unknown as BodyInit,
        // @ts-expect-error – required for streaming body in Node 18+ fetch
        duplex: "half",
      }
    );

    if (!res.ok) {
      const body = await res.text();
      throw new Error(`Upload failed: HTTP ${res.status} – ${body}`);
    }
  }

  /**
   * Tell the CMS that this job failed unrecoverably.
   */
  async markFailed(videoId: string, errorMessage: string): Promise<void> {
    try {
      const res = await fetch(`${this.cmsUrl}/api/transcoder/fail/${videoId}`, {
        method: "POST",
        headers: { ...this.authHeader, "Content-Type": "application/json" },
        body: JSON.stringify({ error: errorMessage }),
      });

      if (!res.ok) {
        console.error(
          `[markFailed] HTTP ${res.status} for ${videoId}: ${await res.text()}`
        );
      }
    } catch (err) {
      // Best-effort – don't throw if the fail call itself errors.
      console.error(`[markFailed] Could not reach CMS for ${videoId}:`, err);
    }
  }
}
