/**
 * Synor Privacy Client
 */

import type {
  PrivacyConfig,
  ConfidentialUTXO,
  ConfidentialOutput,
  ConfidentialTransaction,
  CreateConfidentialTxOptions,
  VerifyConfidentialTxResult,
  RingSignature,
  CreateRingSignatureOptions,
  VerifyRingSignatureOptions,
  VerifyRingSignatureResult,
  StealthAddress,
  StealthKeypair,
  OneTimeAddress,
  DeriveSharedSecretOptions,
  SharedSecret,
  Commitment,
  CreateCommitmentOptions,
  CommitmentWithBlinding,
  OpenCommitmentOptions,
  RangeProof,
  CreateRangeProofOptions,
  VerifyRangeProofResult,
  BlindingSumOptions,
} from './types';

const DEFAULT_ENDPOINT = 'https://privacy.synor.cc/api/v1';

/**
 * Synor Privacy SDK error.
 */
export class PrivacyError extends Error {
  constructor(
    message: string,
    public statusCode?: number,
    public code?: string
  ) {
    super(message);
    this.name = 'PrivacyError';
  }
}

/**
 * Main Synor Privacy client.
 *
 * Provides privacy-enhancing features including confidential transactions,
 * ring signatures, stealth addresses, and Pedersen commitments.
 *
 * @example
 * ```ts
 * const privacy = new SynorPrivacy({ apiKey: 'sk_...' });
 *
 * // Generate a stealth address for receiving private payments
 * const keypair = await privacy.generateStealthKeypair();
 * console.log('Stealth address:', keypair.address.address);
 *
 * // Create a ring signature for anonymous signing
 * const signature = await privacy.createRingSignature({
 *   message: 'Hello, World!',
 *   ring: [pubKey1, pubKey2, myPubKey, pubKey3],
 *   privateKey: myPrivateKey,
 * });
 * ```
 */
export class SynorPrivacy {
  private config: Required<PrivacyConfig>;
  private closed = false;

  constructor(config: PrivacyConfig) {
    this.config = {
      apiKey: config.apiKey,
      endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
      timeout: config.timeout ?? 30000,
      retries: config.retries ?? 3,
      debug: config.debug ?? false,
    };
  }

  // ==================== Confidential Transactions ====================

  /**
   * Create a confidential transaction with hidden amounts.
   *
   * Uses Pedersen commitments and Bulletproof range proofs to hide
   * transaction amounts while proving they are valid (non-negative).
   *
   * @param options - Transaction options
   * @returns Confidential transaction ready for broadcast
   */
  async createConfidentialTransaction(
    options: CreateConfidentialTxOptions
  ): Promise<ConfidentialTransaction> {
    const response = await this.request('POST', '/transactions/confidential', {
      inputs: options.inputs.map((input) => ({
        txid: input.txid,
        vout: input.vout,
        commitment: input.commitment,
        rangeProof: input.rangeProof,
        blinding: input.blinding,
        amount: input.amount,
      })),
      outputs: options.outputs.map((output) => ({
        recipient: output.recipient,
        amount: output.amount,
        blinding: output.blinding,
      })),
      fee: options.fee,
      lockHeight: options.lockHeight,
    });

    return response.transaction;
  }

  /**
   * Verify a confidential transaction.
   *
   * Checks that:
   * - Commitments balance (inputs = outputs + fee)
   * - All range proofs are valid (no negative amounts)
   * - Kernel signature is valid
   * - No duplicate inputs
   *
   * @param transaction - Transaction to verify
   * @returns Verification result with details
   */
  async verifyConfidentialTransaction(
    transaction: ConfidentialTransaction
  ): Promise<VerifyConfidentialTxResult> {
    const response = await this.request('POST', '/transactions/confidential/verify', {
      transaction,
    });

    return response;
  }

  /**
   * Decode a confidential transaction output.
   *
   * If you have the blinding factor, you can decode the committed amount.
   *
   * @param commitment - Output commitment
   * @param blinding - Blinding factor
   * @returns Decoded amount
   */
  async decodeConfidentialOutput(commitment: string, blinding: string): Promise<string> {
    const response = await this.request('POST', '/transactions/confidential/decode', {
      commitment,
      blinding,
    });

    return response.amount;
  }

  // ==================== Ring Signatures ====================

  /**
   * Create a ring signature.
   *
   * Ring signatures allow signing a message as "one of N" without
   * revealing which member of the ring actually signed.
   *
   * @param options - Signing options
   * @returns Ring signature
   */
  async createRingSignature(options: CreateRingSignatureOptions): Promise<RingSignature> {
    const response = await this.request('POST', '/ring-signatures/create', {
      message: options.message,
      ring: options.ring,
      privateKey: options.privateKey,
      signerIndex: options.signerIndex,
    });

    return response.signature;
  }

