import { ref, computed, type ComputedRef, onMounted, onUnmounted } from 'vue';
import { config } from '@/wagmi';
import { type WatchEventReturnType } from 'viem';
import axios from 'axios';
import { getAccount, watchChainId, watchAccount, watchContractEvent, type Config } from '@wagmi/core';
import type { WatchChainIdReturnType, WatchAccountReturnType, GetAccountReturnType } from '@wagmi/core';

import { weiToNumber } from 'kraiken-lib/format';
import { getTaxRateIndexByDecimal } from '@/composables/useAdjustTaxRates';
import logger from '@/utils/logger';
import { DEFAULT_CHAIN_ID } from '@/config';
import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
import { HarbContract } from '@/contracts/harb';
const rawActivePositions = ref<Array<Position>>([]);
const rawClosedPositoins = ref<Array<Position>>([]);
const loading = ref(false);
const positionsError = ref<string | null>(null);
const GRAPHQL_TIMEOUT_MS = 15_000;
const activeChainId = ref<number>(DEFAULT_CHAIN_ID);
const positionsUpdatedAt = ref<bigint>(0n);
const POLL_INTERVAL_MS = 10_000;

const retryManager = createRetryManager(loadPositions, activeChainId);
let positionsPollTimer: ReturnType<typeof setInterval> | null = null;
let pollInFlight = false;
let realtimeConsumerCount = 0;
let isPollingActive = false;
let isContractWatchActive = false;
let unwatchPositionCreated: WatchEventReturnType | null = null;
let unwatchPositionRemoved: WatchEventReturnType | null = null;
let unwatchChainSwitch: WatchChainIdReturnType | null = null;
let unwatchAccountChanged: WatchAccountReturnType | null = null;
let wagmiWatcherConsumers = 0;
const activePositions = computed(() => {
  const account = getAccount(config as Config);

  return rawActivePositions.value
    .map(obj => {
      const taxRateDecimal = Number(obj.taxRate);
      const taxRateIndex =
        Number.isFinite(taxRateDecimal) && !Number.isNaN(taxRateDecimal) ? getTaxRateIndexByDecimal(taxRateDecimal) : undefined;

      return {
        ...obj,
        positionId: BigInt(obj.id),
        amount: weiToNumber(obj.harbDeposit, 18),
        taxRatePercentage: taxRateDecimal * 100,
        taxRate: taxRateDecimal,
        taxRateIndex,
        iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
        totalSupplyEnd: obj.totalSupplyEnd ? BigInt(obj.totalSupplyEnd) : undefined,
        totalSupplyInit: BigInt(obj.totalSupplyInit),
        taxPaid: BigInt(obj.taxPaid),
        share: Number(obj.share),
      };
    })
    .sort((a, b) => {
      // Sort by tax rate index instead of decimal to avoid floating-point issues
      // Positions without an index are pushed to the end
      if (typeof a.taxRateIndex !== 'number') return 1;
      if (typeof b.taxRateIndex !== 'number') return -1;
      return a.taxRateIndex - b.taxRateIndex;
    });
});

export interface Position {
  creationTime: Date;
  id: string;
  positionId: bigint;
  owner: string;
  lastTaxTime: Date;
  taxPaid: bigint;
  taxRate: number;
  taxRateIndex?: number;
  taxRatePercentage: number;
  share: number;
  status: string;
  totalSupplyEnd?: bigint;
  totalSupplyInit: bigint;
  amount: number;
  harbDeposit: bigint;
  iAmOwner: boolean;
  snatched: number;
  closedAt?: string;
  payout?: string;
}

const myClosedPositions: ComputedRef<Position[]> = computed(() => {
  const account = getAccount(config as Config);

  return rawClosedPositoins.value.map(obj => {
    const taxRateDecimal = Number(obj.taxRate);
    const taxRateIndex =
      Number.isFinite(taxRateDecimal) && !Number.isNaN(taxRateDecimal) ? getTaxRateIndexByDecimal(taxRateDecimal) : undefined;

    return {
      ...obj,
      positionId: BigInt(obj.id),
      amount: obj.share * 1000000,
      // amount: weiToNumber(obj.harbDeposit, 18),
      taxRatePercentage: taxRateDecimal * 100,
      taxRate: taxRateDecimal,
      taxRateIndex,
      iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
      totalSupplyEnd: obj.totalSupplyEnd !== undefined ? BigInt(obj.totalSupplyEnd) : undefined,
      totalSupplyInit: BigInt(obj.totalSupplyInit),
      taxPaid: BigInt(obj.taxPaid),
      share: Number(obj.share),
    };
  });
});

