import { AlchemyAddressActivity, AlchemyWebhookPayload } from "../types";

export class WebhookRequestError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number
  ) {
    super(message);
    this.name = "WebhookRequestError";
  }
}

export class AlchemyWebhookPayloadValidator {
  validate(payload: unknown): AlchemyWebhookPayload {
    if (!payload || typeof payload !== "object") {
      throw new WebhookRequestError("Missing webhook payload", 400);
    }

    const candidate = payload as Record<string, unknown>;
    const webhookId = this.requireString(candidate.webhookId, "webhookId");
    const id = this.requireString(candidate.id, "id");
    const createdAt = this.requireIsoDate(candidate.createdAt, "createdAt");
    const type = this.requireString(candidate.type, "type");

    const event = this.validateEvent(candidate.event, type);

    return {
      webhookId,
      id,
      createdAt,
      type,
      event,
    };
  }

  private validateEvent(value: unknown, type: string): AlchemyWebhookPayload["event"] {
    if (value === undefined || value === null) {
      if (type === "ADDRESS_ACTIVITY") {
        throw new WebhookRequestError("ADDRESS_ACTIVITY payload requires event", 400);
      }

      return undefined;
    }

    if (typeof value !== "object") {
      throw new WebhookRequestError("event must be an object", 400);
    }

    const event = value as Record<string, unknown>;
    const network = this.optionalString(event.network, "event.network");
    const activity = this.validateActivities(event.activity, type);
    const transaction = this.validateTransactions(event.transaction, type);
    const slot = this.optionalNumber(event.slot, "event.slot");

    if (type === "ADDRESS_ACTIVITY" && !activity && !transaction) {
      throw new WebhookRequestError(
        "ADDRESS_ACTIVITY payload requires event.activity or event.transaction",
        400
      );
    }

    return {
      network,
      activity,
      transaction,
      slot,
    };
  }

  private validateActivities(value: unknown, type: string): AlchemyAddressActivity[] | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (!Array.isArray(value)) {
      throw new WebhookRequestError("event.activity must be an array", 400);
    }

    return value.map((item, index) => this.validateActivity(item, index));
  }

  private validateTransactions(
    value: unknown,
    _type: string
  ): Array<Record<string, unknown>> | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (!Array.isArray(value)) {
      throw new WebhookRequestError("event.transaction must be an array", 400);
    }

    return value.map((item, index) => {
      if (!item || typeof item !== "object") {
        throw new WebhookRequestError(`event.transaction[${index}] must be an object`, 400);
      }

      return item as Record<string, unknown>;
    });
  }

  private validateActivity(value: unknown, index: number): AlchemyAddressActivity {
    if (!value || typeof value !== "object") {
      throw new WebhookRequestError(`event.activity[${index}] must be an object`, 400);
    }

    const activity = value as Record<string, unknown>;

    return {
      blockNum: this.optionalString(activity.blockNum, `event.activity[${index}].blockNum`),
      hash: this.optionalString(activity.hash, `event.activity[${index}].hash`),
      fromAddress: this.optionalString(activity.fromAddress, `event.activity[${index}].fromAddress`),
      toAddress: this.optionalString(activity.toAddress, `event.activity[${index}].toAddress`),
      value: this.optionalNumberOrString(activity.value, `event.activity[${index}].value`),
      erc721TokenId: this.optionalString(
        activity.erc721TokenId,
        `event.activity[${index}].erc721TokenId`
      ),
      erc1155Metadata: this.optionalErc1155Metadata(
        activity.erc1155Metadata,
        `event.activity[${index}].erc1155Metadata`
      ),
      asset: this.optionalString(activity.asset, `event.activity[${index}].asset`),
      category: this.optionalString(activity.category, `event.activity[${index}].category`),
      rawContract: this.optionalRawContract(
        activity.rawContract,
        `event.activity[${index}].rawContract`
      ),
      typeTraceAddress: this.optionalString(
        activity.typeTraceAddress,
        `event.activity[${index}].typeTraceAddress`
      ),
      log: this.optionalLog(activity.log, `event.activity[${index}].log`),
    };
  }

  private optionalRawContract(value: unknown, field: string): AlchemyAddressActivity["rawContract"] {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (typeof value !== "object") {
      throw new WebhookRequestError(`${field} must be an object`, 400);
    }

    const rawContract = value as Record<string, unknown>;
    return {
      rawValue: this.optionalString(rawContract.rawValue, `${field}.rawValue`),
      address: this.optionalString(rawContract.address, `${field}.address`),
      decimals: this.optionalNumber(rawContract.decimals, `${field}.decimals`),
    };
  }

  private optionalLog(value: unknown, field: string): AlchemyAddressActivity["log"] {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (typeof value !== "object") {
      throw new WebhookRequestError(`${field} must be an object`, 400);
    }

    const log = value as Record<string, unknown>;
    return {
      address: this.optionalString(log.address, `${field}.address`),
      topics: this.optionalStringArray(log.topics, `${field}.topics`),
      data: this.optionalString(log.data, `${field}.data`),
      blockNumber: this.optionalString(log.blockNumber, `${field}.blockNumber`),
      transactionHash: this.optionalString(log.transactionHash, `${field}.transactionHash`),
      transactionIndex: this.optionalString(log.transactionIndex, `${field}.transactionIndex`),
      blockHash: this.optionalString(log.blockHash, `${field}.blockHash`),
      logIndex: this.optionalString(log.logIndex, `${field}.logIndex`),
      removed: this.optionalBoolean(log.removed, `${field}.removed`),
    };
  }

  private optionalErc1155Metadata(
    value: unknown,
    field: string
  ): AlchemyAddressActivity["erc1155Metadata"] {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (!Array.isArray(value)) {
      throw new WebhookRequestError(`${field} must be an array`, 400);
    }

    return value.map((item, index) => {
      if (!item || typeof item !== "object") {
        throw new WebhookRequestError(`${field}[${index}] must be an object`, 400);
      }

      const metadata = item as Record<string, unknown>;
      return {
        tokenId: this.optionalString(metadata.tokenId, `${field}[${index}].tokenId`),
        value: this.optionalString(metadata.value, `${field}[${index}].value`),
      };
    });
  }

  private optionalString(value: unknown, field: string): string | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (typeof value !== "string") {
      throw new WebhookRequestError(`${field} must be a string`, 400);
    }

    return value;
  }

  private optionalStringArray(value: unknown, field: string): string[] | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
      throw new WebhookRequestError(`${field} must be an array of strings`, 400);
    }

    return value;
  }

  private optionalNumber(value: unknown, field: string): number | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (typeof value !== "number" || Number.isNaN(value)) {
      throw new WebhookRequestError(`${field} must be a number`, 400);
    }

    return value;
  }

  private optionalBoolean(value: unknown, field: string): boolean | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (typeof value !== "boolean") {
      throw new WebhookRequestError(`${field} must be a boolean`, 400);
    }

    return value;
  }

  private optionalNumberOrString(value: unknown, field: string): number | string | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }

    if (typeof value !== "number" && typeof value !== "string") {
      throw new WebhookRequestError(`${field} must be a number or string`, 400);
    }

    return value;
  }

  private requireString(value: unknown, field: string): string {
    const result = this.optionalString(value, field);
    if (!result || !result.trim()) {
      throw new WebhookRequestError(`${field} is required`, 400);
    }

    return result;
  }

  private requireIsoDate(value: unknown, field: string): string {
    const result = this.requireString(value, field);
    if (Number.isNaN(Date.parse(result))) {
      throw new WebhookRequestError(`${field} must be a valid ISO date`, 400);
    }

    return result;
  }
}