import fs from "fs";
import os from "os";
import { Logger } from "@perf-profiler/logger";
import {
  canIgnoreAwsTerminationError,
  cleanup,
  executeCommand,
  executeLongRunningProcess,
} from "../shell";
import { Measure, POLLING_INTERVAL, Profiler, ScreenRecorder } from "@perf-profiler/types";
import { CpuMeasureAggregator } from "../cpu/CpuMeasureAggregator";
import { CppPerformanceMeasure, parseCppMeasure } from "../cppProfiler";
import { processOutput } from "../cpu/getCpuStatsByProcess";
import { processOutput as processRamOutput } from "../ram/pollRamUsage";
import { SHELL } from "./DevMode";
export const CppProfilerName = `BAMPerfProfiler`;

// Since Flipper uses esbuild, we copy the bin folder directly
// into the Flipper plugin directory
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const binaryFolder = global.Flipper
  ? `${__dirname}/bin`
  : `${__dirname}/../../..${__dirname.includes("dist") ? "/.." : ""}/cpp-profiler/bin`;

export abstract class UnixProfiler implements Profiler {
  stop(): void {
    throw new Error("Method not implemented.");
  }
  private hasInstalledProfiler = false;
  private cpuClockTick: number | undefined;
  private RAMPageSize: number | undefined;

  /**
   * Main setup function for the cpp profiler
   *
   * It will:
   * - install the C++ profiler for the correct architecture on the device
   * - Populate needed values like CPU clock tick and RAM page size
   *
   * This needs to be done before measures and can take a few seconds
   */
  public installProfilerOnDevice(): void {
    if (!this.hasInstalledProfiler) {
      this.assertSupported();
      this.installCppProfilerOnDevice();
      this.retrieveCpuClockTick();
      this.retrieveRAMPageSize();
    }
    this.hasInstalledProfiler = true;
  }

  private retrieveCpuClockTick() {
    this.cpuClockTick = parseInt(
      executeCommand(
        this.getDeviceCommand(
          `${this.getDeviceProfilerSandboxedPath()} printCpuClockTick`,
          SHELL.COMPONENT_SHELL
        )
      ),
      10
    );
  }

  private retrieveRAMPageSize() {
    this.RAMPageSize = parseInt(
      executeCommand(
        this.getDeviceCommand(
          `${this.getDeviceProfilerSandboxedPath()} printRAMPageSize`,
          SHELL.COMPONENT_SHELL
        )
      ),
      10
    );
  }

  getCpuClockTick(): number {
    this.installProfilerOnDevice();
    if (!this.cpuClockTick) {
      throw new Error("CPU clock tick not initialized");
    }
    return this.cpuClockTick;
  }

  getRAMPageSize(): number {
    this.installProfilerOnDevice();
    if (!this.RAMPageSize) {
      throw new Error("RAM Page size not initialized");
    }
    return this.RAMPageSize;
  }

  private installCppProfilerOnDevice(): void {
    const abi = this.getAbi();
    Logger.info(`Installing C++ profiler for ${abi} architecture`);

    const binaryPath = `${binaryFolder}/${CppProfilerName}-${abi}`;
    const binaryTmpPath = `${os.tmpdir()}/flashlight-${CppProfilerName}-${abi}`;

    // When running from standalone executable, we need to copy the binary to an actual file
    fs.copyFileSync(binaryPath, binaryTmpPath);

    this.pushExecutable(binaryTmpPath);
    Logger.success(`C++ Profiler installed in ${this.getDeviceProfilerPath()}`);
  }

  pollPerformanceMeasures(
    bundleId: string,
    {
      onMeasure,
      onStartMeasuring = () => {
        // noop by default
      },
    }: {
      onMeasure: (measure: Measure) => void;
      onStartMeasuring?: () => void;
    }
  ) {
    if (this.updateIds(bundleId)) {
      // For dev mode: Re-install profiler in another app shell if bundle id changes.
      this.hasInstalledProfiler = false;
    }
    let initialTime: number | null = null;
    let previousTime: number | null = null;
    let cpuMeasuresAggregator = new CpuMeasureAggregator(this.getCpuClockTick());
    const reset = () => {
      initialTime = null;
      previousTime = null;
      cpuMeasuresAggregator = new CpuMeasureAggregator(this.getCpuClockTick());
    };
    return this.pollPerformanceMeasuresWeirdSubfunction(
      bundleId,
      ({ pid, cpu, ram: ramStr, timestamp }) => {
        const subProcessesStats = processOutput(cpu, pid);

        const ram = processRamOutput(ramStr, this.getRAMPageSize());

        if (!initialTime) {
          initialTime = timestamp;
        }

        if (previousTime) {
          const interval = timestamp - previousTime;

          const cpuMeasures = cpuMeasuresAggregator.process(subProcessesStats, interval);

          onMeasure(
            //TODO update this when FPS support is available in kepler.
            this.supportFPS()
              ? {
                  cpu: cpuMeasures,
                  ram,
                  time: timestamp - initialTime,
                }
              : {
                  cpu: cpuMeasures,
                  ram,
                  time: timestamp - initialTime,
                }
          );
        } else {
          onStartMeasuring();
          cpuMeasuresAggregator.initStats(subProcessesStats);
        }
        previousTime = timestamp;
      },
      () => {
        Logger.warn("Process id has changed, ignoring measures until now");
        reset();
      }
    );
  }

  pollPerformanceMeasuresWeirdSubfunction = (
    pid: string,
    onData: (measure: CppPerformanceMeasure) => void,
    onPidChanged?: (pid: string) => void
  ) => {
    const DELIMITER = "=STOP MEASURE=";

    const process = executeLongRunningProcess(
      this.getDeviceCommand(
        `${this.getDeviceProfilerSandboxedPath()} pollPerformanceMeasures ${pid} ${POLLING_INTERVAL}`,
        SHELL.COMPONENT_SHELL
      ),
      DELIMITER,
      (data: string) => {
        onData(parseCppMeasure(data));
      }
    );

    process.stderr?.on("data", (data) => {
      const log = data.toString();

      // Ignore errors, it might be that the thread is dead and we can't read stats anymore
      if (log.includes("CPP_ERROR_CANNOT_OPEN_FILE")) {
        Logger.debug(log);
      } else if (log.includes("CPP_ERROR_MAIN_PID_CLOSED")) {
        onPidChanged?.(pid);
      } else {
        if (!canIgnoreAwsTerminationError(log)) Logger.error(log);
      }
    });

    return {
      stop: () => {
        process.kill("SIGTERM");
        this.stop();
      },
    };
  };

  // Disabling the warning because the method isn't implemented
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public getScreenRecorder(videoPath: string): ScreenRecorder | undefined {
    return undefined;
  }

  // Disabling the warning because the method isn't implemented
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public async stopApp(bundleId: string) {
    throw new Error("Method not implemented.");
  }

  public cleanup() {
    cleanup();
  }

  public abstract getDeviceCommand(command: string, shell?: SHELL): string;
  protected abstract getAbi(): string;
  protected abstract pushExecutable(binaryTmpPath: string): void;
  protected abstract assertSupported(): void;
  protected abstract updateIds(bundleId: string): boolean;
  public abstract getDeviceProfilerPath(): string;
  public abstract getDeviceProfilerSandboxedPath(): string;
  public abstract detectCurrentBundleId(): string;
  public abstract supportFPS(): boolean;
}
