import { getAddress, keccak256, recoverMessageAddress, sha256, toHex, type Address, type Hex } from "viem";
import { z } from "zod";

export const agentEventTypes = [
  "trend.detected",
  "model.decision",
  "launch.submitted",
  "launch.confirmed",
  "fees.claimed",
  "buyback.submitted",
  "buyback.confirmed",
  "agent.paused",
  "agent.resumed",
] as const;

export const agentEventStatuses = ["observed", "proposed", "submitted", "confirmed", "failed"] as const;

export const unsignedAgentEventSchema = z.object({
  version: z.literal(1),
  eventId: z.string().uuid(),
  agentId: z.string().min(3).max(64).regex(/^[a-z0-9][a-z0-9-]*$/),
  sequence: z.number().int().nonnegative(),
  type: z.enum(agentEventTypes),
  status: z.enum(agentEventStatuses),
  occurredAt: z.string().datetime({ offset: true }),
  chainId: z.literal(4663),
  txHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/).optional(),
  payload: z.record(z.string(), z.unknown()),
});

export const signedAgentEventSchema = unsignedAgentEventSchema.extend({
  signer: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
  signature: z.string().regex(/^0x[a-fA-F0-9]{130}$/),
});

export type UnsignedAgentEvent = z.infer<typeof unsignedAgentEventSchema>;
export type SignedAgentEvent = z.infer<typeof signedAgentEventSchema>;

const sortValue = (value: unknown): unknown => {
  if (Array.isArray(value)) return value.map(sortValue);
  if (value !== null && typeof value === "object") {
    return Object.fromEntries(
      Object.entries(value as Record<string, unknown>)
        .sort(([left], [right]) => left.localeCompare(right))
        .map(([key, nested]) => [key, sortValue(nested)]),
    );
  }
  return value;
};

export const stableJson = (value: unknown) => JSON.stringify(sortValue(value));

export const payloadHash = (payload: Record<string, unknown>): Hex => keccak256(toHex(stableJson(payload)));

const eventSigningMessageWithDomain = (event: UnsignedAgentEvent, domain: string): string => {
  const parsed = unsignedAgentEventSchema.parse(event);
  return [
    domain,
    parsed.eventId,
    parsed.agentId,
    String(parsed.sequence),
    parsed.type,
    parsed.status,
    parsed.occurredAt,
    String(parsed.chainId),
    parsed.txHash?.toLowerCase() ?? "",
    payloadHash(parsed.payload),
  ].join("\n");
};

export const eventSigningMessage = (event: UnsignedAgentEvent): string =>
  eventSigningMessageWithDomain(event, "zoi-agent-event:v1");

export const verifySignedAgentEvent = async (input: unknown): Promise<SignedAgentEvent> => {
  const event = signedAgentEventSchema.parse(input);
  const { signer, signature, ...unsigned } = event;
  const messages = [
    eventSigningMessage(unsigned),
    eventSigningMessageWithDomain(unsigned, "pons-agent-event:v1"),
  ];
  const matches = await Promise.all(messages.map(async (message) => getAddress(await recoverMessageAddress({
    message,
    signature: signature as Hex,
  })) === getAddress(signer)));

  if (!matches.some(Boolean)) {
    throw new Error("Event signature does not match signer");
  }

  return { ...event, signer: getAddress(signer) as Address };
};

const tokenAssetIdentitySchema = z.object({
  name: z.string().trim().min(1).max(64),
  symbol: z.string().trim().min(1).max(16).regex(/^[A-Z0-9]+$/, "Symbol must contain only A-Z and 0-9"),
  topic: z.string().trim().min(1).max(280),
});

const tokenAssetBaseSchema = tokenAssetIdentitySchema.extend({
  assetId: z.string().regex(/^[a-f0-9]{64}$/),
  agentId: z.string().min(3).max(64).regex(/^[a-z0-9][a-z0-9-]*$/),
  createdAt: z.string().datetime({ offset: true }),
  chainId: z.literal(4663),
});

export const legacyUnsignedTokenAssetSchema = tokenAssetBaseSchema.extend({
  version: z.literal(1),
});

export const tokenAssetMediaSchema = z.object({
  mimeType: z.literal("image/png"),
  contentHash: z.string().regex(/^[a-f0-9]{64}$/),
  byteLength: z.number().int().min(1).max(4 * 1024 * 1024),
  width: z.literal(1024),
  height: z.literal(1024),
  model: z.string().min(1).max(64),
});

