import axios from "axios";
import last from "lodash/last.js";
import { moonriver } from "viem/chains";
import { Amount } from "../../../common/index.js";
import type { Environment } from "../../../environments/index.js";
import { publicEnvironments } from "../../../environments/index.js";
import {
  MultichainProposalStateMapping,
  type Proposal,
  type ProposalState,
} from "../../../types/proposal.js";
import { postWithRetry } from "../../axiosWithRetry.js";
import type { ApiProposal } from "../governor-api-client.js";

export const WORMHOLE_CONTRACT = "0xc8e2b0cd52cf01b0ce87d389daa3d414d4ce29f3";

type PonderExtendedProposalData = {
  id: number;
  title: string;
  subtitle: string;
  description: string;
  targets: string[];
  calldatas: string[];
  signatures: string[];
  stateChanges: {
    blockNumber: number;
    transactionHash: string;
    state: string;
  }[];
};

axios.defaults.timeout = 5_000;

/**
 * Extract proposal subtitle from description
 */
export const extractProposalSubtitle = (input: string): string => {
  const lines = input.split("\n");
  const h1Line = lines.find((line) => line.startsWith("#"));

  if (!h1Line) {
    return input ? input.substring(0, 100) : "";
  }

  let result = h1Line.substring(1).trim();
  const h2Index = result.indexOf("##");
  if (h2Index !== -1) {
    result = result.substring(0, h2Index).trim();
  }

  result = result.replace(/\\n/g, "").trim();

  if (result.length > 80) {
    result = `${result.substring(0, 80)}...`;
  }

  // Special cases for proposals that don't follow the standard naming convention
  if (result.includes("Moonbeam")) {
    result = result.replace("MIP-B", "MIP-M");
  }

  if (result.indexOf("MIP-B22: Gauntlet") >= 0) {
    result = result.replace("MIP-B22", "MIP-B24");
  }

  if (result.indexOf("MIP-O01: Gauntlet") >= 0) {
    result = result.replace("MIP-O01", "MIP-O03");
  }

  if (result.indexOf("MIP-M02: Upgrade") >= 0) {
    result = result.replace("MIP-M02", "MIP-M03");
  }

  if (result.indexOf("MIP-R02: Upgrade") >= 0) {
    result = result.replace("MIP-R02", "MIP-R03");
  }

  if (result.indexOf("Proposal: Onboard wstETH") >= 0) {
    result = result.replace("Proposal:", "MIP-B08");
  }

  if (
    result.indexOf("Gauntlet's Moonriver Recommendations (2024-01-09)") >= 0
  ) {
    result = result.replace("Gauntlet", "MIP-R10: Gauntlet");
  }

  return result;
};

/**
 * Detects if a proposal is a multichain proposal
 */
export const isMultichainProposal = (targets?: string[]): boolean => {
  return (
    targets?.some(
      (target) => target.toLowerCase() === WORMHOLE_CONTRACT.toLowerCase(),
    ) ?? false
  );
};

/**
 * Routes a proposal to the multichain governor when:
 *   - its targets include the Wormhole bridge (legacy detection), OR
 *   - its proposalId is past the legacy Artemis governor's `proposalCount`,
 *     which means it could only have been created on the multichain governor
 *     (proposals migrated to the multichain governor after the cutoff but
 *     can have local-only targets, e.g. Moonbeam-internal contract calls).
 *
 * `legacyArtemisMaxId === 0` indicates the count read failed; in that case we
 * fall back to the targets-only heuristic.
 */
export const isMultichainAware = (
  proposal: { targets?: string[]; proposalId: number },
  legacyArtemisMaxId: number,
): boolean =>
  isMultichainProposal(proposal.targets) ||
  (legacyArtemisMaxId > 0 && proposal.proposalId > legacyArtemisMaxId);

