import { nativeToScVal, xdr } from '@stellar/stellar-sdk';

import {
  detectBytesEncoding,
  logger,
  stringToBytes,
} from '@openzeppelin/contracts-ui-builder-utils';

// Import types for internal use
import type {
  SorobanArgumentValue,
  SorobanComplexValue,
  SorobanEnumValue,
  SorobanMapEntry,
} from '../transform/input-parser';
import { convertStellarTypeToScValType } from './formatting';

const SYSTEM_LOG_TAG = 'StellarInputParsingUtils';

// Re-export types for convenience
export type {
  SorobanArgumentValue,
  SorobanEnumValue,
  SorobanMapEntry,
  SorobanComplexValue,
} from '../transform/input-parser';

// ================================
// TYPE GUARDS AND UTILITIES
// ================================

export function isPrimitiveArgumentSet(args: Record<string, SorobanComplexValue>): boolean {
  return Object.values(args).every(
    (v) => typeof v === 'object' && v !== null && 'type' in v && 'value' in v
  );
}

export function isEnumArgumentSet(args: Record<string, SorobanComplexValue>): boolean {
  return Object.values(args).some(
    (v) => typeof v === 'object' && v !== null && ('tag' in v || 'enum' in v)
  );
}

export function isMapArray(argValue: unknown[]): boolean {
  try {
    return (
      Array.isArray(argValue) &&
      argValue.every((obj: unknown) => {
        if (typeof obj !== 'object' || obj === null) return false;

        const keys = Object.keys(obj);
        if (keys.length !== 2 || !keys.includes('0') || !keys.includes('1')) {
          return false;
        }

        const keyEntry = (obj as Record<string, unknown>)['0'];
        return (
          typeof keyEntry === 'object' &&
          keyEntry !== null &&
          'value' in keyEntry &&
          'type' in keyEntry
        );
      })
    );
  } catch {
    return false;
  }
}

export function isComplexObjectArray(argValue: unknown[]): boolean {
  return argValue.some(
    (v) =>
      typeof v === 'object' &&
      v !== null &&
      typeof Object.values(v as Record<string, unknown>)[0] === 'object'
  );
}

export function isPrimitiveArray(argValue: unknown[]): argValue is SorobanArgumentValue[] {
  if (argValue.length === 0) return false;

  // Check if all items are SorobanArgumentValue (not SorobanMapEntry)
  const allArePrimitives = argValue.every(
    (v) =>
      typeof v === 'object' &&
      v !== null &&
      'value' in v &&
      'type' in v &&
      !('0' in v) && // Not a map entry
      !('1' in v) // Not a map entry
  );

  if (!allArePrimitives) return false;

  const firstItem = argValue[0] as SorobanArgumentValue;
  return argValue.every((v) => {
    const item = v as SorobanArgumentValue;
    return item.type === firstItem.type;
  });
}

export function isTupleArray(argValue: unknown[]): argValue is SorobanArgumentValue[] {
  return argValue.every(
    (v: unknown) =>
      typeof v === 'object' &&
      v !== null &&
      'type' in v &&
      'value' in v &&
      !('0' in v) && // Not a map entry
      !('1' in v) // Not a map entry
  );
}

export function isObjectWithTypedValues(argValue: SorobanComplexValue): boolean {
  return (
    typeof argValue === 'object' &&
    argValue !== null &&
    !Array.isArray(argValue) &&
    Object.values(argValue).every(
      (v: unknown) => typeof v === 'object' && v !== null && 'type' in v && 'value' in v
    )
  );
}

export function isPrimitiveValue(argValue: SorobanComplexValue): boolean {
  return (
    typeof argValue === 'object' && argValue !== null && 'type' in argValue && 'value' in argValue
  );
}

// ================================
// SCVAL CONVERSION UTILITIES
// ================================

export function getScValFromPrimitive(v: SorobanArgumentValue): xdr.ScVal {
  try {
    if (v.type === 'bool') {
      const boolValue = typeof v.value === 'boolean' ? v.value : v.value === 'true';
      return nativeToScVal(boolValue);
    }

    if (v.type === 'bytes') {
      const stringValue = v.value as string;
      const encoding = detectBytesEncoding(stringValue);
      return nativeToScVal(stringToBytes(stringValue, encoding));
    }

    // Use our improved type conversion utility
    const typeHint = convertStellarTypeToScValType(v.type);
    return nativeToScVal(v.value, { type: typeHint });
  } catch (error) {
    logger.error(SYSTEM_LOG_TAG, `Failed to convert primitive ${v.type}:`, error);
    throw new Error(`Failed to convert primitive value of type ${v.type}: ${error}`);
  }
}

export function getScValFromArg(arg: SorobanComplexValue, scVals: xdr.ScVal[]): xdr.ScVal {
  // Handle array of arrays with numeric objects (nested structures)
  if (Array.isArray(arg) && arg.length > 0) {
    const arrayScVals = arg.map((subArray) => {
      if (Array.isArray(subArray) && isMapArray(subArray)) {
        const { mapVal, mapType } = convertObjectToMap(subArray as SorobanMapEntry[]);

        // Better map key-value pair handling
        const items = Object.keys(mapVal);
        if (items.length > 1) {
          items.forEach((item) => {
            const mapScVal = nativeToScVal(mapVal[item], {
              type: mapType[item],
            });
            scVals.push(mapScVal);
          });
        }

        return nativeToScVal(mapVal, { type: mapType });
      }
      return getScValFromArg(subArray as SorobanComplexValue, scVals);
    });

    return xdr.ScVal.scvVec(arrayScVals);
  }

  // For single values, handle based on type
  if (typeof arg === 'object' && arg !== null && 'type' in arg && 'value' in arg) {
    return getScValFromPrimitive(arg as SorobanArgumentValue);
  }

  // Fallback to nativeToScVal for simple values
  return nativeToScVal(arg);
}

