/**
 * Synor Contract Client
 */

import type {
  ContractConfig,
  Abi,
  AbiEntry,
  DeployContractOptions,
  DeploymentResult,
  CallContractOptions,
  SendContractOptions,
  TransactionResult,
  EventLog,
  DecodedEvent,
  EventFilter,
  EventCallback,
  EventSubscription,
  ContractInterface,
  EncodeCallOptions,
  DecodeResultOptions,
  EstimateGasOptions,
  GasEstimation,
  BytecodeInfo,
  VerifyContractOptions,
  VerificationResult,
  MulticallRequest,
  MulticallResult,
  ReadStorageOptions,
} from './types';

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

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

/**
 * Main Synor Contract client.
 *
 * Provides smart contract deployment, interaction, and event handling.
 *
 * @example
 * ```ts
 * const contract = new SynorContract({ apiKey: 'sk_...' });
 *
 * // Deploy a contract
 * const result = await contract.deploy({
 *   bytecode: '0x608060...',
 *   abi: [...],
 *   args: [100, 'Token'],
 * });
 *
 * // Call a view function
 * const balance = await contract.call({
 *   address: result.address,
 *   method: 'balanceOf',
 *   args: ['0x...'],
 *   abi: [...],
 * });
 *
 * // Send a transaction
 * const tx = await contract.send({
 *   address: result.address,
 *   method: 'transfer',
 *   args: ['0x...', '1000000'],
 *   abi: [...],
 * });
 * ```
 */
export class SynorContract {
  private config: Required<Omit<ContractConfig, 'defaultGasLimit' | 'defaultGasPrice'>> & {
    defaultGasLimit?: string;
    defaultGasPrice?: string;
  };
  private closed = false;
  private subscriptions = new Map<string, WebSocket>();

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

  // ==================== Deployment ====================

  /**
   * Deploy a smart contract.
   *
   * @param options - Deployment options
   * @returns Deployment result with contract address
   */
  async deploy(options: DeployContractOptions): Promise<DeploymentResult> {
    const response = await this.request('POST', '/contracts/deploy', {
      bytecode: options.bytecode,
      abi: options.abi,
      args: options.args,
      gasLimit: options.gasLimit ?? this.config.defaultGasLimit,
      gasPrice: options.gasPrice ?? this.config.defaultGasPrice,
      value: options.value,
      salt: options.salt,
    });

    return response.deployment;
  }

  /**
   * Deploy using CREATE2 for deterministic addresses.
   *
   * @param options - Deployment options (salt required)
   * @returns Deployment result
   */
  async deployDeterministic(options: DeployContractOptions & { salt: string }): Promise<DeploymentResult> {
    return this.deploy(options);
  }

  /**
   * Predict CREATE2 deployment address.
   *
   * @param bytecode - Contract bytecode
   * @param salt - Deployment salt
   * @param constructorArgs - Constructor arguments
   * @returns Predicted address
   */
  async predictAddress(bytecode: string, salt: string, constructorArgs?: unknown[]): Promise<string> {
    const response = await this.request('POST', '/contracts/predict-address', {
      bytecode,
      salt,
      constructorArgs,
    });

    return response.address;
  }

  // ==================== Contract Interaction ====================

  /**
   * Call a view/pure function (no transaction).
   *
   * @param options - Call options
   * @returns Function return value
   */
  async call(options: CallContractOptions): Promise<unknown> {
    const response = await this.request('POST', '/contracts/call', {
      address: options.address,
      method: options.method,
      args: options.args,
      abi: options.abi,
      blockNumber: options.blockNumber ?? 'latest',
    });

    return response.result;
  }

  /**
   * Send a state-changing transaction.
   *
   * @param options - Transaction options
   * @returns Transaction result
   */
  async send(options: SendContractOptions): Promise<TransactionResult> {
    const response = await this.request('POST', '/contracts/send', {
      address: options.address,
      method: options.method,
      args: options.args,
      abi: options.abi,
      gasLimit: options.gasLimit ?? this.config.defaultGasLimit,
      gasPrice: options.gasPrice ?? this.config.defaultGasPrice,
      value: options.value,
    });

    return response.transaction;
  }