export type ApiProposalFormatted = {
  forVotes: Amount;
  againstVotes: Amount;
  abstainVotes: Amount;
  totalVotes: Amount;
  canceled: boolean;
  executed: boolean;
  stateChanges: Array<{
    blockNumber: number;
    transactionHash: string;
    state: string;
    chainId: number;
  }>;
  title: string;
  subtitle: string;
};

/**
 * Parses and formats API proposal data
 */
export const formatApiProposalData = (
  apiProposal: ApiProposal,
): ApiProposalFormatted => {
  const forVotesNum = Number(apiProposal.forVotes);
  const againstVotesNum = Number(apiProposal.againstVotes);
  const abstainVotesNum = Number(apiProposal.abstainVotes);

  const forVotes = new Amount(BigInt(Math.floor(forVotesNum * 1e18)), 18);
  const againstVotes = new Amount(
    BigInt(Math.floor(againstVotesNum * 1e18)),
    18,
  );
  const abstainVotes = new Amount(
    BigInt(Math.floor(abstainVotesNum * 1e18)),
    18,
  );

  const totalVotesValue = forVotesNum + againstVotesNum + abstainVotesNum;
  const totalVotes = new Amount(BigInt(Math.floor(totalVotesValue * 1e18)), 18);

  const canceled =
    apiProposal.stateChanges?.some((sc: any) => sc.state === "CANCELED") ??
    false;
  const executed =
    apiProposal.stateChanges?.some((sc: any) => sc.state === "EXECUTED") ??
    false;

  // IMPORTANT: Use sc.chainId from the state change, not apiProposal.chainId
  // This preserves cross-chain events (e.g., QUEUED/EXECUTED on Base with chainId 8453)
  const stateChanges =
    apiProposal.stateChanges?.map((sc: any) => ({
      blockNumber: Number(sc.blockNumber),
      transactionHash: sc.transactionHash,
      state: sc.state,
      chainId: sc.chainId, // Uses the chainId from the state change itself
      timestamp: sc.timestamp !== undefined ? Number(sc.timestamp) : undefined,
    })) || [];

  const subtitle = extractProposalSubtitle(apiProposal.description);
  const title = `Proposal #${apiProposal.proposalId}`;

  return {
    forVotes,
    againstVotes,
    abstainVotes,
    totalVotes,
    canceled,
    executed,
    stateChanges,
    title,
    subtitle,
  };
};

export type ProposalOnChainData = {
  state: number;
  proposalData: any;
  eta: number;
  votesCollected: boolean;
  quorum: bigint;
};

// Cached per chain: highest proposalId held by the legacy Artemis governor.
// Anything with a higher proposalId belongs to the multichain governor, even
// if its targets don't include the Wormhole bridge. The legacy governor only
// receives new proposals during chain migrations, so a 5-minute TTL is plenty.
const LEGACY_ARTEMIS_MAX_ID_TTL_MS = 5 * 60 * 1000;
const legacyArtemisMaxIdCache = new Map<
  number,
  { value: number; fetchedAt: number }
>();

const getLegacyArtemisMaxId = async (
  governanceEnvironment: Environment,
): Promise<number> => {
  const governor = governanceEnvironment.contracts.governor;
  if (!governor) return 0;

  const cached = legacyArtemisMaxIdCache.get(governanceEnvironment.chainId);
  if (cached && Date.now() - cached.fetchedAt < LEGACY_ARTEMIS_MAX_ID_TTL_MS) {
    return cached.value;
  }

  try {
    const value = Number(await governor.read.proposalCount());
    legacyArtemisMaxIdCache.set(governanceEnvironment.chainId, {
      value,
      fetchedAt: Date.now(),
    });
    return value;
  } catch (error) {
    console.warn("Failed to fetch legacy governor proposalCount:", error);
    return cached?.value ?? 0;
  }
};

/**
 * Fetches on-chain data for multiple proposals
 */
