<!--
  @component Main cluster status view component; renders current system-usage information

  Properties:
  - `presetCluster String`: The cluster to show status information for
-->

 <script>
  import {
    getContext
  } from "svelte"
  import {
    queryStore,
    gql,
    getContextClient,
  } from "@urql/svelte";
  import {
    init,
    scramble,
    scrambleNames,
  } from "../generic/utils.js";
  import {
    formatDurationTime,
    formatNumber,
    scaleNumber
  } from "../generic/units.js";
  import {
    Row,
    Col,
    Card,
    CardTitle,
    CardHeader,
    CardBody,
    Spinner,
    Table,
    Progress,
    Icon,
  } from "@sveltestrap/sveltestrap";
  import Roofline from "../generic/plots/Roofline.svelte";
  import Pie, { colors } from "../generic/plots/Pie.svelte";
  import Stacked from "../generic/plots/Stacked.svelte";
  import DoubleMetric from "../generic/plots/DoubleMetricPlot.svelte";
  import Refresher from "../generic/helper/Refresher.svelte";

  /* Svelte 5 Props */
  let {
    presetCluster,
  } = $props();

  /*Const Init */
  const { query: initq } = init();
  const client = getContextClient();
  const useCbColors = getContext("cc-config")?.plotConfiguration_colorblindMode || false

  /* States */
  let from = $state(new Date(Date.now() - 5 * 60 * 1000));
  let clusterFrom = $state(new Date(Date.now() - (8 * 60 * 60 * 1000)));
  let to = $state(new Date(Date.now()));
  let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
  let colWidthJobs = $state(0);

  /* Derived */
  // States for Stacked charts
  const statesTimed = $derived(queryStore({
    client: client,
    query: gql`
      query ($filter: [NodeFilter!], $typeNode: String!, $typeHealth: String!) {
        nodeStates: nodeStatesTimed(filter: $filter, type: $typeNode) {
          state
          counts
          times
        }
        healthStates: nodeStatesTimed(filter: $filter, type: $typeHealth) {
          state
          counts
          times
        }
      }
    `,
    variables: {
      filter: { cluster: { eq: presetCluster }, timeStart: stackedFrom},
      typeNode: "node",
      typeHealth: "health"
    },
    requestPolicy: "network-only"
  }));

  // Note: nodeMetrics are requested on configured $timestep resolution
  // Result: The latest 5 minutes (datapoints) for each node independent of job
  const statusQuery = $derived(queryStore({
    client: client,
    query: gql`
      query (
        $cluster: String!
        $metrics: [String!]
        $from: Time!
        $to: Time!
        $clusterFrom: Time!
        $jobFilter: [JobFilter!]!
        $paging: PageRequest!
        $sorting: OrderByInput!
      ) {
        # Node 5 Minute Averages for Roofline
        nodeMetrics(
          cluster: $cluster
          metrics: $metrics
          from: $from
          to: $to
        ) {
          host
          subCluster
          metrics {
            name
            metric {
              series {
                statistics {
                  avg
                }
              }
            }
          }
        }
        # Running Job Metric Average for Rooflines
        jobsMetricStats(filter: $jobFilter, metrics: $metrics) {
          id
          jobId
          duration
          numNodes
          numAccelerators
          subCluster
          stats {
            name
            data {
              avg
            }
          }
        }
        # Get Jobs for Per-Node Counts
        jobs(filter: $jobFilter, order: $sorting, page: $paging) {
          items {
            jobId
            resources {
              hostname
            }
          }
          count
        }
        # Only counts shared nodes once 
        allocatedNodes(cluster: $cluster) {
          name
          count
        }
        # totalNodes includes multiples if shared jobs: Info-Card Data
        jobsStatistics(
          filter: $jobFilter
          page: $paging
          groupBy: CLUSTER
        ) {
          id
          totalJobs
          totalUsers
          totalCores
          totalAccs
        }
        # ClusterMetrics for doubleMetricPlot
        clusterMetrics(
          cluster: $cluster
          metrics: $metrics
          from: $clusterFrom
          to: $to
        ) {
          nodeCount
          metrics {
            name
            unit {
              prefix
              base
            }
            timestep
            data
          }
        }
      }
    `,
    variables: {
      cluster: presetCluster,
      metrics: ["flops_any", "mem_bw"], // Fixed names for roofline and status bars
      from: from.toISOString(),
      to: to.toISOString(),
      clusterFrom: clusterFrom.toISOString(),
      jobFilter: [{ cluster: { eq: presetCluster } }, { state: ["running"] }],
      paging: { itemsPerPage: -1, page: 1 }, // Get all: -1
      sorting: { field: "startTime", type: "col", order: "DESC" }
    },
    requestPolicy: "network-only"
  }));

  const topJobsQuery = $derived(queryStore({
    client: client,
    query: gql`
      query (
        $filter: [JobFilter!]!
      ) {
        jobsStatistics(
          filter: $filter
          sortBy: TOTALJOBS
          groupBy: PROJECT
        ) {
          id
          totalJobs
        }
      }
    `,
    variables: {
      filter: [{ cluster: { eq: presetCluster} }, { state: ["running"] }]
    },
    requestPolicy: "network-only"
  }));

  const topProjectJobs = $derived(
    $topJobsQuery?.data?.jobsStatistics
      ?.toSorted((a, b) => b.totalJobs - a.totalJobs)
      .slice(0, 10) ?? [],
  );

  const clusterInfo = $derived.by(() => {
    let rawInfos = {};
    if ($initq?.data?.clusters) {
      // Grouped By Cluster
      if (!rawInfos['allocatedCores']) rawInfos['allocatedCores'] = $statusQuery?.data?.jobsStatistics?.find(({ id }) => id == presetCluster)?.totalCores || 0;
      if (!rawInfos['allocatedAccs']) rawInfos['allocatedAccs'] = $statusQuery?.data?.jobsStatistics?.find(({ id }) => id == presetCluster)?.totalAccs || 0;
      if (!rawInfos['activeUsers']) rawInfos['activeUsers'] = $statusQuery?.data?.jobsStatistics?.find(({ id }) => id == presetCluster)?.totalUsers || 0;
      if (!rawInfos['runningJobs']) rawInfos['runningJobs'] = $statusQuery?.data?.jobsStatistics?.find(({ id }) => id == presetCluster)?.totalJobs || 0;

      // Collected By Subcluster
      let subClusters = $initq?.data?.clusters?.find((c) => c.name == presetCluster)?.subClusters || [];
      for (let subCluster of subClusters) {
        // Allocations
        if (!rawInfos['allocatedNodes']) rawInfos['allocatedNodes'] = $statusQuery?.data?.allocatedNodes?.find(({ name }) => name == subCluster.name)?.count || 0;
        else rawInfos['allocatedNodes'] += $statusQuery?.data?.allocatedNodes?.find(({ name }) => name == subCluster.name)?.count || 0;

        // Infos
        if (!rawInfos['processorTypes']) rawInfos['processorTypes'] = subCluster?.processorType ? new Set([subCluster.processorType]) : new Set([]);
        else rawInfos['processorTypes'].add(subCluster.processorType);

        if (!rawInfos['totalNodes']) rawInfos['totalNodes'] = subCluster?.numberOfNodes || 0;
        else rawInfos['totalNodes'] += subCluster?.numberOfNodes || 0;

        if (!rawInfos['totalCores']) rawInfos['totalCores'] = (subCluster?.socketsPerNode * subCluster?.coresPerSocket * subCluster?.numberOfNodes) || 0;
        else rawInfos['totalCores'] += (subCluster?.socketsPerNode * subCluster?.coresPerSocket * subCluster?.numberOfNodes) || 0;

        if (!rawInfos['totalAccs']) rawInfos['totalAccs'] = (subCluster?.numberOfNodes * subCluster?.topology?.accelerators?.length) || 0;
        else rawInfos['totalAccs'] += (subCluster?.numberOfNodes * subCluster?.topology?.accelerators?.length) || 0;
          
        // Unit Parts (Set Once)
        if (!rawInfos['flopRateUnitBase']) rawInfos['flopRateUnitBase'] = subCluster.flopRateSimd.unit.base
        if (!rawInfos['memBwRateUnitBase']) rawInfos['memBwRateUnitBase'] = subCluster.memoryBandwidth.unit.base
        if (!rawInfos['flopRateUnitPrefix']) rawInfos['flopRateUnitPrefix'] = subCluster.flopRateSimd.unit.prefix
        if (!rawInfos['memBwRateUnitPrefix']) rawInfos['memBwRateUnitPrefix'] = subCluster.memoryBandwidth.unit.prefix

        // Get Maxima For Roofline Knee Render
        if (!rawInfos['roofData']) {
          rawInfos['roofData'] = {
            flopRateScalar: {value: subCluster.flopRateScalar.value},
            flopRateSimd: {value: subCluster.flopRateSimd.value},
            memoryBandwidth: {value: subCluster.memoryBandwidth.value}
          };
        } else { 
          rawInfos['roofData']['flopRateScalar']['value'] = Math.max(rawInfos['roofData']['flopRateScalar']['value'], subCluster.flopRateScalar.value)
          rawInfos['roofData']['flopRateSimd']['value'] = Math.max(rawInfos['roofData']['flopRateSimd']['value'], subCluster.flopRateSimd.value)
          rawInfos['roofData']['memoryBandwidth']['value'] = Math.max(rawInfos['roofData']['memoryBandwidth']['value'], subCluster.memoryBandwidth.value)
        }
      }

      // Get Simple Idle Infos after Sums by Diff
      if (!rawInfos['idleNodes']) rawInfos['idleNodes'] = rawInfos['totalNodes'] - rawInfos['allocatedNodes'];
      if (!rawInfos['idleCores']) rawInfos['idleCores'] = rawInfos['totalCores'] - rawInfos['allocatedCores'];
      if (!rawInfos['idleAccs']) rawInfos['idleAccs'] = rawInfos['totalAccs'] - rawInfos['allocatedAccs'];
      // Cap at 0 (Negative hints towards Config <> Reality Mismatch!)
      if (rawInfos['idleNodes'] < 0) rawInfos['idleNodes'] = 0;
      if (rawInfos['idleCores'] < 0) rawInfos['idleCores'] = 0;
      if (rawInfos['idleAccs'] < 0) rawInfos['idleAccs'] = 0;

      // Keymetrics (Data on Cluster-Scope)
      let rawFlops = $statusQuery?.data?.nodeMetrics?.reduce((sum, node) =>
        sum + (node.metrics.find((m) => m.name == 'flops_any')?.metric?.series[0]?.statistics?.avg || 0),
        0, // Initial Value
      ) || 0;
      rawInfos['flopRate'] = Math.floor((rawFlops * 100) / 100)

      let rawMemBw = $statusQuery?.data?.nodeMetrics?.reduce((sum, node) =>
        sum + (node.metrics.find((m) => m.name == 'mem_bw')?.metric?.series[0]?.statistics?.avg || 0),
        0, // Initial Value
      ) || 0;
      rawInfos['memBwRate'] = Math.floor((rawMemBw * 100) / 100)
    }
    return rawInfos;
  });

  /* Functions */
  function legendColors(targetIdx) {
    // Reuses first color if targetIdx overflows
    let c;
      if (useCbColors) {
        c = [...colors['colorblind']];
      // } else if (useAltColors) {
      //   c = [...colors['alternative']];
      } else {
        c = [...colors['default']];
      }
    return  c[(c.length + targetIdx) % c.length];
  }

  function transformJobsStatsToData(clusterData) {
    /* c will contain values from 0 to 1 representing the duration */
    let data = null
    const x = [], y = [], c = [], day = 86400.0

    if (clusterData) {
      for (let i = 0; i < clusterData.length; i++) {
        const flopsData = clusterData[i].stats.find((s) => s.name == "flops_any")
        const memBwData = clusterData[i].stats.find((s) => s.name == "mem_bw")
            
        const f = flopsData.data.avg
        const m = memBwData.data.avg
        const d = clusterData[i].duration / day

        const intensity = f / m
        if (Number.isNaN(intensity) || !Number.isFinite(intensity))
            continue

        x.push(intensity)
        y.push(f)
        // Long Jobs > 1 Day: Use max Color
        if (d > 1.0) c.push(1.0)
        else c.push(d)
      }
    } else {
        console.warn("transformJobsStatsToData: metrics for 'mem_bw' and/or 'flops_any' missing!")
    }

    if (x.length > 0 && y.length > 0 && c.length > 0) {
        data = [null, [x, y], c] // for dataformat see roofline.svelte
    }
    return data
  }

  function transformJobsStatsToInfo(clusterData) {
    if (clusterData) {
        return clusterData.map((sc) => { return {id: sc.id, jobId: sc.jobId, numNodes: sc.numNodes, numAcc: sc?.numAccelerators? sc.numAccelerators : 0, duration: formatDurationTime(sc.duration)} })
    } else {
        console.warn("transformJobsStatsToInfo: jobInfo missing!")
        return []
    }
  }