export const generatedUnsignedTokenAssetSchema = tokenAssetBaseSchema.extend({
  version: z.literal(2),
  media: tokenAssetMediaSchema,
});

export const unsignedTokenAssetSchema = z.discriminatedUnion("version", [
  legacyUnsignedTokenAssetSchema,
  generatedUnsignedTokenAssetSchema,
]);

const signatureFields = {
  signer: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
  signature: z.string().regex(/^0x[a-fA-F0-9]{130}$/),
};

export const legacySignedTokenAssetSchema = legacyUnsignedTokenAssetSchema.extend(signatureFields);
export const generatedSignedTokenAssetSchema = generatedUnsignedTokenAssetSchema.extend(signatureFields);
export const signedTokenAssetSchema = z.discriminatedUnion("version", [
  legacySignedTokenAssetSchema,
  generatedSignedTokenAssetSchema,
]);

export const tokenAssetUploadSchema = z.object({
  asset: generatedSignedTokenAssetSchema,
  imageBase64: z.string().min(1).max(6 * 1024 * 1024),
});

export type TokenAssetIdentity = z.infer<typeof tokenAssetIdentitySchema>;
export type UnsignedTokenAsset = z.infer<typeof unsignedTokenAssetSchema>;
export type SignedTokenAsset = z.infer<typeof signedTokenAssetSchema>;

export const tokenAssetId = (input: TokenAssetIdentity): string => {
  const identity = tokenAssetIdentitySchema.parse(input);
  return keccak256(toHex(stableJson(identity))).slice(2);
};

export const tokenAssetSigningMessage = (input: UnsignedTokenAsset): string => {
  const asset = unsignedTokenAssetSchema.parse(input);
  const signedPayload = asset.version === 2
    ? { name: asset.name, symbol: asset.symbol, topic: asset.topic, media: asset.media }
    : { name: asset.name, symbol: asset.symbol, topic: asset.topic };
  return [
    `zoi-agent-token-asset:v${asset.version}`,
    asset.assetId,
    asset.agentId,
    asset.createdAt,
    String(asset.chainId),
    payloadHash(signedPayload),
  ].join("\n");
};

export const verifySignedTokenAsset = async (input: unknown): Promise<SignedTokenAsset> => {
  const asset = signedTokenAssetSchema.parse(input);
  if (asset.assetId !== tokenAssetId(asset)) {
    throw new Error("Token asset ID does not match its identity");
  }

  const { signer, signature, ...unsigned } = asset;
  const recovered = await recoverMessageAddress({
    message: tokenAssetSigningMessage(unsigned),
    signature: signature as Hex,
  });
  if (getAddress(recovered) !== getAddress(signer)) {
    throw new Error("Token asset signature does not match signer");
  }

  return { ...asset, signer: getAddress(signer) as Address };
};

const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10] as const;

const readUint32 = (bytes: Uint8Array, offset: number) => (
  bytes[offset]! * 0x1000000
  + bytes[offset + 1]! * 0x10000
  + bytes[offset + 2]! * 0x100
  + bytes[offset + 3]!
);

export const validatePngImage = (bytes: Uint8Array) => {
  if (bytes.length < 57 || !pngSignature.every((value, index) => bytes[index] === value)) {
    throw new Error("Generated image is not a valid PNG");
  }

  let offset = 8;
  let width = 0;
  let height = 0;
  let sawHeader = false;
  let sawImageData = false;
  let sawEnd = false;
  while (offset + 12 <= bytes.length) {
    const length = readUint32(bytes, offset);
    if (length > bytes.length - offset - 12) throw new Error("PNG contains a truncated chunk");
    const type = String.fromCharCode(...bytes.slice(offset + 4, offset + 8));
    const dataOffset = offset + 8;

    if (!sawHeader) {
      if (type !== "IHDR" || length !== 13) throw new Error("PNG must begin with a valid IHDR chunk");
      width = readUint32(bytes, dataOffset);
      height = readUint32(bytes, dataOffset + 4);
      if (width < 1 || height < 1 || width > 4096 || height > 4096) throw new Error("PNG dimensions are invalid");
      if (bytes[dataOffset + 10] !== 0 || bytes[dataOffset + 11] !== 0 || bytes[dataOffset + 12] > 1) {
        throw new Error("PNG uses unsupported compression, filtering, or interlacing");
      }
      sawHeader = true;
    } else if (type === "IDAT" && length > 0) {
      sawImageData = true;
    } else if (type === "IEND") {
      if (length !== 0) throw new Error("PNG IEND chunk is invalid");
      sawEnd = true;
      offset += length + 12;
      break;
    }
    offset += length + 12;
  }

  if (!sawHeader || !sawImageData || !sawEnd || offset !== bytes.length) {
    throw new Error("PNG is missing required image chunks");
  }
  return { width, height };
};

