import axios from "axios";
import {
  conventionalAppSubpath,
  DeviceParams,
  reverseModelMap,
} from "@ledgerhq/speculos-transport";
import { SpeculosDevice } from "./speculos";
import https from "https";
import { sanitizeError } from "./index";
import { v4 as uuid } from "uuid";

const { GITHUB_TOKEN, SPECULOS_IMAGE_TAG } = process.env;
const GIT_API_URL = "https://api.github.com/repos/LedgerHQ/actions/actions/";
const START_WORKFLOW_ID = "workflows/161487603/dispatches";
const STOP_WORKFLOW_ID = "workflows/161487604/dispatches";
const GITHUB_REF = "main";
const getSpeculosAddress = (runId: string) => `https://${runId}.speculos.aws.stg.ldg-tech.com`;
const speculosPort = 443;

function uniqueId(): string {
  return uuid();
}

function slugify(name: string): string {
  return name
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

/**
 * Helper function to make API requests with error handling
 */
async function githubApiRequest<T = unknown>({
  method = "POST",
  urlSuffix,
  data,
  params,
}: {
  method?: "GET" | "POST";
  urlSuffix: string;
  data?: Record<string, unknown>;
  params?: Record<string, unknown>;
}): Promise<T> {
  const url = `${GIT_API_URL}${urlSuffix}`;
  try {
    const response = await axios({
      method,
      url,
      headers: {
        Authorization: `Bearer ${GITHUB_TOKEN}`,
        Accept: "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
      },
      data,
      params,
    });
    return response.data;
  } catch (error) {
    console.warn(`API Request failed: ${method} ${url}`, sanitizeError(error));
    throw sanitizeError(error);
  }
}

export function waitForSpeculosReady(
  deviceId: string,
  { interval = 2_000, timeout = 150_000 } = {},
) {
  return new Promise((resolve, reject) => {
    const startTime = Date.now();
    let currentRequest: ReturnType<typeof https.get> | null = null;
    const url = getSpeculosAddress(deviceId);

    function cleanup() {
      if (currentRequest) {
        currentRequest.destroy();
        currentRequest = null;
      }
    }

    function check() {
      cleanup();

      currentRequest = https.get(url, { timeout: 10000 }, res => {
        if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) {
          process.env.SPECULOS_ADDRESS = url;
          cleanup();
          console.warn(`Speculos is ready at ${url}`);
          resolve(true);
        } else {
          console.warn(`Speculos not ready yet, status: ${res.statusCode}`);
          retry();
        }
      });

      currentRequest.on("error", error => {
        console.error(`Request error: ${error.message}`);
        retry();
      });

      currentRequest.on("timeout", () => {
        console.error("Request timeout");
        retry();
      });
    }

    function retry() {
      if (Date.now() - startTime >= timeout) {
        cleanup();
        reject(new Error(`Timeout: ${url} did not become available within ${timeout}ms`));
      } else {
        setTimeout(check, interval);
      }
    }

    check();
  });
}

function createStartPayload(deviceParams: DeviceParams, runId: string) {
  const { model, firmware, appName, appVersion, dependencies } = deviceParams;

  let additional_args = "-p";

  if (dependencies?.length) {
    additional_args = [
      additional_args,
      ...new Set(
        dependencies.map(
          dep =>
            `-l ${dep.name}:/apps/${conventionalAppSubpath(
              model,
              firmware,
              dep.name,
              dep.appVersion ?? appVersion,
            )}`,
        ),
      ),
    ].join(" ");
  }

  return {
    ref: GITHUB_REF,
    inputs: {
      speculos_version: SPECULOS_IMAGE_TAG?.split(":")[1] || "master",
      coin_app: appName,
      coin_app_version: appVersion,
      device: reverseModelMap[model],
      device_os_version: firmware,
      run_id: runId,
      additional_args,
    },
  };
}

export async function createSpeculosDeviceCI(
  deviceParams: DeviceParams,
): Promise<SpeculosDevice | undefined> {
  const runId = `${slugify(deviceParams.appName)}-${uniqueId()}`;
  try {
    const data = createStartPayload(deviceParams, runId);
    await githubApiRequest({ urlSuffix: START_WORKFLOW_ID, data });
    return {
      id: runId,
      port: speculosPort,
      appName: deviceParams.appName,
      appVersion: deviceParams.appVersion,
      dependencies: deviceParams.dependencies,
    };
  } catch (error) {
    console.warn(
      `Failed to create remote Speculos ${deviceParams.appName}:${deviceParams.appVersion}:`,
      sanitizeError(error),
    );
    return {
      id: runId,
      port: 0,
      appName: deviceParams.appName,
      appVersion: deviceParams.appVersion,
      dependencies: deviceParams.dependencies,
    };
  }
}

export async function releaseSpeculosDeviceCI(runId: string) {
  const data = {
    ref: GITHUB_REF,
    inputs: {
      run_id: runId.toString(),
    },
  };
  try {
    await githubApiRequest({ urlSuffix: STOP_WORKFLOW_ID, data });
  } catch (error) {
    console.warn(`Failed to release remote Speculos ${runId}:`, sanitizeError(error));
  }
}
