import type { StoredAgent } from "./types";
import { PrivateKeySigner } from "./privateKeySigner";

export const AGENT_NAME = "hypurr-connect";

const AGENT_STORAGE_PREFIX = "hypurr-connect-agent";

function storageKey(masterAddress: string): string {
  return `${AGENT_STORAGE_PREFIX}:${masterAddress.toLowerCase()}`;
}

export function loadAgent(masterAddress: string): StoredAgent | null {
  try {
    const raw = localStorage.getItem(storageKey(masterAddress));
    return raw ? JSON.parse(raw) : null;
  } catch {
    return null;
  }
}

export function saveAgent(masterAddress: string, agent: StoredAgent): void {
  localStorage.setItem(storageKey(masterAddress), JSON.stringify(agent));
}

export function clearAgent(masterAddress: string): void {
  localStorage.removeItem(storageKey(masterAddress));
}

/** A locally-stored agent alongside the master address it was approved for. */
export interface LocalApprovedAgent extends StoredAgent {
  masterAddress: string;
}

/**
 * Enumerate every agent approved and stored locally on this device by scanning
 * localStorage for the agent storage prefix. Newest approvals first.
 */
export function loadAllAgents(): LocalApprovedAgent[] {
  const agents: LocalApprovedAgent[] = [];
  try {
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      if (!key || !key.startsWith(`${AGENT_STORAGE_PREFIX}:`)) continue;
      const raw = localStorage.getItem(key);
      if (!raw) continue;
      try {
        const agent = JSON.parse(raw) as StoredAgent;
        agents.push({
          ...agent,
          masterAddress: key.slice(key.indexOf(":") + 1),
        });
      } catch {
        // Skip malformed entries.
      }
    }
  } catch {
    return [];
  }
  return agents.sort((a, b) => b.approvedAt - a.approvedAt);
}

/**
 * Generate a random 32-byte private key and derive its address using the
 * local PrivateKeySigner compatibility wrapper.
 */
export async function generateAgentKey(): Promise<{
  privateKey: `0x${string}`;
  address: `0x${string}`;
}> {
  const bytes = crypto.getRandomValues(new Uint8Array(32));
  const hex = Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  const privateKey = `0x${hex}` as `0x${string}`;

  const signer = new PrivateKeySigner(privateKey);
  return { privateKey, address: signer.address };
}

interface ExtraAgent {
  address: string;
  name: string;
  validUntil: number;
}

function isNamedAgent(agent: ExtraAgent): boolean {
  return agent.name.replace(/ valid_until \d+$/, "") === AGENT_NAME;
}

async function fetchExtraAgentsOrThrow(
  userAddress: string,
  isTestnet: boolean,
): Promise<ExtraAgent[]> {
  const url = isTestnet
    ? "https://api.hyperliquid-testnet.xyz/info"
    : "https://api.hyperliquid.xyz/info";

  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ type: "extraAgents", user: userAddress }),
  });

  if (!res.ok) throw new Error(`extraAgents query failed: ${res.status}`);

  const agents: unknown = await res.json();
  if (!Array.isArray(agents))
    throw new Error("extraAgents: unexpected response shape");

  // The extraAgents API already returns validUntil in epoch ms.
  return agents as ExtraAgent[];
}

async function fetchExtraAgents(
  userAddress: string,
  isTestnet: boolean,
): Promise<ExtraAgent[]> {
  try {
    return await fetchExtraAgentsOrThrow(userAddress, isTestnet);
  } catch {
    return [];
  }
}

/**
 * Query the Hyperliquid info API for the named agents registered to a user.
 * Returns the matching entry for AGENT_NAME if it exists and is still valid.
 */
export async function fetchActiveAgent(
  userAddress: string,
  isTestnet: boolean,
): Promise<ExtraAgent | null> {
  const nowMs = Date.now();
  const agents = await fetchExtraAgents(userAddress, isTestnet);
  const match = agents.find((a) => isNamedAgent(a) && a.validUntil > nowMs);
  if (!match) return null;
  return match;
}

export async function fetchAgentByAddress(
  userAddress: string,
  agentAddress: string,
  isTestnet: boolean,
): Promise<ExtraAgent | null> {
  const agents = await fetchExtraAgents(userAddress, isTestnet);
  return (
    agents.find(
      (agent) => agent.address.toLowerCase() === agentAddress.toLowerCase(),
    ) ?? null
  );
}

/**
 * Checks whether a stored agent is still valid: the address must appear in the
 * on-chain `extraAgents` list and not be expired.
 */
export async function isAgentValid(
  stored: StoredAgent,
  userAddress: string,
  isTestnet: boolean,
): Promise<boolean> {
  if (stored.validUntil <= Date.now()) return false;

  const remote = await fetchActiveAgent(userAddress, isTestnet);
  if (!remote) return false;

  return (
    remote.address.toLowerCase() === stored.address.toLowerCase() &&
    remote.validUntil > Date.now()
  );
}

/**
 * Strict variant of `isAgentValid` for background revalidation: resolves
 * false only when HL definitively returned the agent list without our key
 * still valid. Throws on fetch failure (network, rate limit) so callers can
 * keep the stored agent through transient errors instead of clearing it.
 */
export async function isAgentRegistered(
  stored: StoredAgent,
  userAddress: string,
  isTestnet: boolean,
): Promise<boolean> {
  if (stored.validUntil <= Date.now()) return false;
  const agents = await fetchExtraAgentsOrThrow(userAddress, isTestnet);
  return agents.some(
    (a) =>
      a.address.toLowerCase() === stored.address.toLowerCase() &&
      a.validUntil > Date.now(),
  );
}

const DEAD_AGENT_PATTERNS = [
  /agent address .+ is not valid/i,
  /unknown signer/i,
  /not authorized/i,
  /not an agent/i,
  /user or api wallet .+ does not exist/i,
];

/**
 * Returns true if the error indicates the agent has been pruned, expired,
 * or is otherwise no longer registered on-chain.
 */
export function isDeadAgentError(err: unknown): boolean {
  const msg =
    err instanceof Error
      ? err.message
      : typeof err === "object" && err !== null && "message" in err
        ? String((err as { message: unknown }).message)
        : String(err);
  return DEAD_AGENT_PATTERNS.some((p) => p.test(msg));
}
