// Copyright 2017-2026 @pezkuwi/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { SubmittableResult } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/promise/types';
import type { ActionStatus, ActionStatusPartial, PartialQueueTxExtrinsic, PartialQueueTxRpc, QueueProps, QueueStatus, QueueTx, QueueTxExtrinsic, QueueTxRpc, QueueTxStatus, SignerCallback } from '@pezkuwi/react-components/Status/types';
import type { DispatchError, EventRecord } from '@pezkuwi/types/interfaces';
import type { ITuple, Registry, SignerPayloadJSON } from '@pezkuwi/types/types';

import React, { useCallback, useMemo, useRef, useState } from 'react';

import { getDispatchError, getIncompleteMessage } from '@pezkuwi/react-components/Status/checks';
import { STATUS_COMPLETE } from '@pezkuwi/react-components/Status/constants';
import { getContractAbi } from '@pezkuwi/react-components/util';
import jsonrpc from '@pezkuwi/types/interfaces/jsonrpc';

export interface Props {
  children: React.ReactNode;
}

interface StatusCount {
  count: number;
  status: ActionStatusPartial;
}

const EMPTY_STATE: Partial<QueueProps> = {
  stqueue: [] as QueueStatus[],
  txqueue: [] as QueueTx[]
};

let nextId = 0;

const EVENT_MESSAGE = 'extrinsic event';
const REMOVE_TIMEOUT = 7500;
const SUBMIT_RPC = jsonrpc.author.submitAndWatchExtrinsic;

export const QueueCtx = React.createContext<QueueProps>(EMPTY_STATE as QueueProps);

function mergeStatus (status: ActionStatusPartial[]): ActionStatus[] {
  let others: ActionStatus | null = null;

  const initial = status
    .reduce((result: StatusCount[], status): StatusCount[] => {
      const prev = result.find(({ status: prev }) =>
        prev.action === status.action &&
        prev.status === status.status
      );

      if (prev) {
        prev.count++;
      } else {
        result.push({ count: 1, status });
      }

      return result;
    }, [])
    .map(({ count, status }): ActionStatusPartial =>
      count === 1
        ? status
        : { ...status, action: `${status.action} (x${count})` }
    )
    .filter((status): boolean => {
      if (status.message !== EVENT_MESSAGE) {
        return true;
      }

      if (others) {
        if (status.action.startsWith('system.ExtrinsicSuccess')) {
          (others.action as string[]).unshift(status.action);
        } else {
          (others.action as string[]).push(status.action);
        }
      } else {
        others = {
          ...status,
          action: [status.action]
        };
      }

      return false;
    });

  return others
    ? initial.concat(others)
    : initial;
}

function extractEvents (result?: SubmittableResult): ActionStatus[] {
  return mergeStatus(
    (result?.events || [])
      // filter events handled globally, or those we are not interested in, these are
      // handled by the global overview, so don't add them here
      .filter((record) =>
        !!record.event &&
        record.event.section !== 'democracy'
      )
      .map((record): ActionStatusPartial => {
        const { event: { data, method, section } } = record;

        if (section === 'system' && method === 'ExtrinsicFailed') {
          const [dispatchError] = data as unknown as ITuple<[DispatchError]>;

          return {
            action: `${section}.${method}`,
            message: getDispatchError(dispatchError),
            status: 'error'
          };
        }

        const incomplete = getIncompleteMessage(record);

        if (incomplete) {
          return {
            action: `${section}.${method}`,
            message: incomplete,
            status: 'eventWarn'
          };
        } else if (section === 'contracts') {
          if (method === 'ContractExecution' && data.length === 2) {
            // see if we have info for this contract
            const [accountId, encoded] = data;

            try {
              const abi = getContractAbi(accountId.toString());

              if (abi) {
                const decoded = abi.decodeEvent(encoded as EventRecord);

                return {
                  action: decoded.event.identifier,
                  message: 'contract event',
                  status: 'event'
                };
              }
            } catch (error) {
              // ABI mismatch?
              console.error(error);
            }
          } else if (method === 'Evicted') {
            return {
              action: `${section}.${method}`,
              message: 'contract evicted',
              status: 'error'
            };
          }
        }

        return {
          action: `${section}.${method}`,
          message: EVENT_MESSAGE,
          status: 'event'
        };
      })
  );
}