  /**
   * Verify a ring signature.
   *
   * Verifies that the signature was created by one of the ring members
   * without revealing which one.
   *
   * @param options - Verification options
   * @returns Verification result
   */
  async verifyRingSignature(options: VerifyRingSignatureOptions): Promise<VerifyRingSignatureResult> {
    const response = await this.request('POST', '/ring-signatures/verify', {
      signature: options.signature,
      message: options.message,
    });

    return response;
  }

  /**
   * Check if a key image has been used (double-spend detection).
   *
   * @param keyImage - Key image from ring signature
   * @returns True if the key image has been seen before
   */
  async isKeyImageUsed(keyImage: string): Promise<boolean> {
    const response = await this.request('GET', `/ring-signatures/key-images/${keyImage}`);
    return response.used;
  }

  /**
   * Generate a random ring from available public keys.
   *
   * Useful for creating decoy sets for ring signatures.
   *
   * @param size - Desired ring size
   * @param exclude - Public keys to exclude (e.g., your own)
   * @returns Array of public keys for the ring
   */
  async generateRandomRing(size: number, exclude?: string[]): Promise<string[]> {
    const response = await this.request('POST', '/ring-signatures/random-ring', {
      size,
      exclude,
    });

    return response.ring;
  }

  // ==================== Stealth Addresses ====================

  /**
   * Generate a new stealth address keypair.
   *
   * Stealth addresses allow receiving payments at unique one-time
   * addresses that cannot be linked to the recipient's public address.
   *
   * @returns Stealth keypair (keep private keys secure!)
   */
  async generateStealthKeypair(): Promise<StealthKeypair> {
    const response = await this.request('POST', '/stealth/generate');
    return response.keypair;
  }

  /**
   * Create a one-time address for sending to a stealth address.
   *
   * The sender creates this address using the recipient's stealth
   * address. Only the recipient can detect and spend from it.
   *
   * @param stealthAddress - Recipient's stealth address
   * @returns One-time address and ephemeral key
   */
  async createOneTimeAddress(stealthAddress: StealthAddress): Promise<OneTimeAddress> {
    const response = await this.request('POST', '/stealth/one-time-address', {
      scanPublicKey: stealthAddress.scanPublicKey,
      spendPublicKey: stealthAddress.spendPublicKey,
    });

    return response.oneTimeAddress;
  }

  /**
   * Derive the shared secret for a stealth payment.
   *
   * The recipient uses this to derive the one-time private key
   * needed to spend from the one-time address.
   *
   * @param options - Derivation options
   * @returns Shared secret and derived keys
   */
  async deriveSharedSecret(options: DeriveSharedSecretOptions): Promise<SharedSecret> {
    const response = await this.request('POST', '/stealth/derive-secret', {
      scanPublicKey: options.stealthAddress.scanPublicKey,
      spendPublicKey: options.stealthAddress.spendPublicKey,
      privateKey: options.privateKey,
      ephemeralPublicKey: options.ephemeralPublicKey,
    });

    return response;
  }

  /**
   * Scan transactions to find stealth payments to you.
   *
   * @param scanPrivateKey - Your scan private key
   * @param spendPublicKey - Your spend public key
   * @param transactions - Transaction IDs to scan
   * @returns Found payments with one-time addresses
   */
  async scanForPayments(
    scanPrivateKey: string,
    spendPublicKey: string,
    transactions: string[]
  ): Promise<OneTimeAddress[]> {
    const response = await this.request('POST', '/stealth/scan', {
      scanPrivateKey,
      spendPublicKey,
      transactions,
    });

    return response.payments;
  }

  // ==================== Pedersen Commitments ====================

  /**
   * Create a Pedersen commitment.
   *
   * C = vG + bH where v is the value and b is the blinding factor.
   *
   * @param options - Commitment options
   * @returns Commitment and blinding factor
   */
  async createCommitment(options: CreateCommitmentOptions): Promise<CommitmentWithBlinding> {
    const response = await this.request('POST', '/commitments/create', {
      value: options.value,
      blinding: options.blinding,
    });

    return response;
  }

  /**
   * Open (verify) a Pedersen commitment.
   *
   * Verifies that the commitment commits to the claimed value.
   *
   * @param options - Opening options
   * @returns True if the commitment opens correctly
   */
  async openCommitment(options: OpenCommitmentOptions): Promise<boolean> {
    const response = await this.request('POST', '/commitments/open', {
      commitment: options.commitment,
      value: options.value,
      blinding: options.blinding,
    });

    return response.valid;
  }

