/**
 * Deterministic EIP-712 typed-data construction and hashing for the
 * V2 -> V3 migration intent.
 *
 * Hard rules enforced here:
 *  - Pure functions. No clock, no randomness, no network, no wallet.
 *  - Nothing in this module can sign or broadcast anything: no injected
 *    provider, no wallet RPC method, no HTTP client, no transport of any kind
 *    is referenced anywhere in this file.
 *  - Output is byte-for-byte reproducible for the same selection.
 */

import { keccak_256 } from "@noble/hashes/sha3";
import {
  BASE_CHAIN_ID,
  EIP712_DOMAIN_NAME,
  EIP712_DOMAIN_VERSION,
  SIMULATED_DEADLINE,
  SIMULATED_NONCE,
  SIMULATED_OWNER,
  SIMULATED_VERIFYING_CONTRACT,
  eligibleBaseUnits,
  type V2Position,
} from "../data/simulation";

export type TypedDataField = { name: string; type: string };

export type MigrationTypedData = {
  domain: {
    name: string;
    version: string;
    chainId: number;
    verifyingContract: string;
  };
  primaryType: "MigrationIntent";
  types: Record<string, TypedDataField[]>;
  message: {
    owner: string;
    chainId: string;
    keepV2Live: boolean;
    positions: PositionMigrationMessage[];
    nonce: string;
    deadline: string;
  };
};

export type PositionMigrationMessage = {
  positionId: string;
  v2Contract: string;
  v3Target: string;
  assetSymbol: string;
  amount: string;
  cap: string;
};