export const getProposalsOnChainData = async (
  apiProposals: ApiProposal[],
  governanceEnvironment: Environment,
): Promise<ProposalOnChainData[]> => {
  let quorum = 0n;

  if (governanceEnvironment.contracts.governor) {
    try {
      quorum =
        governanceEnvironment.chainId === 1284
          ? await governanceEnvironment.contracts.governor.read.quorumVotes()
          : await governanceEnvironment.contracts.governor.read.getQuorum();
    } catch (error) {
      console.warn("Failed to fetch quorum:", error);
    }
  }

  const legacyArtemisMaxId = await getLegacyArtemisMaxId(governanceEnvironment);

  const onChainDataList = await Promise.all(
    apiProposals.map(async (p) => {
      const isMultichain = isMultichainAware(p, legacyArtemisMaxId);

      const governorContract = isMultichain
        ? governanceEnvironment.contracts.multichainGovernor
        : governanceEnvironment.contracts.governor;

      let state = 0;
      let proposalData = null;

      if (governorContract) {
        try {
          [state, proposalData] = await Promise.all([
            governorContract.read.state([BigInt(p.proposalId)]),
            governorContract.read.proposals([BigInt(p.proposalId)]),
          ]);
        } catch (error) {
          console.warn("Failed to fetch state and proposalData:", error);
        }
      }

      let eta = 0;

      if (proposalData) {
        const onChainEta = Number(proposalData[4]);
        if (onChainEta === 0 && isMultichain && p.votingEndTime) {
          eta = p.votingEndTime + 86400; // 1 day
        } else {
          eta = onChainEta;
        }
      } else if (isMultichain && p.votingEndTime) {
        eta = p.votingEndTime + 86400; // 1 day
      }

      return { state, proposalData, eta, votesCollected: false, quorum };
    }),
  );

  const votesCollectedList = await Promise.all(
    apiProposals.map(async (apiProposal) => {
      const isMultichain = isMultichainAware(apiProposal, legacyArtemisMaxId);

      if (
        !isMultichain ||
        !governanceEnvironment.contracts.multichainGovernor
      ) {
        return false;
      }

      const xcGovernanceSettings = governanceEnvironment.custom.governance;
      if (!xcGovernanceSettings || xcGovernanceSettings.chainIds.length === 0) {
        return false;
      }

      try {
        const xcEnvironments = xcGovernanceSettings.chainIds
          .map((chainId) =>
            Object.values(publicEnvironments).find(
              (env) => env.chainId === chainId,
            ),
          )
          .filter((env) => {
            if (!env) return false;
            const hasWormhole =
              env.custom &&
              "wormhole" in env.custom &&
              env.custom.wormhole?.chainId;
            const hasVoteCollector =
              env.contracts &&
              "voteCollector" in env.contracts &&
              env.contracts.voteCollector;
            return !!(hasWormhole && hasVoteCollector);
          });

        if (xcEnvironments.length === 0) {
          return false;
        }

        const votesCollectedChecks = await Promise.all(
          xcEnvironments.map(async (xcEnvironment) => {
            try {
              const wormholeChainId = (xcEnvironment!.custom as any)?.wormhole
                ?.chainId;
              if (!wormholeChainId) return false;

              const [forVotes, againstVotes, abstainVotes] =
                await governanceEnvironment.contracts.multichainGovernor!.read.chainVoteCollectorVotes(
                  [wormholeChainId, BigInt(apiProposal.proposalId)],
                );
              return forVotes > 0n || againstVotes > 0n || abstainVotes > 0n;
            } catch (error) {
              return false;
            }
          }),
        );

        return (
          votesCollectedChecks.length > 0 &&
          votesCollectedChecks.every((collected) => collected)
        );
      } catch (error) {
        console.warn("Failed to check votes collected status:", error);
        return false;
      }
    }),
  );

  return onChainDataList.map((data, index) => ({
    ...data,
    votesCollected: votesCollectedList[index] ?? false,
  }));
};

