/**
 *
 * Copyright 2023-2024 InspectorRAGet Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **/

'use client';

import { isEmpty } from 'lodash';
import cx from 'classnames';
import { useState, useMemo } from 'react';
import {
  Tag,
  FilterableMultiSelect,
  Toggletip,
  ToggletipButton,
  ToggletipContent,
  DataTable,
  Table,
  TableHead,
  TableRow,
  TableHeader,
  TableBody,
  TableCell,
} from '@carbon/react';
import { Information, WarningAlt } from '@carbon/icons-react';

import { BoxplotChart, StackedBarChart } from '@carbon/charts-react';
import { ScaleTypes } from '@carbon/charts';

import { useTheme } from '@/src/theme';
import { Model, TaskEvaluation, Metric } from '@/src/types';
import { AgreementLevels } from '@/src/utilities/metrics';
import {
  getAgreementLevelColorPalette,
  getVotingPatternColorPalette,
} from '@/src/utilities/colors';
import { areObjectsIntersecting } from '@/src/utilities/objects';
import Filters from '@/src/components/filters/Filters';

import '@carbon/charts-react/styles.css';
import classes from './AnnotatorBehavior.module.scss';
import InterAnnotatorAgreementHeatMap from '@/src/views/annotator-behavior/InterAnnotatorAgreementHeatMap.tsx';

// ===================================================================================
//                                TYPES
// ===================================================================================
interface Props {
  evaluationsPerMetric: { [key: string]: TaskEvaluation[] };
  models: Model[];
  metrics: Metric[];
  filters: { [key: string]: string[] };
}

// ===================================================================================
//                                CONSTANTS
// ===================================================================================
const VOTING_PATTERNS: string[] = Object.keys(getVotingPatternColorPalette());

// ===================================================================================
//                               HELPER FUNCTIONS
// ===================================================================================
function normalize(data: { [key: string]: number }) {
  const total = Object.values(data).reduce((a, b) => a + b, 0);
  return Object.fromEntries(
    Object.entries(data).map(([agreementLevel, count]) => {
      return [
        agreementLevel,
        Math.round(((count / total) * 100 + Number.EPSILON) * 100) / 100,
      ];
    }),
  );
}

function updateVotingPattern(
  votingPatternsPerTopicPerVoter: {
    [key: string]: { [key: string]: { [key: string]: number } };
  },
  topic: string,
  voter: string,
  votingPattern: string,
) {
  if (votingPatternsPerTopicPerVoter.hasOwnProperty(topic)) {
    if (votingPatternsPerTopicPerVoter[topic].hasOwnProperty(voter)) {
      votingPatternsPerTopicPerVoter[topic][voter][votingPattern] += 1;
    } else {
      votingPatternsPerTopicPerVoter[topic][voter] = {
        ...Object.fromEntries(VOTING_PATTERNS.map((pattern) => [pattern, 0])),
        [votingPattern]: 1,
      };
    }
  } else {
    votingPatternsPerTopicPerVoter[topic] = {
      [voter]: {
        ...Object.fromEntries(VOTING_PATTERNS.map((pattern) => [pattern, 0])),
        [votingPattern]: 1,
      },
    };
  }
}