  /**
   * Execute multiple calls in a single request.
   *
   * @param requests - Array of multicall requests
   * @param abis - Optional ABIs for decoding (keyed by address)
   * @returns Array of results
   */
  async multicall(
    requests: MulticallRequest[],
    abis?: Record<string, Abi>
  ): Promise<MulticallResult[]> {
    const response = await this.request('POST', '/contracts/multicall', {
      calls: requests,
      abis,
    });

    return response.results;
  }

  // ==================== Events ====================

  /**
   * Get historical events.
   *
   * @param filter - Event filter
   * @returns Array of decoded events
   */
  async getEvents(filter: EventFilter): Promise<DecodedEvent[]> {
    const response = await this.request('POST', '/contracts/events', {
      address: filter.address,
      eventName: filter.eventName,
      abi: filter.abi,
      fromBlock: filter.fromBlock ?? 'earliest',
      toBlock: filter.toBlock ?? 'latest',
      filter: filter.filter,
    });

    return response.events;
  }

  /**
   * Get raw event logs.
   *
   * @param filter - Event filter
   * @returns Array of raw logs
   */
  async getLogs(filter: EventFilter): Promise<EventLog[]> {
    const response = await this.request('POST', '/contracts/logs', {
      address: filter.address,
      eventName: filter.eventName,
      abi: filter.abi,
      fromBlock: filter.fromBlock ?? 'earliest',
      toBlock: filter.toBlock ?? 'latest',
      filter: filter.filter,
    });

    return response.logs;
  }