const myActivePositions: ComputedRef<Position[]> = computed(() =>
  activePositions.value.filter((obj: Position) => {
    return obj.iAmOwner;
  })
);

const tresholdValue = computed(() => {
  // Compute average tax rate index instead of percentage to avoid floating-point issues
  const validIndices = activePositions.value.map(obj => obj.taxRateIndex).filter((idx): idx is number => typeof idx === 'number');

  if (validIndices.length === 0) return 0;

  const sum = validIndices.reduce((partialSum, idx) => partialSum + idx, 0);
  const avgIndex = sum / validIndices.length;

  // Return half the average index (rounded down)
  return Math.floor(avgIndex / 2);
});

interface ActivePositionsResult {
  positions: Position[];
  updatedAt: bigint;
}

export async function loadActivePositions(chainId: number, endpointOverride?: string): Promise<ActivePositionsResult> {
  const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
  logger.info(`loadActivePositions for chainId: ${chainId}`);
  const res = await axios.post(
    targetEndpoint,
    {
      query: `query ActivePositions {
			positionss(where: { status: "Active" }, orderBy: "taxRate", orderDirection: "asc", limit: 1000) {
				items {
					id
					lastTaxTime
					owner
					payout
					share
					kraikenDeposit
					snatched
					status
					taxPaid
					taxRate
					totalSupplyEnd
					totalSupplyInit
					creationTime
				}
			}
			stats(id: "0x01") {
				positionsUpdatedAt
			}
		}`,
    },
    { timeout: GRAPHQL_TIMEOUT_MS }
  );

  const errors = res.data?.errors;
  if (Array.isArray(errors) && errors.length > 0) {
    throw new Error(errors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', '));
  }

  const items = res.data?.data?.positionss?.items ?? [];
  const stats = res.data?.data?.stats;
  const updatedAtRaw = stats?.positionsUpdatedAt;
  const updatedAt = typeof updatedAtRaw === 'string' ? BigInt(updatedAtRaw) : 0n;

  return {
    positions: items.map((item: Record<string, unknown>) => ({
      ...item,
      harbDeposit: item.kraikenDeposit ?? '0',
    })) as Position[],
    updatedAt,
  };
}

// Position IDs are now directly converted to BigInt without transformation
// since GraphQL returns them as numeric strings

export async function loadMyClosedPositions(chainId: number, endpointOverride: string | undefined, account: GetAccountReturnType) {
  if (!account.address) return [];
  const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
  logger.info(`loadMyClosedPositions for chainId: ${chainId}`);
  const res = await axios.post(
    targetEndpoint,
    {
      query: `query ClosedPositions($owner: String!) {
			positionss(where: { status: "Closed", owner: $owner }, limit: 1000) {
				items {
					id
					lastTaxTime
					owner
					payout
					share
					kraikenDeposit
					snatched
					status
					taxPaid
					taxRate
					totalSupplyEnd
					totalSupplyInit
					creationTime
					closedAt
				}
			}
		}`,
      variables: { owner: account.address?.toLowerCase() ?? '' },
    },
    { timeout: GRAPHQL_TIMEOUT_MS }
  );
  const errors = res.data?.errors;
  if (Array.isArray(errors) && errors.length > 0) {
    throw new Error(errors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', '));
  }
  const items = res.data?.data?.positionss?.items ?? [];
  return items.map((item: Record<string, unknown>) => ({
    ...item,
    harbDeposit: item.kraikenDeposit ?? '0',
  })) as Position[];
}