function prepareBoxPlotData(data: { [key: string]: number[] }) {
  // Populate plot data array
  const plotData: { [key: string]: string | number }[] = [];
  for (const [worker, durations] of Object.entries(data)) {
    durations.forEach((duration) => {
      plotData.push({
        group: worker,
        key: 'All',
        value: Math.floor(duration / 60) + (duration % 60) / 60,
      });
    });
  }
  return plotData;
}
// ===================================================================================
//                              RENDER FUNCTIONS
// ===================================================================================
function renderAgreementDistribution(
  record: {
    [key: string]: { [key: string]: number };
  },
  key: string,
  caption: string,
  models: Model[],
  theme?: string,
) {
  // Step 1: Compute overall agreement level distribution
  const overall: { [key: string]: number } = {
    No: 0,
    Low: 0,
    High: 0,
    Absolute: 0,
  };
  for (const distribution of Object.values(record)) {
    for (const [agreementLevel, count] of Object.entries(distribution)) {
      overall[agreementLevel] += count;
    }
  }

  // Step 2: Build chart data
  const chartData: { [key: string]: string | number }[] = [];
  for (const [agreementLevel, count] of Object.entries(normalize(overall))) {
    chartData.push({ group: agreementLevel, key: 'All', value: count });
  }
  for (const [modelId, distribution] of Object.entries(record)) {
    for (const [agreementLevel, count] of Object.entries(
      normalize(distribution),
    )) {
      const model = models.find((entry) => entry.modelId === modelId);
      chartData.push({
        group: agreementLevel,
        key: model ? model.name : modelId,
        value: count,
      });
    }
  }

  // Step 3: Render
  return (
    <div key={key} className={classes.graph}>
      <h5>
        <strong>{caption}</strong>
      </h5>
      <StackedBarChart
        data={chartData}
        options={{
          axes: {
            left: {
              scaleType: ScaleTypes.LABELS,
            },
            bottom: {
              stacked: true,
              ticks: {
                formatter: (tick) => tick + '%',
              },
            },
          },
          color: {
            scale: getAgreementLevelColorPalette(),
          },
          width: '500px',
          height: '500px',
          toolbar: {
            enabled: false,
          },
          theme: theme,
        }}
      ></StackedBarChart>
    </div>
  );
}

function renderAnnotatorVotingPattern(
  record: {
    [key: string]: { [key: string]: number };
  },
  key: string,
  caption: string,
  theme?: string,
) {
  // Step 1: Compute overall agreement level distribution
  const overall: { [key: string]: number } = Object.fromEntries(
    VOTING_PATTERNS.map((pattern) => [pattern, 0]),
  );
  for (const distribution of Object.values(record)) {
    for (const [affiliaion, count] of Object.entries(distribution)) {
      overall[affiliaion] += count;
    }
  }

  // Step 2: Build chart data
  const chartData: { [key: string]: string | number }[] = [];
  for (const [affliation, count] of Object.entries(normalize(overall))) {
    chartData.push({ group: affliation, key: 'All', value: count });
  }
  for (const [individual, distribution] of Object.entries(record)) {
    for (const [affiliaion, count] of Object.entries(normalize(distribution))) {
      chartData.push({
        group: affiliaion,
        key: `ID: ${individual}`,
        value: count,
      });
    }
  }

  // Step 3: Render
  return (
    <div key={key} className={classes.graph}>
      <h5>
        <strong>{caption}</strong>
      </h5>

      <StackedBarChart
        data={chartData}
        options={{
          axes: {
            left: {
              scaleType: ScaleTypes.LABELS,
            },
            bottom: {
              stacked: true,
              ticks: {
                formatter: (tick) => tick + '%',
              },
            },
          },
          color: {
            scale: getVotingPatternColorPalette(),
          },
          width: '500px',
          height: '500px',
          toolbar: {
            enabled: false,
          },
          theme: theme,
        }}
      ></StackedBarChart>
    </div>
  );
}