const EMPTY_QUEUE_ST: QueueStatus[] = [];
const EMPTY_QUEUE_TX: QueueTx[] = [];

export function QueueCtxRoot ({ children }: Props): React.ReactElement<Props> {
  const [stqueue, _setStQueue] = useState<QueueStatus[]>(EMPTY_QUEUE_ST);
  const [txqueue, _setTxQueue] = useState<QueueTx[]>(EMPTY_QUEUE_TX);
  const stRef = useRef(stqueue);
  const txRef = useRef(txqueue);

  const setStQueue = useCallback(
    (st: QueueStatus[]): void => {
      stRef.current = st;
      _setStQueue(st);
    },
    []
  );

  const setTxQueue = useCallback(
    (tx: QueueTx[]): void => {
      txRef.current = tx;
      _setTxQueue(tx);
    },
    []
  );

  const addToTxQueue = useCallback(
    (value: QueueTxExtrinsic | QueueTxRpc | QueueTx): void => {
      const id = ++nextId;
      const removeItem = () => setTxQueue([
        ...txRef.current.map((item): QueueTx =>
          item.id === id
            ? { ...item, status: 'completed' }
            : item
        )
      ]);

      setTxQueue([...txRef.current, {
        ...value,
        id,
        removeItem,
        rpc: (value as QueueTxRpc).rpc || SUBMIT_RPC,
        status: 'queued'
      }]);
    },
    [setTxQueue]
  );

  const queueAction = useCallback(
    (_status: ActionStatus | ActionStatus[]): void => {
      const status = Array.isArray(_status) ? _status : [_status];

      status.length && setStQueue([
        ...stRef.current,
        ...(status.map((item): QueueStatus => {
          const id = ++nextId;
          const removeItem = (): void =>
            setStQueue([...stRef.current.filter((item) => item.id !== id)]);

          setTimeout(removeItem, REMOVE_TIMEOUT);

          return {
            ...item,
            id,
            isCompleted: false,
            removeItem
          };
        }))
      ]);
    },
    [setStQueue]
  );

  const queueExtrinsic = useCallback(
    (value: PartialQueueTxExtrinsic) => addToTxQueue({ ...value }),
    [addToTxQueue]
  );

  const queuePayload = useCallback(
    (registry: Registry, payload: SignerPayloadJSON, signerCb: SignerCallback): void => {
      addToTxQueue({
        accountId: payload.address,
        // this is not great, but the Extrinsic doesn't need a submittable
        extrinsic: registry.createType('Extrinsic',
          { method: registry.createType('Call', payload.method) },
          { version: payload.version }
        ) as unknown as SubmittableExtrinsic,
        payload,
        signerCb
      });
    },
    [addToTxQueue]
  );

  const queueRpc = useCallback(
    (value: PartialQueueTxRpc) => addToTxQueue({ ...value }),
    [addToTxQueue]
  );

  const queueSetTxStatus = useCallback(
    (id: number, status: QueueTxStatus, result?: SubmittableResult, error?: Error): void => {
      setTxQueue([
        ...txRef.current.map((item): QueueTx =>
          item.id === id
            ? {
              ...item,
              error: error === undefined
                ? item.error
                : error,
              result: result === undefined
                ? item.result as SubmittableResult
                : result,
              status: item.status === 'completed'
                ? item.status
                : status
            }
            : item
        )
      ]);

      queueAction(extractEvents(result));

      if (STATUS_COMPLETE.includes(status)) {
        setTimeout((): void => {
          const item = txRef.current.find((item) => item.id === id);

          item && item.removeItem();
        }, REMOVE_TIMEOUT);
      }
    },
    [queueAction, setTxQueue]
  );

  const value = useMemo(
    () => ({
      queueAction,
      queueExtrinsic,
      queuePayload,
      queueRpc,
      queueSetTxStatus,
      stqueue,
      txqueue
    }),
    [queueAction, queueExtrinsic, queuePayload, queueRpc, queueSetTxStatus, stqueue, txqueue]
  );

  return (
    <QueueCtx.Provider value={value}>
      {children}
    </QueueCtx.Provider>
  );
}