/**
 * Get proposal data from on-chain (Ponder-based, for Moonriver)
 */
export const getProposalData = async (params: {
  environment: Environment;
  id?: number;
}) => {
  try {
    if (params.environment.contracts.governor) {
      let count = 0n;
      let quorum = 0n;

      if (params.environment.chainId === moonriver.id) {
        [count, quorum] = await Promise.all([
          params.environment.contracts.governor.read.proposalCount(),
          params.environment.contracts.governor.read.getQuorum(),
        ]);
      } else {
        [count, quorum] = await Promise.all([
          params.environment.contracts.governor.read.proposalCount(),
          params.environment.contracts.governor.read.quorumVotes(),
        ]);
      }

      if (params.id) {
        if (BigInt(params.id) > count) {
          return [];
        }
      }

      const ids = params.id
        ? [BigInt(params.id)]
        : Array.from({ length: Number(count) }, (_, i) => count - BigInt(i));

      const proposalDataCall = Promise.all(
        ids.map((id) =>
          params.environment.contracts.governor?.read.proposals([id]),
        ),
      );

      const proposalStateCall = Promise.all(
        ids.map((id) =>
          params.environment.contracts.governor?.read.state([id]),
        ),
      );

      const [proposalsData, proposalsState] = await Promise.all([
        proposalDataCall,
        proposalStateCall,
      ]);

      const proposals = proposalsData?.map((item, index: number) => {
        const state = proposalsState?.[index]!;

        const [
          id,
          proposer,
          eta,
          startTimestamp,
          endTimestamp,
          startBlock,
          forVotes,
          againstVotes,
          abstainVotes,
          totalVotes,
          canceled,
          executed,
        ] = item!;

        const proposal: Proposal = {
          chainId: params.environment.chainId,
          id: Number(id),
          proposalId: Number(id),
          proposer,
          eta: Number(eta),
          startTimestamp: Number(startTimestamp),
          endTimestamp: Number(endTimestamp),
          startBlock: Number(startBlock),
          forVotes: new Amount(forVotes, 18),
          againstVotes: new Amount(againstVotes, 18),
          abstainVotes: new Amount(abstainVotes, 18),
          totalVotes: new Amount(totalVotes, 18),
          canceled,
          executed,
          quorum: new Amount(quorum, 18),
          state,
        };

        return proposal;
      });

      return proposals;
    } else {
      return [];
    }
  } catch (error) {
    console.warn(
      `[getProposalData] RPC failed for chain ${params.environment.chainId}:`,
      error,
    );
    params.environment.onError?.(error, {
      source: "governance-proposals",
      chainId: params.environment.chainId,
    });
    return [];
  }
};

/**
 * Get cross-chain proposal data (Ponder-based, for Moonriver)
 */