  /**
   * Subscribe to contract events.
   *
   * @param filter - Event filter
   * @param callback - Callback for new events
   * @returns Subscription handle
   */
  async subscribeEvents(filter: EventFilter, callback: EventCallback): Promise<EventSubscription> {
    const wsEndpoint = this.config.endpoint.replace('https://', 'wss://').replace('http://', 'ws://');
    const ws = new WebSocket(`${wsEndpoint}/contracts/events/subscribe`);

    const id = crypto.randomUUID();
    this.subscriptions.set(id, ws);

    return new Promise((resolve, reject) => {
      ws.onopen = () => {
        ws.send(
          JSON.stringify({
            type: 'subscribe',
            auth: this.config.apiKey,
            filter: {
              address: filter.address,
              eventName: filter.eventName,
              abi: filter.abi,
              filter: filter.filter,
            },
          })
        );

        resolve({
          id,
          unsubscribe: async () => {
            ws.close();
            this.subscriptions.delete(id);
          },
        });
      };

      ws.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          if (data.type === 'event') {
            callback(data.event);
          }
        } catch (e) {
          if (this.config.debug) {
            console.error('[SynorContract] Failed to parse event:', e);
          }
        }
      };

      ws.onerror = (error) => {
        reject(new ContractError('WebSocket error'));
      };
    });
  }

  /**
   * Decode a raw event log.
   *
   * @param log - Raw log
   * @param abi - Contract ABI
   * @returns Decoded event
   */
  async decodeLog(log: EventLog, abi: Abi): Promise<DecodedEvent> {
    const response = await this.request('POST', '/contracts/decode-log', {
      log,
      abi,
    });

    return response.event;
  }

  // ==================== ABI Utilities ====================

  /**
   * Load and parse a contract ABI.
   *
   * @param abi - Contract ABI
   * @returns Parsed contract interface
   */
  loadAbi(abi: Abi): ContractInterface {
    const functions: Record<string, AbiEntry> = {};
    const events: Record<string, AbiEntry> = {};
    const errors: Record<string, AbiEntry> = {};

    for (const entry of abi) {
      if (entry.type === 'function' && entry.name) {
        functions[entry.name] = entry;
      } else if (entry.type === 'event' && entry.name) {
        events[entry.name] = entry;
      } else if (entry.type === 'error' && entry.name) {
        errors[entry.name] = entry;
      }
    }

    return { abi, functions, events, errors };
  }

  /**
   * Encode a function call.
   *
   * @param options - Encoding options
   * @returns Encoded calldata (hex)
   */
  async encodeCall(options: EncodeCallOptions): Promise<string> {
    const response = await this.request('POST', '/contracts/encode', {
      method: options.method,
      args: options.args,
      abi: options.abi,
    });

    return response.data;
  }

  /**
   * Decode function return data.
   *
   * @param options - Decoding options
   * @returns Decoded result
   */
  async decodeResult(options: DecodeResultOptions): Promise<unknown> {
    const response = await this.request('POST', '/contracts/decode', {
      method: options.method,
      data: options.data,
      abi: options.abi,
    });

    return response.result;
  }

  /**
   * Get function selector (first 4 bytes of keccak256 hash).
   *
   * @param signature - Function signature (e.g., "transfer(address,uint256)")
   * @returns Function selector (hex)
   */
  async getFunctionSelector(signature: string): Promise<string> {
    const response = await this.request('POST', '/contracts/selector', {
      signature,
    });

    return response.selector;
  }

  // ==================== Gas Estimation ====================

  /**
   * Estimate gas for a contract call or deployment.
   *
   * @param options - Estimation options
   * @returns Gas estimation
   */
  async estimateGas(options: EstimateGasOptions): Promise<GasEstimation> {
    const response = await this.request('POST', '/contracts/estimate-gas', {
      address: options.address,
      method: options.method,
      args: options.args,
      abi: options.abi,
      bytecode: options.bytecode,
      value: options.value,
      from: options.from,
    });

    return response.estimation;
  }

  // ==================== Contract Info ====================

  /**
   * Get contract bytecode.
   *
   * @param address - Contract address
   * @returns Bytecode info
   */
  async getBytecode(address: string): Promise<BytecodeInfo> {
    const response = await this.request('GET', `/contracts/${address}/bytecode`);
    return response;
  }

  /**
   * Check if an address is a contract.
   *
   * @param address - Address to check
   * @returns True if address has code
   */
  async isContract(address: string): Promise<boolean> {
    const response = await this.request('GET', `/contracts/${address}/is-contract`);
    return response.isContract;
  }

  /**
   * Read contract storage slot.
   *
   * @param options - Storage read options
   * @returns Storage value (hex)
   */
  async readStorage(options: ReadStorageOptions): Promise<string> {
    const response = await this.request('POST', '/contracts/storage', {
      address: options.address,
      slot: options.slot,
      blockNumber: options.blockNumber ?? 'latest',
    });

    return response.value;
  }

  // ==================== Verification ====================

  /**
   * Submit contract for verification.
   *
   * @param options - Verification options
   * @returns Verification result
   */
  async verifyContract(options: VerifyContractOptions): Promise<VerificationResult> {
    const response = await this.request('POST', '/contracts/verify', {
      address: options.address,
      sourceCode: options.sourceCode,
      compilerVersion: options.compilerVersion,
      constructorArguments: options.constructorArguments,
      optimization: options.optimization,
      optimizationRuns: options.optimizationRuns,
      contractName: options.contractName,
    });

    return response;
  }

  /**
   * Check verification status.
   *
   * @param address - Contract address
   * @returns Verification result
   */
  async getVerificationStatus(address: string): Promise<VerificationResult> {
    const response = await this.request('GET', `/contracts/${address}/verification`);
    return response;
  }

  /**
   * Get verified contract ABI.
   *
   * @param address - Contract address
   * @returns Contract ABI (if verified)
   */
  async getVerifiedAbi(address: string): Promise<Abi | null> {
    try {
      const response = await this.request('GET', `/contracts/${address}/abi`);
      return response.abi;
    } catch {
      return null;
    }
  }

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

  /**
   * Check if the contract 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;
    for (const ws of this.subscriptions.values()) {
      ws.close();
    }
    this.subscriptions.clear();
  }

  /**
   * 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 ContractError('Client has been closed');
    }

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

    if (this.config.debug) {
      console.log(`[SynorContract] ${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 ContractError(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;
  }
}
