import nacl from "tweetnacl";
import bs58 from "bs58";

export interface SignedRequest {
  headers: Record<string, string>;
  body: string;
  timestamp: string;
}

export interface SigningConfig {
  apiKey?: string;
  secret?: string;
  subaccount?: string;
}

export interface SignatureHeader {
  timestamp: number;
  expiry_window: number;
  type: string;
}

const encoder = new TextEncoder();

/**
 * Sign request according to Pacifica Ed25519 spec.
 *
 * Signature base string: `${timestamp}:${method}:${path}:${body || ''}`
 * - `timestamp`: milliseconds since epoch
 * - `method`: upper-case HTTP verb
 * - `path`: request path including query string (e.g. `/orders?symbol=BTC`)
 * - `body`: JSON string if present, otherwise empty string
 */
export function signRequest(
  cfg: SigningConfig,
  method: string,
  path: string,
  body: unknown
): SignedRequest {
  const bodyString =
    body === undefined || body === null
      ? ""
      : typeof body === "string"
      ? body
      : JSON.stringify(body);

  // Public endpoints may omit keys.
  if (!cfg.apiKey || !cfg.secret) {
    return {
      headers: {},
      body: bodyString,
      timestamp: Date.now().toString()
    };
  }

  const timestamp = Date.now().toString();
  const baseString = `${timestamp}:${method.toUpperCase()}:${path}:${bodyString}`;

  const secretKey = deriveSecretKey(cfg.secret);
  const keyPair = nacl.sign.keyPair.fromSecretKey(secretKey);
  const derivedAccount = bs58.encode(keyPair.publicKey);

  if (cfg.apiKey && cfg.apiKey !== derivedAccount) {
    throw new Error(
      `Pacifica apiKey (${cfg.apiKey}) does not match private key (derived ${derivedAccount})`
    );
  }
  const signatureBytes = nacl.sign.detached(encoder.encode(baseString), secretKey);
  const signature = Buffer.from(signatureBytes).toString("base64");

  const headers: Record<string, string> = {
    "X-Pacific-Key": cfg.apiKey,
    "X-Pacific-Timestamp": timestamp,
    "X-Pacific-Signature": signature
  };

  if (cfg.subaccount) {
    headers["X-Pacific-Subaccount"] = cfg.subaccount;
  }

  return {
    headers,
    body: bodyString,
    timestamp
  };
}

export function signStructuredPayload(
  cfg: SigningConfig,
  header: SignatureHeader,
  payload: Record<string, unknown>
): { message: string; signature: string } {
  if (!cfg.secret) {
    throw new Error("Pacifica secret is required for message signing");
  }

  const secretKey = deriveSecretKey(cfg.secret);
  const canonical = canonicalize({ ...header, data: payload });
  const message = JSON.stringify(canonical);

  const signatureBytes = nacl.sign.detached(encoder.encode(message), secretKey);
  const signature = bs58.encode(signatureBytes);

  return { message, signature };
}

function deriveSecretKey(secret: string): Uint8Array {
  const trimmed = secret.trim();
  let decoded: Uint8Array | undefined;

  // Try base58 first.
  try {
    decoded = bs58.decode(trimmed);
  } catch {
    // Ignore and try base64.
  }

  if (!decoded || decoded.length === 0) {
    const buf = Buffer.from(trimmed, "base64");
    if (buf.length === 0) {
      throw new Error("Pacifica secret is not valid base58/base64 Ed25519 key");
    }
    decoded = new Uint8Array(buf);
  }

  if (decoded.length === nacl.sign.secretKeyLength) {
    return decoded;
  }

  if (decoded.length === nacl.sign.seedLength) {
    return nacl.sign.keyPair.fromSeed(decoded).secretKey;
  }

  throw new Error(
    `Pacifica secret has invalid length ${decoded.length}, expected ${nacl.sign.seedLength} or ${nacl.sign.secretKeyLength}`
  );
}

function canonicalize(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(item => canonicalize(item));
  }

  if (value && typeof value === "object") {
    const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => {
      if (a < b) return -1;
      if (a > b) return 1;
      return 0;
    });

    const sorted: Record<string, unknown> = {};
    for (const [key, val] of entries) {
      sorted[key] = canonicalize(val);
    }
    return sorted;
  }

  return value;
}
