import { GlobalOptions } from "../../types.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import * as log from "../../core/log.ts";
import JSZip from "jszip";
import { extract } from "tar-stream";
import { Readable } from "node:stream";
import { Workspace } from "../workspace/workspace.ts";
import { getHeaders } from "../../utils/utils.ts";
import { detectAuthGatewayChallenge } from "../../utils/http_guards.ts";

/**
 * Adapter that wraps tar entries in a JSZip-compatible interface
 * so ZipFSElement in sync.ts can consume it without changes.
 */
class TarAsZip {
  files: Record<string, { dir: boolean; name: string; async(type: "text"): Promise<string> }> = {};

  constructor(entries: Map<string, { content: string; isDir: boolean }>) {
    for (const [name, entry] of entries) {
      const content = entry.content;
      this.files[name] = {
        dir: entry.isDir,
        name,
        async(_type: "text") {
          return content;
        },
      };
    }
  }

  /** Return a filtered view containing only entries under the given prefix, with relative paths. */
  folder(prefix: string): TarAsZip | null {
    const normalized = prefix.endsWith("/") ? prefix : prefix + "/";
    const sub = new TarAsZip(new Map());
    for (const [name, file] of Object.entries(this.files)) {
      if (name.startsWith(normalized)) {
        const relative = name.slice(normalized.length);
        if (relative) {
          sub.files[relative] = { ...file, name: relative };
        }
      }
    }
    return Object.keys(sub.files).length > 0 ? sub : null;
  }
}

async function parseTarResponse(response: Response): Promise<TarAsZip> {
  const buffer = Buffer.from(await response.arrayBuffer());
  const entries = new Map<string, { content: string; isDir: boolean }>();
  const ex = extract();

  return new Promise((resolve, reject) => {
    ex.on("entry", (header, stream, next) => {
      const chunks: Buffer[] = [];
      stream.on("data", (chunk: Buffer) => chunks.push(chunk));
      stream.on("end", () => {
        entries.set(header.name, {
          content: Buffer.concat(chunks).toString("utf-8"),
          isDir: header.type === "directory",
        });
        next();
      });
      stream.on("error", reject);
      stream.resume();
    });
    ex.on("finish", () => resolve(new TarAsZip(entries)));
    ex.on("error", reject);
    Readable.from(buffer).pipe(ex);
  });
}

export async function downloadZip(
  workspace: Workspace,
  plainSecrets: boolean | undefined,
  skipVariables?: boolean,
  skipResources?: boolean,
  skipResourceTypes?: boolean,
  skipSecrets?: boolean,
  includeSchedules?: boolean,
  includeTriggers?: boolean,
  includeUsers?: boolean,
  includeGroups?: boolean,
  includeSettings?: boolean,
  includeKey?: boolean,
  skipWorkspaceDependencies?: boolean,
  defaultTs?: "bun" | "deno"
): Promise<JSZip | TarAsZip | undefined> {
  const requestHeaders = new Headers();
  requestHeaders.set("Authorization", "Bearer " + workspace.token);
  requestHeaders.set("Content-Type", "application/octet-stream");

  const extraHeaders = getHeaders();
  if (extraHeaders) {
    for (const [key, value] of Object.entries(extraHeaders)) {
      requestHeaders.set(key, value);
    }
  }

  const includeWorkspaceDependenciesValue = !(skipWorkspaceDependencies ?? false);
  // `preserve_extra_perms=true` opts the tarball into surfacing granular ACLs
  // on flow / script / app rows. Default-off on the server protects cross-
  // workspace tarball imports from carrying ACLs that reference identities
  // missing in the target workspace; the CLI sync flow explicitly wants them.
  const baseParams = `&plain_secret=${plainSecrets ?? false
    }&skip_variables=${skipVariables ?? false}&skip_resources=${skipResources ?? false
    }&skip_secrets=${skipSecrets ?? false}&include_schedules=${includeSchedules ?? false
    }&include_triggers=${includeTriggers ?? false}&include_users=${includeUsers ?? false
    }&include_groups=${includeGroups ?? false}&include_settings=${includeSettings ?? false
    }&include_key=${includeKey ?? false}&include_workspace_dependencies=${includeWorkspaceDependenciesValue}&default_ts=${defaultTs ?? "bun"}&skip_resource_types=${skipResourceTypes ?? false}&settings_version=v2&preserve_extra_perms=true`;

  const baseUrl = workspace.remote + "api/w/" + workspace.workspaceId + "/workspaces/tarball?";

  // Try zip first (standard format), fall back to tar if zip is not supported
  const zipUrl = baseUrl + "archive_type=zip" + baseParams;
  const zipResponse = await fetch(zipUrl, { headers: requestHeaders, method: "GET" });

  await detectAuthGatewayChallenge(zipResponse, zipUrl);

  if (zipResponse.ok) {
    log.debug("Downloaded zip archive successfully");
    const blob = await zipResponse.blob();
    return await JSZip.loadAsync((await blob.arrayBuffer()) as any);
  }

  const body = await zipResponse.text();

  // If zip format is not supported (backend compiled without zip feature), try tar
  if (zipResponse.status === 400 && body.includes("Invalid Archive Type")) {
    log.debug("Zip archive not supported by backend, falling back to tar");
    const tarUrl = baseUrl + "archive_type=tar" + baseParams;
    const tarResponse = await fetch(tarUrl, { headers: requestHeaders, method: "GET" });

    await detectAuthGatewayChallenge(tarResponse, tarUrl);

    if (tarResponse.ok) {
      log.debug("Downloaded tar archive successfully");
      return await parseTarResponse(tarResponse);
    }

    const tarBody = await tarResponse.text();
    log.info(colors.red(`Failed to request tarball from API: ${tarResponse.status} ${tarResponse.statusText}`));
    if (tarBody) log.info(colors.red(tarBody));
    return process.exit(1);
  }

  if (zipResponse.status === 404 || body.includes("no rows returned")) {
    log.info(colors.red(`Workspace '${workspace.workspaceId}' not found on ${workspace.remote}. Please check your --workspace and try again.`));
  } else {
    log.info(colors.red(`Failed to request tarball from API: ${zipResponse.status} ${zipResponse.statusText}`));
    if (body) log.info(colors.red(body));
  }
  return process.exit(1);
}

function stub(_opts: GlobalOptions & { override: boolean }, _dir: string) {
  console.log(
    colors.red.underline(
      'Pull is deprecated. Use "sync pull --raw" instead. See https://www.windmill.dev/docs/advanced/cli/sync for more information.'
    )
  );
}

const command = new Command()
  .description(
    "Pull all definitions in the current workspace from the API and write them to disk."
  )
  .arguments("<dir:string>")
  .action(stub as any);

export default command;