export const imageContentHash = (bytes: Uint8Array): string => sha256(bytes).slice(2);

const escapeXml = (value: string) => value.replace(/[&<>"']/g, (character) => ({
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  "\"": "&quot;",
  "'": "&apos;",
})[character]!);

const logoPalettes = [
  { background: "#07171a", surface: "#0d292c", accent: "#56e3d4", ink: "#effffc" },
  { background: "#111827", surface: "#1f2937", accent: "#67e8f9", ink: "#f8fafc" },
  { background: "#101820", surface: "#1d2f3a", accent: "#7dd3fc", ink: "#f0f9ff" },
  { background: "#0b1612", surface: "#183128", accent: "#6ee7b7", ink: "#f0fdf4" },
  { background: "#17130b", surface: "#302817", accent: "#fde68a", ink: "#fffbeb" },
  { background: "#181012", surface: "#352025", accent: "#fda4af", ink: "#fff1f2" },
] as const;

export const renderTokenLogoSvg = (input: TokenAssetIdentity): string => {
  const identity = tokenAssetIdentitySchema.parse(input);
  const assetId = tokenAssetId(identity);
  const palette = logoPalettes[Number.parseInt(assetId.slice(0, 2), 16) % logoPalettes.length]!;
  const rotation = Number.parseInt(assetId.slice(2, 4), 16) % 90 - 45;
  const inset = 128 + Number.parseInt(assetId.slice(4, 6), 16) % 96;
  const orbit = 270 + Number.parseInt(assetId.slice(6, 8), 16) % 90;
  const symbolSize = Math.max(116, 300 - Math.max(0, identity.symbol.length - 4) * 17);
  const displayName = [...identity.name].slice(0, 34).join("");

  return [
    '<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img">',
    `<title>${escapeXml(identity.name)} (${escapeXml(identity.symbol)})</title>`,
    `<rect width="1024" height="1024" fill="${palette.background}"/>`,
    `<circle cx="512" cy="512" r="${orbit}" fill="none" stroke="${palette.accent}" stroke-width="2" opacity="0.48"/>`,
    `<rect x="${inset}" y="${inset}" width="${1024 - inset * 2}" height="${1024 - inset * 2}" rx="92" fill="${palette.surface}" transform="rotate(${rotation} 512 512)"/>`,
    `<circle cx="512" cy="512" r="226" fill="${palette.background}" stroke="${palette.accent}" stroke-width="8"/>`,
    `<path d="M512 238L548 326L642 330L570 390L594 482L512 432L430 482L454 390L382 330L476 326Z" fill="${palette.accent}" opacity="0.9"/>`,
    `<text x="512" y="610" text-anchor="middle" fill="${palette.ink}" font-family="Arial, Helvetica, sans-serif" font-size="${symbolSize}" font-weight="700" letter-spacing="0">${escapeXml(identity.symbol)}</text>`,
    `<text x="512" y="720" text-anchor="middle" fill="${palette.ink}" opacity="0.72" font-family="Arial, Helvetica, sans-serif" font-size="34" font-weight="600" letter-spacing="0">${escapeXml(displayName)}</text>`,
    `<text x="512" y="918" text-anchor="middle" fill="${palette.accent}" opacity="0.72" font-family="Arial, Helvetica, sans-serif" font-size="20" font-weight="700" letter-spacing="0">PONS / ROBINHOOD CHAIN</text>`,
    `<text x="512" y="952" text-anchor="middle" fill="${palette.ink}" opacity="0.38" font-family="monospace" font-size="16" letter-spacing="0">${assetId.slice(0, 16).toUpperCase()}</text>`,
    "</svg>",
  ].join("");
};