// ===================================================================================
//                               MAIN FUNCTION
// ===================================================================================
export default function AnnotatorBehavior({
  evaluationsPerMetric,
  models,
  metrics,
  filters,
}: Props) {
  // Step 1: Initialize state and necessary variables
  const eligibleMetricNames = useMemo(() => {
    return new Set(metrics.map((metric) => metric.name));
  }, [metrics]);
  const [selectedModels, setSelectedModels] = useState<Model[]>(models);
  const [selectedFilters, setSelectedFilters] = useState<{
    [key: string]: string[];
  }>({});

  // Step 2: Run effects
  // Step 2.a: Fetch theme
  const { theme } = useTheme();

  // Step 2.b: Adjust visible evaluations based on selected models
  const visibleEvaluationsPerMetric = useMemo(() => {
    const filteredEvaluationsPerMetric: { [key: string]: TaskEvaluation[] } =
      {};
    for (const [metric, evaluations] of Object.entries(evaluationsPerMetric)) {
      if (eligibleMetricNames.has(metric)) {
        filteredEvaluationsPerMetric[metric] = evaluations.filter(
          (evaluation) =>
            selectedModels
              .map((model) => model.modelId)
              .includes(evaluation.modelId) &&
            (!isEmpty(selectedFilters)
              ? areObjectsIntersecting(selectedFilters, evaluation)
              : true),
        );
      }
    }
    return filteredEvaluationsPerMetric;
  }, [
    evaluationsPerMetric,
    eligibleMetricNames,
    selectedModels,
    selectedFilters,
  ]);

  // Step 2.c: Build agreement distribution chart data per model for visible evaluations
  const [
    agreementDistributionPerMetricPerModel,
    annotatorVotingPatternPerMetric,
  ] = useMemo(() => {
    // Step 2.c.i: Initialize necessary variables
    const agreementStatisticPerMetricPerModel: {
      [key: string]: { [key: string]: { [key: string]: number } };
    } = {};
    const votingPatternsPerMetricPerAnnotator: {
      [key: string]: { [key: string]: { [key: string]: number } };
    } = {};

    for (const metric in visibleEvaluationsPerMetric) {
      agreementStatisticPerMetricPerModel[metric] = Object.fromEntries(
        selectedModels.map((model) => [
          model.modelId,
          { No: 0, Low: 0, High: 0, Absolute: 0 },
        ]),
      );
    }

    // Step 2.c.ii: Iterate over evaluations for each metric
    for (const [metric, evaluations] of Object.entries(
      visibleEvaluationsPerMetric,
    )) {
      evaluations.forEach((evaluation) => {
        switch (evaluation[`${metric}_agg`].level) {
          // Case: All annotators gave same rating
          case AgreementLevels.ABSOLUTE_AGREEMENT:
            agreementStatisticPerMetricPerModel[metric][evaluation.modelId][
              'Absolute'
            ] += 1;

            // Update voting pattern
            for (const annotator in evaluation[metric]) {
              updateVotingPattern(
                votingPatternsPerMetricPerAnnotator,
                metric,
                annotator,
                'Unanimous',
              );
            }
            break;

          case AgreementLevels.HIGH_AGREEMENT:
            // Case: Majority of annotators gave same rating and minority rating is close to majority rating
            agreementStatisticPerMetricPerModel[metric][evaluation.modelId][
              'High'
            ] += 1;

            // Update voting pattern
            for (const annotator in evaluation[metric]) {
              updateVotingPattern(
                votingPatternsPerMetricPerAnnotator,
                metric,
                annotator,
                evaluation[metric][annotator]['value'] ===
                  evaluation[`${metric}_agg`].value
                  ? 'Majority'
                  : 'Dissidents (minor)',
              );
            }

            break;

          case AgreementLevels.LOW_AGREEMENT:
            // Case: Majority of annotators gave same rating and minority rating is far from majority rating
            agreementStatisticPerMetricPerModel[metric][evaluation.modelId][
              'Low'
            ] += 1;

            // Update voting pattern
            for (const annotator in evaluation[metric]) {
              updateVotingPattern(
                votingPatternsPerMetricPerAnnotator,
                metric,
                annotator,
                evaluation[metric][annotator]['value'] ===
                  evaluation[`${metric}_agg`].value
                  ? 'Majority'
                  : 'Dissidents (major)',
              );
            }

            break;

          default:
            // Case: All annotators gave different rating
            agreementStatisticPerMetricPerModel[metric][evaluation.modelId][
              'No'
            ] += 1;

            // Update voting pattern
            for (const annotator in evaluation[metric]) {
              updateVotingPattern(
                votingPatternsPerMetricPerAnnotator,
                metric,
                annotator,
                'Divided',
              );
            }
        }
      });
    }

    return [
      agreementStatisticPerMetricPerModel,
      votingPatternsPerMetricPerAnnotator,
    ];
  }, [visibleEvaluationsPerMetric, selectedModels]);

  // Step 2.d: Build time duration distribution per annotator for all evaluations
  const timeDistributionPerAnnotator: {
    [key: string]: number[];
  } = useMemo(() => {
    const temp: { [key: string]: number[] } = {};
    const processedEvaluationTaskIDs = new Set();
    for (const [metric, evaluations] of Object.entries(evaluationsPerMetric)) {
      evaluations.forEach((evaluation) => {
        if (!processedEvaluationTaskIDs.has(evaluation.taskId)) {
          Object.keys(evaluation[metric]).forEach((worker) => {
            if (evaluation[metric][worker]['duration'] !== undefined) {
              if (temp.hasOwnProperty(worker)) {
                temp[worker] = [
                  ...temp[worker],
                  evaluation[metric][worker]['duration'],
                ];
              } else {
                temp[worker] = [evaluation[metric][worker]['duration']];
              }
            }
          });
        }
      });
      break;
    }
    return temp;
  }, [evaluationsPerMetric]);

  // Step 3: Render
  return (
    <div className={classes.page}>
      {Array.isArray(metrics) && !metrics.length ? (
        <>
          <div className={classes.warningContainer}>
            <WarningAlt
              height={'32px'}
              width={'32px'}
              className={classes.warningContainerIcon}
            />
            <span className={classes.warningContainerText}>
              Nothing to see here in absence of human evaluations.
            </span>
          </div>
        </>
      ) : (
        <>
          <div className={classes.selectors}>
            <div className={classes.modelSelector}>
              <FilterableMultiSelect
                id={'model-selector'}
                titleText="Choose models"
                items={models}
                initialSelectedItems={selectedModels}
                itemToString={(item) => item.name}
                onChange={(event) => {
                  setSelectedModels(event.selectedItems);
                }}
                invalid={selectedModels.length === 0}
                invalidText={'You must select a model to review.'}
              ></FilterableMultiSelect>
              <div>
                {selectedModels.map((model) => {
                  return (
                    <Tag type={'cool-gray'} key={'model-' + model.modelId}>
                      {model.name}
                    </Tag>
                  );
                })}
              </div>
            </div>
          </div>
          {!isEmpty(filters) ? (
            <Filters
              keyPrefix="AnnotatorBehavior"
              filters={filters}
              selectedFilters={selectedFilters}
              setSelectedFilters={setSelectedFilters}
            />
          ) : null}
          <div className={classes.row}>
            <h4>Agreement Distribution</h4>
            <div
              className={cx(
                classes.graphsContainer,
                Object.keys(agreementDistributionPerMetricPerModel).length <= 3
                  ? classes.center
                  : null,
              )}
            >
              {Object.entries(agreementDistributionPerMetricPerModel).map(
                ([metricName, agreementDistributionPerModel], idx) => {
                  const metric = metrics.find(
                    (entry) => entry.name === metricName,
                  );
                  return renderAgreementDistribution(
                    agreementDistributionPerModel,
                    'agreement-distribution-graph-' + idx,
                    metric?.displayName
                      ? metric?.displayName
                      : metricName.charAt(0).toUpperCase() +
                          metricName.slice(1).toLowerCase(),
                    models,
                    theme,
                  );
                },
              )}
            </div>
          </div>
          <div className={classes.row}>
            <h4>Annotator Contribution</h4>
            <div
              className={cx(
                classes.graphsContainer,
                Object.keys(annotatorVotingPatternPerMetric).length <= 3
                  ? classes.center
                  : null,
              )}
            >
              {Object.entries(annotatorVotingPatternPerMetric).map(
                ([metricName, distribution], idx) => {
                  const metric = metrics.find(
                    (entry) => entry.name === metricName,
                  );
                  return renderAnnotatorVotingPattern(
                    distribution,
                    'annotator-abnormality-graph-' + idx,
                    metric?.displayName
                      ? metric?.displayName
                      : metricName.charAt(0).toUpperCase() +
                          metricName.slice(1).toLowerCase(),
                    theme,
                  );
                },
              )}
            </div>
          </div>
          <div className={classes.row}>
            <h4>
              Inter Annotator Agreement (Cohen's Kappa&nbsp;
              <Toggletip>
                <ToggletipButton label="Show information">
                  <Information />
                </ToggletipButton>
                <ToggletipContent>
                  <h6>How to interprete Cohen's kappa coefficient?</h6>
                  <DataTable
                    rows={[
                      {
                        id: 0,
                        score: '0',
                        intepretation: 'No agreement',
                      },
                      {
                        id: 1,
                        score: '0.10-0.20',
                        intepretation: 'Slight agreement',
                      },
                      {
                        id: 2,
                        score: '0.21-0.40',
                        intepretation: 'Fair agreement',
                      },
                      {
                        id: 3,
                        score: '0.41-0.60',
                        intepretation: 'Moderate agreement',
                      },
                      {
                        id: 4,
                        score: '0.61-0.80',
                        intepretation: 'Substantial agreement',
                      },
                      {
                        id: 5,
                        score: '0.81-0.99',
                        intepretation: 'Near perfect agreement',
                      },
                      {
                        id: 6,
                        score: '1',
                        intepretation: 'Perfect agreement',
                      },
                    ]}
                    headers={[
                      {
                        key: 'score',
                        header: "Cohen's kappa",
                      },
                      {
                        key: 'intepretation',
                        header: 'Intepretation',
                      },
                    ]}
                  >
                    {({
                      rows,
                      headers,
                      getTableProps,
                      getHeaderProps,
                      getRowProps,
                    }) => (
                      <Table {...getTableProps()}>
                        <TableHead>
                          <TableRow>
                            {headers.map((header, idx) => (
                              <TableHeader
                                key={'toggletip-table-header-' + idx}
                                {...getHeaderProps({ header })}
                              >
                                {header.header}
                              </TableHeader>
                            ))}
                          </TableRow>
                        </TableHead>
                        <TableBody>
                          {rows.map((row, idx) => (
                            <TableRow
                              key={'toggletip-table-row-' + idx}
                              {...getRowProps({ row })}
                            >
                              {row.cells.map((cell) => (
                                <TableCell key={cell.id}>
                                  {cell.value}
                                </TableCell>
                              ))}
                            </TableRow>
                          ))}
                        </TableBody>
                      </Table>
                    )}
                  </DataTable>
                </ToggletipContent>
              </Toggletip>
              )
            </h4>
            <div
              className={cx(
                classes.graphsContainer,
                Object.keys(visibleEvaluationsPerMetric).length <= 3
                  ? classes.center
                  : null,
              )}
            >
              {Object.keys(visibleEvaluationsPerMetric).map(
                (metricName, idx) => {
                  const metric = metrics.find(
                    (entry) => entry.name === metricName,
                  );
                  return (
                    <div
                      key={'inter-annotator-agreement-heatmap-' + idx}
                      className={classes.graph}
                    >
                      <h5>
                        <strong>
                          {metric?.displayName
                            ? metric?.displayName
                            : metricName.charAt(0).toUpperCase() +
                              metricName.slice(1).toLowerCase()}
                        </strong>
                      </h5>
                      {visibleEvaluationsPerMetric[metricName] && (
                        <InterAnnotatorAgreementHeatMap
                          evaluations={visibleEvaluationsPerMetric[metricName]}
                          metric={metricName}
                          theme={theme}
                        ></InterAnnotatorAgreementHeatMap>
                      )}
                    </div>
                  );
                },
              )}
            </div>
          </div>
          {!isEmpty(timeDistributionPerAnnotator) ? (
            <div className={classes.row}>
              <h4>Time spent</h4>
              <div className={classes.timeSpentGraphsContainer}>
                <div
                  key={'time-spent-graph'}
                  className={classes.timeSpentDistributionGraph}
                >
                  <h5>
                    <strong>Time spent per task (in minutes)</strong>
                  </h5>
                  <BoxplotChart
                    key={'per-annotator-time-spent'}
                    data={prepareBoxPlotData(timeDistributionPerAnnotator)}
                    options={{
                      axes: {
                        left: {
                          scaleType: ScaleTypes.LABELS,
                          mapsTo: 'group',
                        },
                        bottom: {
                          mapsTo: 'value',
                        },
                      },
                      width: '1000px',
                      height: '400px',
                      toolbar: {
                        enabled: false,
                      },
                      theme: theme,
                    }}
                  ></BoxplotChart>
                </div>
              </div>
            </div>
          ) : null}
        </>
      )}
    </div>
  );
}
