// Copyright 2017-2025 @pezkuwi/types authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { AnyJson, AnyTuple, Codec } from '@pezkuwi/types-codec/types';
import type { AllHashers } from '../interfaces/metadata/definitions.js';
import type { StorageEntryMetadataLatest, StorageHasher } from '../interfaces/metadata/index.js';
import type { SiLookupTypeId } from '../interfaces/scaleInfo/index.js';
import type { IStorageKey, Registry } from '../types/index.js';
import type { StorageEntry } from './types.js';

import { Bytes } from '@pezkuwi/types-codec';
import { isFunction, isString, isU8a } from '@pezkuwi/util';

import { getSiName } from '../metadata/util/index.js';
import { unwrapStorageType } from '../util/index.js';

interface Decoded {
  key?: Uint8Array | string | undefined;
  method?: string | undefined;
  section?: string | undefined;
}

interface StorageKeyExtra {
  method: string;
  section: string;
}

// hasher type -> [initialHashLength, canDecodeKey]
const HASHER_MAP: Record<keyof typeof AllHashers, [number, boolean]> = {
  // opaque
  Blake2_128: [16, false], // eslint-disable-line camelcase
  Blake2_128Concat: [16, true], // eslint-disable-line camelcase
  Blake2_256: [32, false], // eslint-disable-line camelcase
  Identity: [0, true],
  Twox128: [16, false],
  Twox256: [32, false],
  Twox64Concat: [8, true]
};

/** @internal */
function decodeStorageKey (value?: string | Uint8Array | StorageKey | StorageEntry | [StorageEntry, unknown[]?]): Decoded {
  if (isU8a(value) || !value || isString(value)) {
    // let Bytes handle these inputs
    return { key: value };
  } else if (value instanceof StorageKey) {
    return {
      key: value,
      method: value.method,
      section: value.section
    };
  } else if (isFunction(value)) {
    return {
      key: value(),
      method: value.method,
      section: value.section
    };
  } else if (Array.isArray(value)) {
    const [fn, args = []] = value;

    if (!isFunction(fn)) {
      throw new Error('Expected function input for key construction');
    }

    if (fn.meta && fn.meta.type.isMap) {
      const map = fn.meta.type.asMap;

      if (!Array.isArray(args) || args.length !== map.hashers.length) {
        throw new Error(`Expected an array of ${map.hashers.length} values as params to a Map query`);
      }
    }

    return {
      key: fn(...args),
      method: fn.method,
      section: fn.section
    };
  }

  throw new Error(`Unable to convert input ${value as string} to StorageKey`);
}

/** @internal */
function decodeHashers <A extends AnyTuple> (registry: Registry, value: Uint8Array, hashers: [StorageHasher, SiLookupTypeId][]): A {
  // the storage entry is xxhashAsU8a(prefix, 128) + xxhashAsU8a(method, 128), 256 bits total
  let offset = 32;
  const count = hashers.length;
  const result = new Array<Codec>(count);

  for (let i = 0; i < count; i++) {
    const [hasher, type] = hashers[i];
    const [hashLen, canDecode] = HASHER_MAP[hasher.type as 'Identity'];
    const decoded = canDecode
      ? registry.createTypeUnsafe(getSiName(registry.lookup, type), [value.subarray(offset + hashLen)])
      : registry.createTypeUnsafe('Raw', [value.subarray(offset, offset + hashLen)]);

    offset += hashLen + (canDecode ? decoded.encodedLength : 0);
    result[i] = decoded;
  }

  return result as A;
}

/** @internal */
function decodeArgsFromMeta <A extends AnyTuple> (registry: Registry, value: Uint8Array, meta?: StorageEntryMetadataLatest): A {
  if (!meta || !meta.type.isMap) {
    return [] as unknown as A;
  }

  const { hashers, key } = meta.type.asMap;
  const keys = hashers.length === 1
    ? [key]
    : registry.lookup.getSiType(key).def.asTuple;

  return decodeHashers(registry, value, hashers.map((h, i) => [h, keys[i]]));
}

/** @internal */
function getMeta (value: StorageKey | StorageEntry | [StorageEntry, unknown[]?]): StorageEntryMetadataLatest | undefined {
  if (value instanceof StorageKey) {
    return value.meta;
  } else if (isFunction(value)) {
    return value.meta;
  } else if (Array.isArray(value)) {
    const [fn] = value;

    return fn.meta;
  }

  return undefined;
}

/** @internal */
function getType (registry: Registry, value: StorageKey | StorageEntry | [StorageEntry, unknown[]?]): string {
  if (value instanceof StorageKey) {
    return value.outputType;
  } else if (isFunction(value)) {
    return unwrapStorageType(registry, value.meta.type);
  } else if (Array.isArray(value)) {
    const [fn] = value;

    if (fn.meta) {
      return unwrapStorageType(registry, fn.meta.type);
    }
  }

  // If we have no type set, default to Raw
  return 'Raw';
}

/**
 * @name StorageKey
 * @description
 * A representation of a storage key (typically hashed) in the system. It can be
 * constructed by passing in a raw key or a StorageEntry with (optional) arguments.
 */
export class StorageKey<A extends AnyTuple = AnyTuple> extends Bytes implements IStorageKey<A> {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore This is assigned via this.decodeArgsFromMeta()
  #args: A;
  #meta?: StorageEntryMetadataLatest | undefined;
  #outputType: string;
  #method?: string | undefined;
  #section?: string | undefined;

  constructor (registry: Registry, value?: string | Uint8Array | StorageKey | StorageEntry | [StorageEntry, unknown[]?], override: Partial<StorageKeyExtra> = {}) {
    const { key, method, section } = decodeStorageKey(value);

    super(registry, key);

    this.#outputType = getType(registry, value as StorageKey);

    // decode the args (as applicable based on the key and the hashers, after all init)
    this.setMeta(getMeta(value as StorageKey), override.section || section, override.method || method);
  }

  /**
   * @description Return the decoded arguments (applicable to map with decodable values)
   */
  public get args (): A {
    return this.#args;
  }

  /**
   * @description The metadata or `undefined` when not available
   */
  public get meta (): StorageEntryMetadataLatest | undefined {
    return this.#meta;
  }

  /**
   * @description The key method or `undefined` when not specified
   */
  public get method (): string | undefined {
    return this.#method;
  }

  /**
   * @description The output type
   */
  public get outputType (): string {
    return this.#outputType;
  }

  /**
   * @description The key section or `undefined` when not specified
   */
  public get section (): string | undefined {
    return this.#section;
  }

  public is (key: IStorageKey<AnyTuple>): key is IStorageKey<A> {
    return key.section === this.section && key.method === this.method;
  }

  /**
   * @description Sets the meta for this key
   */
  public setMeta (meta?: StorageEntryMetadataLatest, section?: string, method?: string): this {
    this.#meta = meta;
    this.#method = method || this.#method;
    this.#section = section || this.#section;

    if (meta) {
      this.#outputType = unwrapStorageType(this.registry, meta.type);
    }

    try {
      this.#args = decodeArgsFromMeta(this.registry, this.toU8a(true), meta);
    } catch {
      // ignore...
    }

    return this;
  }

  /**
   * @description Returns the Human representation for this type
   */
  public override toHuman (_isExtended?: boolean, disableAscii?: boolean): AnyJson {
    return this.#args.length
      ? this.#args.map((a) => a.toHuman(undefined, disableAscii))
      : super.toHuman(undefined, disableAscii);
  }

  /**
   * @description Returns the raw type for this
   */
  public override toRawType (): string {
    return 'StorageKey';
  }
}