export const MIGRATION_TYPES: Record<string, TypedDataField[]> = {
  EIP712Domain: [
    { name: "name", type: "string" },
    { name: "version", type: "string" },
    { name: "chainId", type: "uint256" },
    { name: "verifyingContract", type: "address" },
  ],
  MigrationIntent: [
    { name: "owner", type: "address" },
    { name: "chainId", type: "uint256" },
    { name: "keepV2Live", type: "bool" },
    { name: "positions", type: "PositionMigration[]" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" },
  ],
  PositionMigration: [
    { name: "positionId", type: "bytes32" },
    { name: "v2Contract", type: "address" },
    { name: "v3Target", type: "address" },
    { name: "assetSymbol", type: "string" },
    { name: "amount", type: "uint256" },
    { name: "cap", type: "uint256" },
  ],
};

/**
 * Build the exact typed-data payload for the opted-in positions.
 * Selection order is normalised (ascending `positionId`) so the payload does
 * not depend on the order the user clicked checkboxes.
 */
export function buildMigrationTypedData(
  selected: V2Position[],
): MigrationTypedData {
  const positions: PositionMigrationMessage[] = selected
    .filter((p) => p.v3 !== null)
    .map((p) => ({
      positionId: p.positionId,
      v2Contract: p.v2Address,
      v3Target: p.v3!.address,
      assetSymbol: p.assetSymbol,
      amount: eligibleBaseUnits(p),
      cap: p.v3!.capBaseUnits,
    }))
    .sort((a, b) => (a.positionId < b.positionId ? -1 : a.positionId > b.positionId ? 1 : 0));

  return {
    domain: {
      name: EIP712_DOMAIN_NAME,
      version: EIP712_DOMAIN_VERSION,
      chainId: BASE_CHAIN_ID,
      verifyingContract: SIMULATED_VERIFYING_CONTRACT,
    },
    primaryType: "MigrationIntent",
    types: MIGRATION_TYPES,
    message: {
      owner: SIMULATED_OWNER,
      chainId: String(BASE_CHAIN_ID),
      // V2 always remains live. This field is a constant, not a user choice.
      keepV2Live: true,
      positions,
      nonce: SIMULATED_NONCE,
      deadline: SIMULATED_DEADLINE,
    },
  };
}

/** Canonical, stable JSON rendering used by the review screen and by tests. */
export function typedDataJson(data: MigrationTypedData): string {
  return JSON.stringify(data, null, 2);
}

/* ------------------------------------------------------------------ */
/* Minimal EIP-712 hashing (keccak256 + ABI-encode of static words)    */
/* ------------------------------------------------------------------ */

function toHex(bytes: Uint8Array): string {
  return `0x${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
}

function fromHex(hex: string): Uint8Array {
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
  const out = new Uint8Array(clean.length / 2);
  for (let i = 0; i < out.length; i += 1) {
    out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
  }
  return out;
}

function concat(chunks: Uint8Array[]): Uint8Array {
  const total = chunks.reduce((n, c) => n + c.length, 0);
  const out = new Uint8Array(total);
  let off = 0;
  for (const c of chunks) {
    out.set(c, off);
    off += c.length;
  }
  return out;
}

export function keccakHex(bytes: Uint8Array): string {
  return toHex(keccak_256(bytes));
}

function utf8(value: string): Uint8Array {
  return new TextEncoder().encode(value);
}

function word(value: bigint): Uint8Array {
  if (value < 0n) throw new Error("negative values are not encodable");
  const out = new Uint8Array(32);
  let v = value;
  for (let i = 31; i >= 0 && v > 0n; i -= 1) {
    out[i] = Number(v & 0xffn);
    v >>= 8n;
  }
  return out;
}

function addressWord(value: string): Uint8Array {
  const b = fromHex(value.toLowerCase());
  if (b.length !== 20) throw new Error(`bad address: ${value}`);
  const out = new Uint8Array(32);
  out.set(b, 12);
  return out;
}

function bytes32Word(value: string): Uint8Array {
  const b = fromHex(value);
  if (b.length !== 32) throw new Error(`bad bytes32: ${value}`);
  return b;
}

/** `encodeType` per EIP-712: primary type first, then referenced types sorted. */
export function encodeType(
  primaryType: string,
  types: Record<string, TypedDataField[]>,
): string {
  const deps = new Set<string>();
  const walk = (t: string) => {
    const base = t.replace(/\[\d*\]$/, "");
    if (!types[base] || deps.has(base) || base === "EIP712Domain") return;
    deps.add(base);
    for (const f of types[base]) walk(f.type);
  };
  walk(primaryType);
  deps.delete(primaryType);
  const ordered = [primaryType, ...Array.from(deps).sort()];
  return ordered
    .map((t) => `${t}(${types[t].map((f) => `${f.type} ${f.name}`).join(",")})`)
    .join("");
}

export function typeHash(
  primaryType: string,
  types: Record<string, TypedDataField[]>,
): string {
  return keccakHex(utf8(encodeType(primaryType, types)));
}

function encodeValue(
  type: string,
  value: unknown,
  types: Record<string, TypedDataField[]>,
): Uint8Array {
  if (type.endsWith("]")) {
    const inner = type.replace(/\[\d*\]$/, "");
    const items = value as unknown[];
    return keccak_256(concat(items.map((i) => encodeValue(inner, i, types))));
  }
  if (type === "string") return keccak_256(utf8(value as string));
  if (type === "bytes") return keccak_256(fromHex(value as string));
  if (type === "address") return addressWord(value as string);
  if (type === "bool") return word(value ? 1n : 0n);
  if (type === "bytes32") return bytes32Word(value as string);
  if (/^uint\d+$/.test(type)) return word(BigInt(value as string | number));
  if (types[type]) return fromHex(hashStruct(type, value as Record<string, unknown>, types));
  throw new Error(`unsupported type: ${type}`);
}

export function hashStruct(
  primaryType: string,
  data: Record<string, unknown>,
  types: Record<string, TypedDataField[]>,
): string {
  const parts: Uint8Array[] = [fromHex(typeHash(primaryType, types))];
  for (const field of types[primaryType]) {
    parts.push(encodeValue(field.type, data[field.name], types));
  }
  return keccakHex(concat(parts));
}

export function domainSeparator(data: MigrationTypedData): string {
  return hashStruct("EIP712Domain", data.domain as unknown as Record<string, unknown>, data.types);
}

/** The `\x19\x01`-prefixed digest a wallet would be asked to sign. */
export function typedDataDigest(data: MigrationTypedData): string {
  const prefix = new Uint8Array([0x19, 0x01]);
  return keccakHex(
    concat([
      prefix,
      fromHex(domainSeparator(data)),
      fromHex(hashStruct(data.primaryType, data.message as unknown as Record<string, unknown>, data.types)),
    ]),
  );
}

/**
 * A clearly-labelled fake signature, derived deterministically from the digest
 * so QA output is stable. This is NOT a signature: no key exists, it cannot
 * recover to any address, and nothing is sent anywhere.
 */
export function simulatedSignature(digest: string): string {
  const r = keccakHex(utf8(`simulation:r:${digest}`)).slice(2);
  const s = keccakHex(utf8(`simulation:s:${digest}`)).slice(2, 2 + 64 - 2) + "00";
  return `0x${r}${s}1b`;
}