export const getCrossChainProposalData = async (params: {
  environment: Environment;
  id?: number;
}) => {
  try {
    if (params.environment.contracts.governor) {
      const xcGovernanceSettings = params.environment.custom.governance;
      if (
        params.environment.contracts.multichainGovernor &&
        xcGovernanceSettings &&
        xcGovernanceSettings.chainIds.length > 0
      ) {
        const xcEnvironments = xcGovernanceSettings.chainIds
          .map((chainId) =>
            (Object.values(publicEnvironments) as Environment[]).find(
              (env) => env.chainId === chainId,
            ),
          )
          .filter((xcEnvironment) => !!xcEnvironment)
          .filter(
            (xcEnvironment) =>
              xcEnvironment!.custom?.wormhole?.chainId &&
              xcEnvironment!.contracts.voteCollector,
          );

        const [xcCount, xcQuorum] = await Promise.all([
          params.environment.contracts.multichainGovernor.read.proposalCount(),
          params.environment.contracts.multichainGovernor.read.quorum(),
        ]);

        if (params.id) {
          params.id =
            Number(params.id) -
            (params.environment.custom?.governance?.proposalIdOffset || 0);

          if (params.id < 0) return [];
          if (BigInt(params.id) > xcCount) return [];
        }

        const ids = params.id
          ? [BigInt(params.id)]
          : Array.from(
              { length: Number(xcCount) },
              (_, i) => xcCount - BigInt(i),
            );

        const xcProposalsDataCall = Promise.all(
          ids.map((id) =>
            params.environment.contracts.multichainGovernor?.read.proposals([
              id,
            ]),
          ),
        );

        const xcProposalsStateCall = Promise.all(
          ids.map((id) =>
            params.environment.contracts.multichainGovernor?.read.state([id]),
          ),
        );

        const xcVotesCall = Promise.all(
          xcEnvironments.map((xcEnvironment) =>
            Promise.all(
              ids.map((id) =>
                params.environment.contracts.multichainGovernor?.read.chainVoteCollectorVotes(
                  [(xcEnvironment!.custom as any).wormhole.chainId, id],
                ),
              ),
            ),
          ),
        );

        const [xcProposalsData, xcProposalsState, xcVotes] = await Promise.all([
          xcProposalsDataCall,
          xcProposalsStateCall,
          xcVotesCall,
        ]);

        const proposals = ids.map((xcId, proposalIndex: number) => {
          const state = xcProposalsState?.[proposalIndex]!;
          const id =
            Number(xcId) +
            (params.environment.custom?.governance?.proposalIdOffset || 0);

          const votesCollected = false;

          const votes = xcVotes.reduce(
            (prevVotes, currVotes) => {
              const voteData = currVotes[proposalIndex];
              if (!voteData) {
                return prevVotes;
              }

              // chainVoteCollectorVotes returns [forVotes, againstVotes, abstainVotes]
              const forVotes = voteData[0] || 0n;
              const againstVotes = voteData[1] || 0n;
              const abstainVotes = voteData[2] || 0n;
              const totalVotes = forVotes + againstVotes + abstainVotes;

              return {
                totalVotes: prevVotes.totalVotes + totalVotes,
                forVotes: prevVotes.forVotes + forVotes,
                againstVotes: prevVotes.againstVotes + againstVotes,
                abstainVotes: prevVotes.abstainVotes + abstainVotes,
              };
            },
            {
              totalVotes: 0n,
              forVotes: 0n,
              againstVotes: 0n,
              abstainVotes: 0n,
            },
          );

          const proposalData = xcProposalsData?.[proposalIndex];
          if (!proposalData) {
            throw new Error(
              `Proposal data not found for index ${proposalIndex}`,
            );
          }

          const [
            proposer,
            _voteSnapshotTimestamp,
            votingStartTime,
            votingEndTime,
            crossChainVoteCollectionEndTimestamp,
            voteSnapshotBlock,
            proposalForVotes,
            proposalAgainstVotes,
            proposalAbstainVotes,
            proposalTotalVotes,
            canceled,
            executed,
          ] = proposalData;

          const multichainState = (
            MultichainProposalStateMapping as { [key: number]: ProposalState }
          )[state]!;

          const proposal: Proposal = {
            chainId: params.environment.chainId,
            id,
            proposalId: Number(xcId),
            proposer,
            eta: Number(crossChainVoteCollectionEndTimestamp),
            startTimestamp: Number(votingStartTime),
            endTimestamp: Number(votingEndTime),
            startBlock: Number(voteSnapshotBlock),
            forVotes: new Amount(proposalForVotes + votes.forVotes, 18),
            againstVotes: new Amount(
              proposalAgainstVotes + votes.againstVotes,
              18,
            ),
            abstainVotes: new Amount(
              proposalAbstainVotes + votes.abstainVotes,
              18,
            ),
            totalVotes: new Amount(proposalTotalVotes + votes.totalVotes, 18),
            canceled,
            executed,
            quorum: new Amount(xcQuorum, 18),
            state: multichainState,
            multichain: {
              id: Number(xcId),
              votesCollected,
            },
          };

          return proposal;
        });

        return proposals;
      } else {
        return [];
      }
    } else {
      return [];
    }
  } catch (error) {
    console.warn(
      `[getCrossChainProposalData] RPC failed for chain ${params.environment.chainId}:`,
      error,
    );
    params.environment.onError?.(error, {
      source: "governance-proposals",
      chainId: params.environment.chainId,
    });
    return [];
  }
};