</script>

<Card>
  <CardBody class="align-content-center">
    <Row>
      <Col>
        <Refresher
          hideSelector
          initially={60}
          onRefresh={(interval) => {
            from = new Date(Date.now() - 5 * 60 * 1000);
            to = new Date(Date.now());
            clusterFrom = new Date(Date.now() - (8 * 60 * 60 * 1000))

            if (interval) stackedFrom += Math.floor(interval / 1000);
            else stackedFrom += 1 // Workaround: TimeSelection not linked, just trigger new data on manual refresh
          }}
        />
      </Col>
    </Row>
    {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching}
      <Row class="justify-content-center">
        <Col xs="auto">
          <Spinner />
        </Col>
      </Row>

    {:else if $statusQuery.error || $statesTimed.error || $topJobsQuery.error}
      <Row>
        {#if $statusQuery.error}
          <Col>
            <Card color="danger"><CardBody>Error Requesting Status Data: {$statusQuery.error.message}</CardBody></Card>
          </Col>
        {/if}
        {#if $statesTimed.error}
          <Col>
            <Card color="danger"><CardBody>Error Requesting Node Scheduler States: {$statesTimed.error.message}</CardBody></Card>
          </Col>
        {/if}
        {#if $topJobsQuery.error}
          <Col>
            <Card color="danger"><CardBody>Error Requesting Jobs By Project: {$topJobsQuery.error.message}</CardBody></Card>
          </Col>
        {/if}
      </Row>

    {:else}
      <Row cols={{xs:1, md:2, xl: 3}}>
        <Col> <!-- Info Card -->
          <Card class="h-auto mt-1">
            <CardHeader>
              <CardTitle class="mb-0">Cluster "{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}"</CardTitle>
              <span>{[...clusterInfo?.processorTypes].join(', ')}</span>
            </CardHeader>
            <CardBody>
              <Table borderless class="mb-0">
                <tr class="py-2">
                  <td style="font-size:x-large;">{clusterInfo?.runningJobs} Running Jobs</td>
                  <td style="font-size:x-large;">{clusterInfo?.activeUsers} Active Users</td>
                </tr>
                <hr class="my-1"/>
                <tr class="pt-2">
                  <td style="font-size: large;">
                    Flop Rate (<span style="cursor: help;" title="Flops[Any] = (Flops[Double] x 2) + Flops[Single]">Any</span>)
                  </td>
                  <td style="font-size: large;">
                    Memory BW Rate
                  </td>
                </tr>
                <tr class="pb-2">
                  <td style="font-size:x-large;">
                    {scaleNumber(clusterInfo?.flopRate, clusterInfo?.flopRateUnitPrefix)}{clusterInfo?.flopRateUnitBase}
                  </td>
                  <td style="font-size:x-large;">
                    {scaleNumber(clusterInfo?.memBwRate, clusterInfo?.memBwRateUnitPrefix)}{clusterInfo?.memBwRateUnitBase}
                  </td>
                </tr>
                <hr class="my-1"/>
                <tr class="pt-2">
                  <td>{formatNumber(clusterInfo?.allocatedNodes)} Active Nodes</td>
                  <td>{formatNumber(clusterInfo?.idleNodes)} Idle Nodes</td>
                </tr>
                <tr class="pb-2">
                  <td colspan="2"> <Col class="p-0">
                    <Progress
                      color="success" style="cursor:help; height:1rem;"
                      value={clusterInfo?.allocatedNodes} max={clusterInfo?.totalNodes}
                      title={`${formatNumber(clusterInfo?.totalNodes)} Total Nodes`}
                    />
                  </Col> </td>
                </tr>
                <tr class="pt-2">
                  <td>{formatNumber(clusterInfo?.allocatedCores)} Active Cores</td>
                  <td>{formatNumber(clusterInfo?.idleCores)} Idle Cores</td>
                </tr>
                <tr class="pb-2">
                  <td colspan="2"> <Col class="p-0">
                    <Progress 
                      color="success" style="cursor:help; height:1rem;" 
                      value={clusterInfo?.allocatedCores} max={clusterInfo?.totalCores}
                      title={`${formatNumber(clusterInfo?.totalCores)} Total Cores`}
                    />
                  </Col> </td>
                </tr>
                {#if clusterInfo?.totalAccs !== 0}
                  <tr class="pt-2">
                    <td>{formatNumber(clusterInfo?.allocatedAccs)} Active Accelerators</td>
                    <td>{formatNumber(clusterInfo?.idleAccs)} Idle Accelerators</td>
                  </tr>
                  <tr class="pb-2">
                    <td colspan="2"> <Col class="p-0">
                      <Progress 
                        color="success" style="cursor:help; height:1rem;"
                        value={clusterInfo?.allocatedAccs} max={clusterInfo?.totalAccs}
                        title={`${formatNumber(clusterInfo?.totalAccs)} Total Accelerators`}
                      />
                    </Col> </td >
                  </tr>
                {/if}
              </Table>
            </CardBody>
          </Card>
        </Col>

        <Col> <!-- Pie Jobs -->
          {#if $topJobsQuery?.data?.jobsStatistics?.length > 0}
            <Row cols={{xs:1, md:2}}>
              <Col class="p-2">
                <div bind:clientWidth={colWidthJobs}>
                  <h4 class="text-center">
                    Top Projects: Jobs
                  </h4>
                  <Pie
                    {useCbColors}
                    canvasId="hpcpie-jobs-projects"
                    size={colWidthJobs * 0.75}
                    sliceLabel={'Jobs'}
                    quantities={topProjectJobs.map(
                      (tp) => tp['totalJobs'],
                    )}
                    entities={topProjectJobs.map((tp) =>
                      scrambleNames ? scramble(tp.id) : tp.id)
                    }
                  />
                </div>
              </Col>
              <Col class="p-2">
                <Table>
                  <tr class="mb-2">
                    <th></th>
                    <th style="padding-left: 0.5rem;">Project</th>
                    <th>Jobs</th>
                  </tr>
                  {#each topProjectJobs as tp, i}
                    <tr>
                      <td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
                      <td>
                        <a target="_blank" href="/monitoring/jobs/?cluster={presetCluster}&state=running&project={tp.id}&projectMatch=eq"
                          >{scrambleNames ? scramble(tp.id) : tp.id}
                        </a>
                      </td>
                      <td>{tp['totalJobs']}</td>
                    </tr>
                  {/each}
                </Table>
              </Col>
            </Row>
          {:else}
            <Card body color="warning" class="mx-4 my-2"
              >Cannot render jobs by project: No data returned</Card
            >
          {/if}
        </Col>

        <Col> <!-- Job Roofline -->
          <div>
            {#key $statusQuery?.data?.jobsMetricStats}
              <Roofline
                useColors={true}
                allowSizeChange
                subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null}
                roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats)}
                jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats)}
              />
            {/key}
          </div>
        </Col>

        <Col> <!-- Total Cluster Metric in Time SUMS-->
          <div>
            {#key $statusQuery?.data?.clusterMetrics}
              <DoubleMetric
                timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60}
                numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0}
                metricData={$statusQuery?.data?.clusterMetrics?.metrics || []}
                cluster={presetCluster}
              />
            {/key}
          </div>
        </Col>

        <Col> <!-- Stacked SchedState -->
          <div>
            {#key $statesTimed?.data?.nodeStates}
              <Stacked
                data={$statesTimed?.data?.nodeStates}
                height={330}
                xlabel="Time"
                ylabel="Nodes"
                yunit = "#Count"
                title = "Node States"
                stateType = "Node"
              />
            {/key}
          </div>
        </Col>

        <Col> <!-- Stacked Healthstate -->
          <div>
            {#key $statesTimed?.data?.healthStates}
              <Stacked
                data={$statesTimed?.data?.healthStates}
                height={330}
                xlabel="Time"
                ylabel="Nodes"
                yunit = "#Count"
                title = "Metric Health"
                stateType = "Health"
              />
            {/key}
          </div>
        </Col>
      </Row>
    {/if}
  </CardBody>
</Card>
