// To run:
//  npm install
//  GRAFANA_BEARER_TOKEN=XXX GRAFANA_URL=https://XXX.grafana.net/ npx tsx grafana-dashboard.ts

import {
  DashboardBuilder,
  DashboardCursorSync,
  QueryVariableBuilder,
  RowBuilder,
} from "@grafana/grafana-foundation-sdk/dashboard";
import {
  PanelBuilder as TextPanelBuilder,
  TextMode,
} from "@grafana/grafana-foundation-sdk/text";
import { DataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus";
import { PanelBuilder as TimeseriesBuilder } from "@grafana/grafana-foundation-sdk/timeseries";

const TOKEN = process.env.GRAFANA_BEARER_TOKEN;
const GRAFANA_URL = process.env.GRAFANA_URL;

// This is the definition of the dashboard!
function makeBlogDashboard() {
  // Declare the name and define a unique id.
  const dash = new DashboardBuilder("Blog Host Metrics Dashboard");
  dash
    .uid("blog-host-dashboard")
    .tags(["generated", "blog"])
    .refresh("30s")
    .time({ from: "now-1h", to: "now" })
    .tooltip(DashboardCursorSync.Crosshair)
    .timezone("browser");

  // Helper function for adding charts.
  const addTimeseriesChart = makeAddTimeseriesChart(dash);

  // Variable definitions. Grafana uses metric queries to populate the possible
  // values, and the variable queries can depend on each other.
  dash
    .withVariable(
      new QueryVariableBuilder("instance")
        .query("label_values(up, instance)")
        .current({ text: "All", value: "$__all" })
        .multi(true),
    );

  // Example of a text panel with generation timestamp.
  dash.withPanel(
    new TextPanelBuilder()
      .title("")
      .content(
        `Generated at ${new Date()}.`,
      )
      .mode(TextMode.Markdown)
      // Full width.
      .gridPos({ x: 0, y: 0, w: 24, h: 2 }),
  );

  // Let's give the row a title!
  dash.withRow(new RowBuilder("Host Metrics"));

  // Add basic host metrics charts
  addTimeseriesChart(
    "CPU Usage %",
    `100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle",instance=~"$instance"}[5m])) * 100)`,
    {
      panelCustomization: (panel) => panel.unit("percent").max(100).min(0),
    },
  );

  addTimeseriesChart(
    "Load Average",
    `node_load1{instance=~"$instance"}`,
    {
      panelCustomization: (panel) => panel.unit("short"),
    },
  );

  addTimeseriesChart(
    "Memory Usage %",
    `(1 - (node_memory_MemAvailable_bytes{instance=~"$instance"} / node_memory_MemTotal_bytes{instance=~"$instance"})) * 100`,
    {
      panelCustomization: (panel) => panel.unit("percent").max(100).min(0),
    },
  );

  return dash;
}

// Helper method to create "addTimeseriesChart" methods for your dashboard.
function makeAddTimeseriesChart(dash: DashboardBuilder) {
  const builders = {
    buildPanel: () =>
      new TimeseriesBuilder().gridPos({ x: 0, y: 0, w: 8, h: 6 }),
    // You might need to specify a default datasource, like so:
    // new DataqueryBuilder().datasource({ type: "prometheus", uid: "prometheus-uid" })
    buildQueryTarget: () => new DataqueryBuilder(),
  };
  return makeAddChart<TimeseriesBuilder>(dash, builders);
}

// Helper method for the helper methods. This facilitates using panel types.
function makeAddChart<T extends TimeseriesBuilder>(
  dash: DashboardBuilder,
  builders: { buildPanel: () => T; buildQueryTarget: () => DataqueryBuilder },
) {
  return function addChart(
    title: string,
    query: string,
    {
      panelCustomization,
      queryCustomization,
    }: {
      panelCustomization?: (panel: T) => T;
      queryCustomization?: (dataQuery: DataqueryBuilder) => DataqueryBuilder;
    } = {},
  ) {
    const panel = builders.buildPanel().title(title);
    const queryTarget = builders.buildQueryTarget().expr(query);

    // Apply query customization if provided
    if (queryCustomization) {
      queryCustomization(queryTarget);
    }

    // Apply panel customization if provided
    if (panelCustomization) {
      panelCustomization(panel);
    }

    // Attach the customized query target to the panel
    panel.withTarget(queryTarget);
    dash.withPanel(panel);
  };
}

/**
 * Invokes the Grafana API to create or update the given dashboard.
 */
async function createDashboard(dash: DashboardBuilder) {
  let version: number | undefined = undefined;
  const built = dash.build();
  // 1) Try to fetch existing dashboard to get its version:
  const getResponse = await fetch(
    `${GRAFANA_URL}api/dashboards/uid/${built.uid}`,
    {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${TOKEN}`,
      },
    },
  );
  // If response is OK, parse JSON and retrieve version:
  if (getResponse.ok) {
    const getData = await getResponse.json() as { dashboard: { version: number } };
    version = getData.dashboard.version;
    built.version = version;
  } else if (getResponse.status === 404) {
    // If the dashboard does not exist, set version to 0
    built.version = 0;
  } else {
    // Other non-200 responses are treated as errors:
    throw new Error(`Fetch GET failed with status: ${getResponse.status}`);
  }

  // 2) POST (create or overwrite) the dashboard:
  try {
    const postResponse = await fetch(`${GRAFANA_URL}api/dashboards/db`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${TOKEN}`,
      },
      body: JSON.stringify({
        dashboard: built,
        overwrite: true,
        message: "Automated update from blog post",
      }),
    });
    if (!postResponse.ok) {
      const errorText = await postResponse.text();
      throw new Error(
        `Fetch POST failed with status: ${postResponse.status}. Response: ${errorText}`,
      );
    }
    // Parse JSON for the returned info
    const data = await postResponse.json() as { uid: string };
    console.log("Dashboard updated successfully!");
    console.log("Dashboard URL:", `${GRAFANA_URL}d/${data.uid}`);
    return data;
  } catch (error) {
    console.error("Error posting dashboard:", error);
    throw error;
  }
}

async function main() {
  if (!TOKEN) {
    console.error(
      "Please provide a Grafana bearer token in the GRAFANA_BEARER_TOKEN environment variable.",
    );
    process.exit(1);
  }
  if (!GRAFANA_URL) {
    console.error(
      "Please provide the Grafana URL in the GRAFANA_URL environment variable.",
    );
    process.exit(1);
  }

  // Ensure URL ends with /
  const normalizedUrl = GRAFANA_URL.endsWith("/")
    ? GRAFANA_URL
    : GRAFANA_URL + "/";
  process.env.GRAFANA_URL = normalizedUrl;

  console.log("Creating dashboard...");
  await createDashboard(makeBlogDashboard());
}

// Run main function if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
  await main();
}