import { LEGO_API, SOLANA_CHAIN_ID } from '@shogun-sdk/money-legos';

/**
 * Response data structure returned by the Lego API
 */
interface IResponseData {
  steps: IStep[];
  details: IDetails;
  fees: IFees;
}

/**
 * Transaction details including sender, recipient and token information
 */
interface IDetails {
  sender: string;
  recipient: string;
  tokenIn: ITokenIn;
}

/**
 * Token information including amount, chain, and metadata
 */
interface ITokenIn {
  amountUsd: string;
  chainId: number;
  address: string;
  symbol: string;
  name: string;
  decimals: number;
  amount?: string;
}

/**
 * Fee information for transactions
 */
interface IFees {
  gas: ITokenIn;
}

/**
 * Individual transaction step with blockchain interaction details
 */
interface IStep {
  from: string;
  to: string;
  data: string;
  value: string;
  chainId: number;
  gas: string;
  maxFeePerGas: string;
  maxPriorityFeePerGas: string;
}

/**
 * Represents a single NFT item with its contract address and token ID
 */
interface INFTItem {
  address: string;
  tokenId: string;
}

/**
 * Represents the token being used for the purchase
 */
interface IToken {
  address: string;
  decimals: number;
  chainId: number;
}

/**
 * Input parameters required for fetching Lego data
 */
interface IFetchLegoProps {
  items: INFTItem[];
  token: IToken;
  userAddress: string;
  recipientAddress?: string;
}

/**
 * Output structure returned by the MagicEden.SwapForNFT function
 */
interface ILegoResult {
  status: boolean;
  error?: string | null;
  data?: IResponseData | null;
  refetch: () => Promise<ILegoResult>;
}

/**
 * Error structure returned by the API
 */
interface IApiError {
  statusCode: number;
  message: string;
  error: string;
}

/**
 * Configuration options for the LegoClient
 */
interface ILegoClientConfig {
  /**
   * API key for authentication
   */
  apiKey: string;
}

/**
 * Client for interacting with the Lego API
 *
 * @example
 * ```typescript
 * const legoClient = new LegoClient({ apiKey: 'your-api-key' });
 * const result = await legoClient.MagicEden.SwapForNFT({ items, token, userAddress, recipientAddress });
 * const updatedResult = await result.refetch();
 * ```
 */
export class LegoClient {
  private apiKey: string;
  private activeControllers: Map<string, AbortController> = new Map();

  /**
   * Creates a new LegoClient instance
   *
   * @param {LegoClientConfig} config - Configuration options including API key
   */
  constructor(config: ILegoClientConfig) {
    this.apiKey = config.apiKey;
  }

  /**
   * Generates a unique request ID for tracking purposes
   *
   * @param {IFetchLegoProps} props - The fetch parameters
   * @returns {string} - A unique identifier for this request
   * @private
   */
  private generateRequestId(props: IFetchLegoProps): string {
    const { items, token, userAddress } = props;
    const itemsStr = items.map((item) => `${item.address}-${item.tokenId}`).join('|');
    return `${userAddress}-${token.address}-${itemsStr}`;
  }

  /**
   * MagicEden marketplace operations
   */
  MagicEden = {
    /**
     * Swaps tokens for NFTs on MagicEden with support for refetching
     *
     * @param {IFetchLegoProps} props - Input parameters containing NFT items, token, and user address
     * @returns {Promise<ILegoResult>} - Result containing status, data, error message, and refetch function
     */
    SwapForNFT: async (props: IFetchLegoProps): Promise<ILegoResult> => {
      const { items, token, userAddress, recipientAddress } = props;
      const requestId = this.generateRequestId(props);

      // Create refetch function that will be returned with the result
      const refetch = async (): Promise<ILegoResult> => {
        return this.MagicEden.SwapForNFT(props);
      };

      // ✅ Validate required parameters
      if (!items.length || !token.address || !userAddress) {
        return {
          status: false,
          error: 'Invalid parameters provided',
          data: null,
          refetch,
        };
      }

      const isSolanaTokenIn = token.chainId === SOLANA_CHAIN_ID;

      // ✅ Solana requires recipient address
      if (isSolanaTokenIn && !recipientAddress) {
        return {
          status: false,
          error: 'Recipient address is required for Solana transactions.',
          data: null,
          refetch,
        };
      }

      // Cancel any ongoing request with the same ID
      if (this.activeControllers.has(requestId)) {
        this.activeControllers.get(requestId)?.abort();
        this.activeControllers.delete(requestId);
      }

      // Create a new AbortController for this request
      const controller = new AbortController();
      this.activeControllers.set(requestId, controller);

      try {
        // ✅ Construct the request payload conditionally
        const requestBody = JSON.stringify({
          tokenIn: {
            address: token.address,
            decimals: token.decimals,
            chainId: token.chainId,
          },
          nft: items,
          address: userAddress,
          ...(isSolanaTokenIn || recipientAddress ? { recipient: recipientAddress } : {}),
        });

        // ✅ Send API request
        const response = await fetch(`${LEGO_API}/nft/purchase`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-api-key': this.apiKey,
          },
          body: requestBody,
          signal: controller.signal,
        });

        // Clean up controller reference after request
        this.activeControllers.delete(requestId);

        // ✅ Handle non-200 responses
        if (!response.ok) {
          const errorData: IApiError = await response.json();
          throw new Error(errorData.message || response.statusText);
        }

        // ✅ Parse response
        const result = await response.json();

        // ✅ Return successful result
        return {
          status: !!result?.data?.execute,
          error: null,
          data: result?.data?.execute || null,
          refetch,
        };
      } catch (err) {
        // Clean up controller reference
        this.activeControllers.delete(requestId);

        // ✅ Ignore abort errors
        if (err instanceof DOMException && err.name === 'AbortError') {
          return {
            status: false,
            error: 'Request aborted',
            data: null,
            refetch,
          };
        }

        // ✅ Return error result
        return {
          status: false,
          error: err instanceof Error ? err.message : 'Unknown error',
          data: null,
          refetch,
        };
      }
    },
  };
}