export function convertEnumToScVal(obj: SorobanEnumValue, scVals?: xdr.ScVal[]): xdr.ScVal {
  try {
    // Integer variant - has enum property (keep as-is for integer enums)
    if (obj.enum !== undefined) {
      const enumScVal = nativeToScVal(obj.enum, { type: 'u32' });
      return enumScVal;
    }

    if (!obj.tag) {
      throw new Error('Enum object must have either "tag" or "enum" property');
    }

    // Use Vector format with Symbol for variant names (as per Soroban documentation)
    // Unit variants: ScVec containing single ScSymbol
    // Tuple variants: ScVec with ScSymbol + payload elements
    const tagSymbol = nativeToScVal(obj.tag, { type: 'symbol' });

    if (!obj.values || obj.values.length === 0) {
      // Unit variant - ScVec containing single ScSymbol
      const unitVec = xdr.ScVal.scvVec([tagSymbol]);
      return unitVec;
    }

    // Tuple variant - ScVec with ScSymbol + payload elements
    const valuesVal = obj.values.map((v) => getScValFromArg(v, scVals || []));
    const tupleVec = xdr.ScVal.scvVec([tagSymbol, ...valuesVal]);
    return tupleVec;
  } catch (error) {
    logger.error(SYSTEM_LOG_TAG, 'Failed to convert enum:', error);
    throw new Error(`Failed to convert enum: ${error}`);
  }
}

export function convertValuesToScVals(
  values: SorobanArgumentValue[],
  scVals: xdr.ScVal[]
): xdr.ScVal[] {
  return values.map((v) => getScValFromArg(v, scVals));
}

export function convertObjectToScVal(obj: Record<string, SorobanArgumentValue>): xdr.ScVal {
  try {
    const convertedValue: Record<string, unknown> = {};
    const typeHints: Record<string, string | string[]> = {};

    // Process each field in the object
    for (const key in obj) {
      const field = obj[key];

      if (field.type === 'bool') {
        convertedValue[key] =
          typeof field.value === 'boolean' ? field.value : field.value === 'true';
        typeHints[key] = ['symbol']; // Key is always symbol, value type varies
      } else {
        convertedValue[key] = field.value;
        const fieldTypeHint = convertStellarTypeToScValType(field.type);
        typeHints[key] = [
          'symbol',
          ...(Array.isArray(fieldTypeHint) ? fieldTypeHint : [fieldTypeHint]),
        ];
      }
    }

    return nativeToScVal(convertedValue, { type: typeHints });
  } catch (error) {
    logger.error(SYSTEM_LOG_TAG, 'Failed to convert object:', error);
    throw new Error(`Failed to convert object to ScVal: ${error}`);
  }
}

export function convertObjectToMap(mapArray: SorobanMapEntry[]): {
  mapVal: Record<string, unknown>;
  mapType: Record<string, string | string[]>;
} {
  try {
    const mapVal = mapArray.reduce((acc: Record<string, unknown>, pair) => {
      const key = pair['0'].value as string;

      if (Array.isArray(pair['1'])) {
        // Handle nested array values
        const valueScVal = getScValFromArg(pair['1'], []);
        acc[key] = valueScVal;
      } else {
        // Handle primitive values
        const value = pair['1'].value;
        acc[key] = pair['1'].type === 'bool' ? value === 'true' : value;
      }
      return acc;
    }, {});

    const mapType = mapArray.reduce((acc: Record<string, string[]>, pair) => {
      const key = pair['0'].value as string;
      const keyTypeHint = convertStellarTypeToScValType(pair['0'].type);
      const valueTypeHint = convertStellarTypeToScValType(pair['1'].type);
      acc[key] = [
        ...(Array.isArray(keyTypeHint) ? keyTypeHint : [keyTypeHint]),
        ...(Array.isArray(valueTypeHint) ? valueTypeHint : [valueTypeHint]),
      ];
      return acc;
    }, {});

    return { mapVal, mapType };
  } catch (error) {
    logger.error(SYSTEM_LOG_TAG, 'Failed to convert map:', error);
    throw new Error(`Failed to convert map: ${error}`);
  }
}

export function convertTupleToScVal(tupleArray: SorobanArgumentValue[]): xdr.ScVal {
  try {
    const tupleScVals = tupleArray.map((v) => {
      if (v.type === 'bool') {
        const boolValue = typeof v.value === 'boolean' ? v.value : v.value === 'true';
        return nativeToScVal(boolValue);
      }

      if (v.type === 'bytes') {
        const encoding = detectBytesEncoding(v.value as string);
        return nativeToScVal(stringToBytes(v.value as string, encoding));
      }

      const typeHint = convertStellarTypeToScValType(v.type);
      return nativeToScVal(v.value, { type: typeHint });
    });

    // JS SDK's nativeToScVal doesn't support mixed-type arrays, so use xdr.ScVal.scvVec
    return xdr.ScVal.scvVec(tupleScVals);
  } catch (error) {
    logger.error(SYSTEM_LOG_TAG, 'Failed to convert tuple:', error);
    throw new Error(`Failed to convert tuple: ${error}`);
  }
}
