import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { bytesToHex, keccak256, type Address, type Hex } from "viem";
import type { SolverClientConfig } from "./config.js";

export interface CachedSubmission {
  submissionId: string;
  solver: Address;
  commitHash: Hex;
  solutionRef: string;
  solutionDigest: Hex;
  solutionKind?: number;
  solutionKindCode?: string;
  solutionKindName?: string;
  proofFormat?: number;
  proofFormatName?: string;
  bondToken?: Address;
  solverBond?: string;
  committedAt: string;
  revealedAt: string;
  quorumReachedAt: string;
  forVotes: string;
  againstVotes: string;
  state: number;
  stateLabel: string;
  verified: boolean;
  finalized: boolean;
}

export interface CachedBounty {
  bountyId: string;
  bountyCode: string;
  paymentToken?: Address;
  instanceRef: string;
  instanceDigest: Hex;
  reward: string;
  verifierRewardPool: string;
  commitDeadline: string;
  revealDeadline: string;
  verificationDeadline: string;
  verifierQuorum: string;
  submissionCount: string;
  acceptedCandidateCount: string;
  finalized: boolean;
  verifiedAnswerCount: number;
  submissions: Record<string, CachedSubmission>;
}

export interface MarketplaceResponse {
  status: {
    configured: boolean;
    chainId: number;
    chainName: string;
    bountyManager?: Address;
    cachedBounties: number;
    cachedSubmissions: number;
    storage: "json" | "postgres";
  };
  bounties: CachedBounty[];
}

export interface BountyDetail {
  bountyId: string;
  bountyCode: string;
  bounty: {
    issuer: Address;
    paymentToken?: Address;
    instanceRef: string;
    instanceDigest: Hex;
    instanceDownloadUrl: string;
    metadataRef: string;
    metadataDigest: Hex;
    reward: string;
    verifierRewardPool: string;
    postingFee: string;
    commitDeadline: string;
    revealDeadline: string;
    verificationDeadline: string;
    verifierQuorum: string;
    submissionCount: string;
    acceptedCandidateCount: string;
    finalized: boolean;
  };
  submissions: CachedSubmission[];
}

export interface StoredArtifact {
  ref: string;
  digest: Hex;
  name: string;
  storedName: string;
  size: number;
  storage: "local" | "r2";
  solutionKind?: number;
  solutionKindName?: string;
  proofFormat?: number;
  proofFormatName?: string;
}

export interface PreparedProtocolTransaction {
  label: string;
  contract: string;
  functionName: string;
  to: Address;
  data: Hex;
  value: "0";
  args: unknown[];
}

export interface PrepareCommitResult {
  token?: Address;
  bondToken?: Address;
  bondTokenSymbol?: string;
  bondTokenDecimals?: number;
  bountyManager: Address;
  bountyId: string;
  bountyCode: string;
  solver: Address;
  solutionKind: number;
  solutionKindCode: string;
  proofFormat: number;
  proofFormatName: string;
  solutionRef: string;
  solutionDigest: Hex;
  salt: Hex;
  commitHash: Hex;
  solverBond: string;
  transactions: PreparedProtocolTransaction[];
}

export interface PrepareRevealResult {
  bountyManager: Address;
  bountyId: string;
  bountyCode: string;
  submissionId: string;
  solutionKind: number;
  solutionKindCode: string;
  proofFormat: number;
  proofFormatName: string;
  transactions: PreparedProtocolTransaction[];
}

async function readJson<T>(response: Response): Promise<T> {
  const body = await response.text();
  let payload: unknown = {};
  try {
    payload = body ? JSON.parse(body) : {};
  } catch {
    throw new Error(body || `HTTP ${response.status}`);
  }
  if (!response.ok) {
    const error = typeof payload === "object" && payload && "error" in payload ? (payload as { error?: unknown }).error : undefined;
    throw new Error(typeof error === "string" ? error : `HTTP ${response.status}`);
  }
  return payload as T;
}

function apiUrl(config: SolverClientConfig, pathname: string) {
  return new URL(pathname, config.apiUrl);
}

export class ProtocolApiClient {
  constructor(private readonly config: SolverClientConfig) {}

  async marketplace() {
    const response = await fetch(apiUrl(this.config, "/api/protocol/marketplace"));
    return readJson<MarketplaceResponse>(response);
  }

  async bounty(idOrCode: string) {
    const response = await fetch(apiUrl(this.config, `/api/protocol/sdk/bounties/${encodeURIComponent(idOrCode)}`));
    return readJson<BountyDetail>(response);
  }

  async downloadInstance(input: {
    bountyId: string;
    instanceDownloadUrl: string;
    expectedDigest: Hex;
  }) {
    const response = await fetch(new URL(input.instanceDownloadUrl, this.config.apiUrl));
    if (!response.ok) {
      throw new Error(`Instance download failed with HTTP ${response.status}.`);
    }

    const bytes = new Uint8Array(await response.arrayBuffer());
    const digest = keccak256(bytesToHex(bytes));
    if (digest.toLowerCase() !== input.expectedDigest.toLowerCase()) {
      throw new Error("Downloaded instance digest does not match the chain digest.");
    }

    await mkdir(this.config.artifactDir, { recursive: true });
    const filePath = path.join(this.config.artifactDir, `bounty-${input.bountyId}-instance.cnf`);
    await writeFile(filePath, bytes);
    return filePath;
  }

  async uploadSolution(filePath: string, fileName: string, input?: { solutionKind?: number; proofFormat?: number }) {
    const bytes = await readFile(filePath);
    const form = new FormData();
    form.set("kind", "solution");
    if (input?.solutionKind !== undefined) {
      form.set("solutionKind", String(input.solutionKind));
    }
    if (input?.proofFormat !== undefined) {
      form.set("proofFormat", String(input.proofFormat));
    }
    form.set("file", new File([new Uint8Array(bytes)], fileName, { type: "application/x-dimacs-cnf" }));

    const response = await fetch(apiUrl(this.config, "/api/protocol/storage"), {
      method: "POST",
      body: form
    });
    return readJson<StoredArtifact>(response);
  }

  async prepareCommit(input: {
    bounty: string;
    solver: Address;
    solutionKind?: number;
    proofFormat?: number;
    solutionRef: string;
    solutionDigest: Hex;
  }) {
    const response = await fetch(apiUrl(this.config, "/api/protocol/sdk/solver/prepare-commit"), {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(input)
    });
    return readJson<PrepareCommitResult>(response);
  }

  async prepareReveal(input: {
    bounty: string;
    submissionId: string;
    solutionKind?: number;
    proofFormat?: number;
    solutionRef: string;
    solutionDigest: Hex;
    salt: Hex;
  }) {
    const response = await fetch(apiUrl(this.config, "/api/protocol/sdk/solver/prepare-reveal"), {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(input)
    });
    return readJson<PrepareRevealResult>(response);
  }
}