export async function fetchPositionsUpdatedAt(chainId: number, endpointOverride?: string): Promise<bigint> {
  const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
  logger.info(`fetchPositionsUpdatedAt for chainId: ${chainId}`);

  const res = await axios.post(
    targetEndpoint,
    {
      query: `query PositionsUpdatedAt {
			stats(id: "0x01") {
				positionsUpdatedAt
			}
		}`,
    },
    { timeout: GRAPHQL_TIMEOUT_MS }
  );

  const errors = res.data?.errors;
  if (Array.isArray(errors) && errors.length > 0) {
    throw new Error(errors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', '));
  }

  const updatedAtRaw = res.data?.data?.stats?.positionsUpdatedAt;
  if (typeof updatedAtRaw !== 'string') {
    throw new Error('positionsUpdatedAt missing from GraphQL response');
  }

  return BigInt(updatedAtRaw);
}

export async function loadPositions(chainId?: number) {
  loading.value = true;

  const targetChainId = typeof chainId === 'number' ? chainId : (activeChainId.value ?? DEFAULT_CHAIN_ID);
  activeChainId.value = targetChainId;

  let endpoint: string;
  try {
    endpoint = resolveGraphqlEndpoint(targetChainId);
  } catch (error) {
    rawActivePositions.value = [];
    rawClosedPositoins.value = [];
    positionsError.value = error instanceof Error ? error.message : 'GraphQL endpoint not configured for this chain.';
    retryManager.clear();
    retryManager.reset();
    loading.value = false;
    return;
  }

  try {
    const { positions, updatedAt } = await loadActivePositions(targetChainId, endpoint);
    rawActivePositions.value = positions;
    positionsUpdatedAt.value = updatedAt;
    const account = getAccount(config as Config);
    if (account.address) {
      rawClosedPositoins.value = await loadMyClosedPositions(targetChainId, endpoint, account);
    } else {
      rawClosedPositoins.value = [];
    }
    positionsError.value = null;
    retryManager.reset();
    retryManager.clear();
  } catch (error) {
    rawActivePositions.value = [];
    rawClosedPositoins.value = [];
    positionsError.value = formatGraphqlError(error);
    retryManager.schedule();
    positionsUpdatedAt.value = 0n;
  } finally {
    loading.value = false;
  }
}

async function pollPositionsOnce() {
  if (!isPollingActive || realtimeConsumerCount === 0 || pollInFlight || loading.value) {
    return;
  }
  pollInFlight = true;
  try {
    const chainId = activeChainId.value ?? DEFAULT_CHAIN_ID;
    let endpoint: string;
    try {
      endpoint = resolveGraphqlEndpoint(chainId);
    } catch (error) {
      logger.info('positions polling skipped: no GraphQL endpoint', error);
      return;
    }
    const latestUpdatedAt = await fetchPositionsUpdatedAt(chainId, endpoint);
    if (latestUpdatedAt > positionsUpdatedAt.value) {
      await loadPositions(chainId);
    }
  } catch (error) {
    logger.info('positions polling failed', error);
  } finally {
    pollInFlight = false;
  }
}

function startPositionsPolling() {
  if (isPollingActive || realtimeConsumerCount === 0) {
    return;
  }
  positionsPollTimer = setInterval(() => {
    void pollPositionsOnce();
  }, POLL_INTERVAL_MS);
  isPollingActive = true;
  void pollPositionsOnce();
}

function stopPositionsPolling() {
  if (!isPollingActive) {
    return;
  }
  if (positionsPollTimer) {
    clearInterval(positionsPollTimer);
    positionsPollTimer = null;
  }
  isPollingActive = false;
}

function startContractEventWatchers() {
  if (isContractWatchActive || realtimeConsumerCount === 0) {
    return;
  }

  unwatchPositionCreated = watchContractEvent(config as Config, {
    address: HarbContract.contractAddress,
    abi: HarbContract.abi,
    eventName: 'PositionCreated',
    async onLogs() {
      await loadPositions(activeChainId.value);
    },
  });

  unwatchPositionRemoved = watchContractEvent(config as Config, {
    address: HarbContract.contractAddress,
    abi: HarbContract.abi,
    eventName: 'PositionRemoved',
    async onLogs() {
      await loadPositions(activeChainId.value);
    },
  });

  isContractWatchActive = true;
}

function stopContractEventWatchers() {
  if (!isContractWatchActive) {
    return;
  }
  if (unwatchPositionCreated) {
    unwatchPositionCreated();
    unwatchPositionCreated = null;
  }
  if (unwatchPositionRemoved) {
    unwatchPositionRemoved();
    unwatchPositionRemoved = null;
  }
  isContractWatchActive = false;
}

