import fetch, { Response } from "node-fetch";
import {
  BlinkCreateInvoiceInput,
  BlinkCreateInvoicePayload,
  BlinkGraphQlResponse,
  BlinkInvoice,
  BlinkWallet,
} from "./types";

type FetchLike = typeof fetch;

interface BlinkClientOptions {
  apiUrl: string;
  apiKey: string;
  fetchImpl?: FetchLike;
}

interface MeQueryResponse {
  me?: {
    defaultAccount?: {
      wallets?: BlinkWallet[] | null;
    } | null;
  } | null;
}

interface LnInvoiceCreateResponse {
  lnInvoiceCreate?: BlinkCreateInvoicePayload | null;
}

export class BlinkClientError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly details?: Record<string, unknown>
  ) {
    super(message);
    this.name = "BlinkClientError";
  }
}

export class BlinkClient {
  private readonly apiUrl: string;
  private readonly apiKey: string;
  private readonly fetchImpl: FetchLike;
  private btcWalletId?: string;

  constructor(options: BlinkClientOptions) {
    this.apiUrl = String(options.apiUrl || "").trim();
    this.apiKey = String(options.apiKey || "").trim();
    this.fetchImpl = options.fetchImpl || fetch;
  }

  async createLightningInvoice(input: BlinkCreateInvoiceInput): Promise<BlinkInvoice> {
    const payload = await this.graphql<LnInvoiceCreateResponse>(
      `
        mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) {
          lnInvoiceCreate(input: $input) {
            invoice {
              createdAt
              externalId
              paymentHash
              paymentRequest
              paymentSecret
              paymentStatus
              satoshis
            }
            errors {
              message
            }
          }
        }
      `,
      { input }
    );

    const result = payload.lnInvoiceCreate;
    const mutationError = result?.errors?.find((error) => error?.message);
    if (mutationError?.message) {
      throw new BlinkClientError("Blink invoice creation failed", 502, {
        provider: "blink",
        reason: mutationError.message,
      });
    }

    if (!result?.invoice) {
      throw new BlinkClientError("Blink invoice creation returned no invoice", 502, {
        provider: "blink",
      });
    }

    return result.invoice;
  }

  async getBtcWalletId(): Promise<string> {
    if (this.btcWalletId) {
      return this.btcWalletId;
    }

    const payload = await this.graphql<MeQueryResponse>(
      `
        query me {
          me {
            defaultAccount {
              wallets {
                id
                walletCurrency
              }
            }
          }
        }
      `
    );

    const wallets = payload.me?.defaultAccount?.wallets || [];
    const btcWallet = wallets.find((wallet) => wallet.walletCurrency?.toUpperCase() === "BTC");

    if (!btcWallet?.id) {
      throw new BlinkClientError("Blink BTC wallet not found", 502, {
        provider: "blink",
      });
    }

    this.btcWalletId = btcWallet.id;
    return btcWallet.id;
  }

  private async graphql<TData>(
    query: string,
    variables?: Record<string, unknown>
  ): Promise<TData> {
    if (!this.apiUrl) {
      throw new BlinkClientError("Blink is not configured", 503, {
        provider: "blink",
        missing: "BLINK_API_URL",
      });
    }

    if (!this.apiKey) {
      throw new BlinkClientError("Blink is not configured", 503, {
        provider: "blink",
        missing: "BLINK_API_KEY",
      });
    }

    let response: Response;

    try {
      response = await this.fetchImpl(this.apiUrl, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          "x-api-key": this.apiKey,
        },
        body: JSON.stringify({ query, variables }),
      });
    } catch (error: unknown) {
      throw new BlinkClientError("Blink request failed", 502, {
        provider: "blink",
        reason: error instanceof Error ? error.message : String(error),
      });
    }

    if (!response.ok) {
      const body = await response.text().catch(() => "");
      throw new BlinkClientError("Blink request failed", 502, {
        provider: "blink",
        http_status: response.status,
        body: body.slice(0, 500),
      });
    }

    const payload = (await response.json()) as BlinkGraphQlResponse<TData>;
    const graphQlError = payload.errors?.find((error) => error?.message);
    if (graphQlError?.message) {
      throw new BlinkClientError("Blink GraphQL request failed", 502, {
        provider: "blink",
        reason: graphQlError.message,
      });
    }

    if (!payload.data) {
      throw new BlinkClientError("Blink response was missing data", 502, {
        provider: "blink",
      });
    }

    return payload.data;
  }
}
