import type { DataPoint } from "regression";
import { linear } from "regression";
import type { PrismaClientOrTransaction } from "~/db.server";
import { env } from "~/env.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { getUsageSeries } from "~/services/platform.v3.server";
import { createTimeSeriesData } from "~/utils/graphs";
import { BasePresenter } from "./basePresenter.server";

type Options = {
  organizationId: string;
  startDate: Date;
};

export type TaskUsageItem = {
  taskIdentifier: string;
  runCount: number;
  averageDuration: number;
  averageCost: number;
  totalDuration: number;
  totalCost: number;
  totalBaseCost: number;
};

export type UsageSeriesData = {
  date: string;
  dollars: number;
}[];

export class UsagePresenter extends BasePresenter {
  public async call({ organizationId, startDate }: Options) {
    if (isNaN(startDate.getTime())) {
      throw new Error("Invalid start date");
    }

    //month period
    const startOfMonth = new Date(startDate);
    startOfMonth.setUTCDate(1);
    startOfMonth.setUTCHours(0, 0, 0, 0);

    const endOfMonth = new Date(
      startOfMonth.getFullYear(),
      startOfMonth.getMonth() + 1,
      0,
      23,
      59,
      59,
      999
    );

    //usage data from the platform
    const usage = getUsageSeries(organizationId, {
      from: startOfMonth,
      to: endOfMonth,
      window: "DAY",
    }).then((data) => {
      //we want to sum it to get the total usage
      const current = (data?.data.reduce((acc, period) => acc + period.value, 0) ?? 0) / 100;

      // Get the start day (the day the customer started using the product) or the first day of the month
      const startDay = new Date(data?.data.at(0)?.windowStart ?? startOfMonth).getDate();

      // We want to project so we convert the data into an array of tuples [dayNumber, value]
      const projectionData =
        data?.data.map((period, index) => {
          // Each value should be the sum of the previous values + the current value
          // Adjust the day number to start from 1 when the customer started using the product
          return [
            new Date(period.windowStart).getDate() - startDay + 1,
            data.data.slice(0, index + 1).reduce((acc, period) => acc + period.value, 0) / 100,
          ] as DataPoint;
        }) ?? ([] as DataPoint[]);

      const result = linear(projectionData);
      const [a, b] = result.equation;

      // Adjust the total days in the month based on when the customer started
      const totalDaysInMonth = endOfMonth.getDate() - startDay + 1;
      const projected = a * totalDaysInMonth + b;
      const overall = {
        current,
        projected,
      };

      //and create daily data for the graph
      const timeSeries = createTimeSeriesData({
        startDate: startOfMonth,
        endDate: endOfMonth,
        window: "DAY",
        data: data
          ? data.data.map((period) => ({
              date: new Date(period.windowStart),
              value: period.value,
            }))
          : [],
      }).map((period) => ({
        date: period.date.toISOString(),
        dollars: (period.value ?? 0) / 100,
      }));

      return {
        overall,
        timeSeries,
      };
    });

    //usage by task
    const tasks = await getTaskUsageByOrganization(
      organizationId,
      startOfMonth,
      endOfMonth,
      this._replica
    );

    return {
      usage,
      tasks,
    };
  }
}

async function getTaskUsageByOrganization(
  organizationId: string,
  startOfMonth: Date,
  endOfMonth: Date,
  replica: PrismaClientOrTransaction
) {
  const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
    organizationId,
    "standard"
  );
  const [queryError, tasks] = await clickhouse.taskRuns.getTaskUsageByOrganization({
    startTime: startOfMonth.getTime(),
    endTime: endOfMonth.getTime(),
    organizationId,
  });

  if (queryError) {
    throw queryError;
  }

  return tasks
    .map((task) => ({
      taskIdentifier: task.task_identifier,
      runCount: Number(task.run_count),
      averageDuration: Number(task.average_duration),
      averageCost: Number(task.average_cost) + env.CENTS_PER_RUN / 100,
      totalDuration: Number(task.total_duration),
      totalCost: Number(task.total_cost) + Number(task.total_base_cost),
    }))
    .sort((a, b) => b.totalCost - a.totalCost);
}