function syncRealtimeMode() {
  if (realtimeConsumerCount === 0) {
    stopContractEventWatchers();
    stopPositionsPolling();
    return;
  }

  const account = getAccount(config as Config);
  const shouldUseContractEvents = Boolean(account.address);

  if (shouldUseContractEvents) {
    stopPositionsPolling();
    startContractEventWatchers();
  } else {
    stopContractEventWatchers();
    startPositionsPolling();
  }
}

function registerRealtimeConsumer() {
  realtimeConsumerCount += 1;
  syncRealtimeMode();
}

function unregisterRealtimeConsumer() {
  if (realtimeConsumerCount === 0) {
    return;
  }
  realtimeConsumerCount -= 1;
  if (realtimeConsumerCount === 0) {
    stopContractEventWatchers();
    stopPositionsPolling();
  }
}

function ensureWagmiWatchers() {
  wagmiWatcherConsumers += 1;
  if (wagmiWatcherConsumers > 1) {
    return;
  }

  if (!unwatchChainSwitch) {
    unwatchChainSwitch = watchChainId(config as Config, {
      async onChange(nextChainId) {
        const resolvedChainId = nextChainId ?? DEFAULT_CHAIN_ID;
        activeChainId.value = resolvedChainId;
        await loadPositions(resolvedChainId);
        syncRealtimeMode();
      },
    });
  }

  if (!unwatchAccountChanged) {
    unwatchAccountChanged = watchAccount(config as Config, {
      async onChange() {
        await loadPositions(activeChainId.value);
        syncRealtimeMode();
      },
    });
  }
}

function teardownWagmiWatchers() {
  wagmiWatcherConsumers = Math.max(0, wagmiWatcherConsumers - 1);
  if (wagmiWatcherConsumers > 0) {
    return;
  }
  if (unwatchChainSwitch) {
    unwatchChainSwitch();
    unwatchChainSwitch = null;
  }
  if (unwatchAccountChanged) {
    unwatchAccountChanged();
    unwatchAccountChanged = null;
  }
}

export function usePositions(chainId: number = DEFAULT_CHAIN_ID) {
  activeChainId.value = chainId;

  onMounted(async () => {
    ensureWagmiWatchers();

    if (activePositions.value.length < 1 && loading.value === false) {
      await loadPositions(activeChainId.value);
    }

    registerRealtimeConsumer();
  });

  onUnmounted(() => {
    unregisterRealtimeConsumer();
    teardownWagmiWatchers();
    retryManager.clear();
  });

  function createRandomPosition(amount: number = 1) {
    for (let index = 0; index < amount; index++) {
      const newPosition: Position = {
        creationTime: new Date(),
        id: '123',
        positionId: 123n,
        owner: 'bla',
        lastTaxTime: new Date(),
        taxPaid: 100n,
        taxRate: randomInRange(0.01, 1),
        taxRateIndex: Math.floor(randomInRange(1, 30)),
        taxRatePercentage: getRandomInt(1, 100),
        share: getRandomInt(0.001, 0.09),
        status: 'active',
        totalSupplyEnd: undefined,
        totalSupplyInit: 1000000000000n,
        amount: 150,
        harbDeposit: getRandomBigInt(1000, 5000),
        iAmOwner: false,
        snatched: 0,
      };
      rawActivePositions.value.push(newPosition);
    }
  }

  function randomInRange(min: number, max: number) {
    return Math.random() < 0.5 ? (1 - Math.random()) * (max - min) + min : Math.random() * (max - min) + min;
  }

  function getRandomInt(min: number, max: number) {
    const minCeiled = Math.ceil(min);
    const maxFloored = Math.floor(max);
    const randomNumber = Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive

    return randomNumber;
  }

  function getRandomBigInt(min: number, max: number) {
    const randomNumber = getRandomInt(min, max);
    return BigInt(randomNumber) * 10n ** 18n;
  }

  return {
    activePositions,
    myActivePositions,
    myClosedPositions,
    tresholdValue,
    createRandomPosition,
    positionsError,
    loading,
    positionsUpdatedAt,
  };
}