  /**
   * Add two commitments.
   *
   * C1 + C2 = C3 where the values and blindings also add.
   *
   * @param commitment1 - First commitment
   * @param commitment2 - Second commitment
   * @returns Sum commitment
   */
  async addCommitments(commitment1: string, commitment2: string): Promise<string> {
    const response = await this.request('POST', '/commitments/add', {
      commitment1,
      commitment2,
    });

    return response.commitment;
  }

  /**
   * Subtract two commitments.
   *
   * @param commitment1 - First commitment
   * @param commitment2 - Second commitment
   * @returns Difference commitment
   */
  async subtractCommitments(commitment1: string, commitment2: string): Promise<string> {
    const response = await this.request('POST', '/commitments/subtract', {
      commitment1,
      commitment2,
    });

    return response.commitment;
  }

  /**
   * Compute blinding factor sum for transaction balancing.
   *
   * For a balanced transaction: sum(input blindings) - sum(output blindings) = kernel blinding
   *
   * @param options - Blinding factors
   * @returns Net blinding factor
   */
  async computeBlindingSum(options: BlindingSumOptions): Promise<string> {
    const response = await this.request('POST', '/commitments/blinding-sum', {
      positive: options.positive,
      negative: options.negative,
    });

    return response.sum;
  }

  /**
   * Generate a random blinding factor.
   *
   * @returns Random 32-byte blinding factor (hex)
   */
  async generateBlinding(): Promise<string> {
    const response = await this.request('POST', '/commitments/random-blinding');
    return response.blinding;
  }

  // ==================== Range Proofs ====================

  /**
   * Create a Bulletproof range proof.
   *
   * Proves that a committed value is in range [0, 2^n) without
   * revealing the actual value.
   *
   * @param options - Range proof options
   * @returns Range proof
   */
  async createRangeProof(options: CreateRangeProofOptions): Promise<RangeProof> {
    const response = await this.request('POST', '/range-proofs/create', {
      value: options.value,
      blinding: options.blinding,
      message: options.message,
      bitLength: options.bitLength ?? 64,
    });

    return response.proof;
  }

  /**
   * Verify a Bulletproof range proof.
   *
   * @param commitment - Commitment the proof is for
   * @param proof - Range proof data
   * @returns Verification result
   */
  async verifyRangeProof(commitment: string, proof: string): Promise<VerifyRangeProofResult> {
    const response = await this.request('POST', '/range-proofs/verify', {
      commitment,
      proof,
    });

    return response;
  }

  /**
   * Create an aggregated range proof for multiple outputs.
   *
   * More efficient than individual proofs for multiple outputs.
   *
   * @param outputs - Array of {value, blinding} pairs
   * @returns Aggregated proof
   */
  async createAggregatedRangeProof(
    outputs: Array<{ value: string; blinding: string }>
  ): Promise<string> {
    const response = await this.request('POST', '/range-proofs/aggregate', {
      outputs,
    });

    return response.proof;
  }

  // ==================== Lifecycle ====================

  /**
   * Check if the privacy service is healthy.
   *
   * @returns True if the service is operational
   */
  async healthCheck(): Promise<boolean> {
    try {
      const response = await this.request('GET', '/health');
      return response.status === 'healthy';
    } catch {
      return false;
    }
  }

  /**
   * Close the client and release resources.
   */
  close(): void {
    this.closed = true;
  }

  /**
   * Check if the client has been closed.
   */
  isClosed(): boolean {
    return this.closed;
  }

  // ==================== Private ====================

  private async request(method: string, path: string, body?: unknown): Promise<any> {
    if (this.closed) {
      throw new PrivacyError('Client has been closed');
    }

    const url = `${this.config.endpoint}${path}`;

    if (this.config.debug) {
      console.log(`[SynorPrivacy] ${method} ${url}`);
    }

    let lastError: Error | null = null;

    for (let attempt = 0; attempt < this.config.retries; attempt++) {
      try {
        const response = await fetch(url, {
          method,
          headers: {
            Authorization: `Bearer ${this.config.apiKey}`,
            'Content-Type': 'application/json',
            'X-SDK-Version': 'js/1.0.0',
          },
          body: body ? JSON.stringify(body) : undefined,
          signal: AbortSignal.timeout(this.config.timeout),
        });

        if (!response.ok) {
          const error = await response.json().catch(() => ({ message: response.statusText }));
          throw new PrivacyError(error.message || 'Request failed', response.status, error.code);
        }

        return response.json();
      } catch (error) {
        lastError = error as Error;
        if (attempt < this.config.retries - 1) {
          await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000));
        }
      }
    }

    throw lastError;
  }
}