export const appendProposalExtendedData = (
  proposals: Proposal[],
  extendedDatas: PonderExtendedProposalData[],
) => {
  proposals.forEach((proposal) => {
    const extendedData = extendedDatas.find(
      (item) => item.id === proposal.proposalId,
    );

    if (extendedData) {
      proposal.title = extendedData.title;
      proposal.calldatas = extendedData.calldatas;
      proposal.description = extendedData.description;
      proposal.signatures = extendedData.signatures;
      proposal.stateChanges = extendedData.stateChanges.map((change) => ({
        blockNumber: change.blockNumber,
        transactionHash: change.transactionHash,
        state: change.state,
        chainId: proposal.chainId,
      }));
      proposal.subtitle = extendedData.subtitle;
      proposal.targets = extendedData.targets;
    }
  });
};

export const getExtendedProposalData = async (params: {
  environment: Environment;
  id?: number;
}): Promise<PonderExtendedProposalData[]> => {
  const result: PonderExtendedProposalData[] = [];
  let lastId = -1;
  let shouldContinue = true;
  const MAX_PAGES = 100;
  let page = 0;

  try {
    while (shouldContinue && page < MAX_PAGES) {
      page++;
      const response = await postWithRetry<{
        data: {
          proposals: {
            items: {
              proposalId: number;
              description: string;
              targets: string[];
              calldatas: string[];
              signatures: string[];
              stateChanges: {
                items: {
                  txnHash: string;
                  blockNumber: number;
                  newState: string;
                }[];
              };
            }[];
          };
        };
      }>("https://ponder-eu2.moonwell.fi", {
        query: `
            query {
              proposals(
                limit: 1000,
                orderDirection: "desc",
                orderBy: "proposalId",
                where: {
                  chainId: ${params.environment.chainId}
                  ${params.id ? `, proposalId: ${params.id}` : lastId >= 0 ? `, proposalId_lt: ${lastId}` : ""}
                }
              ) {
                items {
                  id
                  proposalId
                  description
                  targets
                  calldatas
                  signatures
                  stateChanges(orderBy: "blockNumber") {
                    items {
                      txnHash
                      blockNumber
                      newState
                    }
                  }
                }
              }
            }
          `,
      });

      if (response.status === 200 && response.data?.data?.proposals) {
        const proposals = response.data.data.proposals.items.map((item) => {
          const extendedProposalData: PonderExtendedProposalData = {
            id: item.proposalId,
            title: `Proposal #${item.proposalId}`,
            subtitle: extractProposalSubtitle(item.description),
            description: item.description,
            calldatas: item.calldatas,
            signatures: item.signatures,
            stateChanges: item.stateChanges.items.map((change) => {
              return {
                blockNumber: change.blockNumber,
                state: change.newState,
                transactionHash: change.txnHash,
              };
            }),
            targets: item.targets,
          };

          return extendedProposalData;
        });

        if (proposals.length < 1000 || proposals.length === 0) {
          shouldContinue = false;
        } else {
          lastId = last(proposals)!.id;
        }

        result.push(...proposals);
      } else {
        shouldContinue = false;
      }
    }
  } catch (error) {
    console.warn(
      `[getExtendedProposalData] Ponder failed for chain ${params.environment.chainId}:`,
      error,
    );
    params.environment.onError?.(error, {
      source: "governance-proposals",
      chainId: params.environment.chainId,
    });
    return result;
  }

  return result;
};
