import axios from "axios";
import crypto from "crypto";
import {
  DasBudgetConfig,
  TokenResponse,
  Account,
  Transaction,
  Bucket,
  RefreshesResponse,
  Budget,
  BudgetsResponse,
  FREE_TO_SPEND,
  TransactionsOptions,
  AssignTransactionOptions,
  ApiOptions,
  PaginatedResponse,
  TransactionsResponse,
  AccountsResponse,
  RefreshOptions,
  AccountItem,
  ItemsResponse,
} from "./types";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require("../package.json");

export default class DasBudget {
  private refreshToken: string;
  private apiKey: string;
  private debug: boolean;
  private accessToken: string | null = null;
  private tokenExpiry: number | null = null;
  private userId: string | null = null;
  private budgetId: string | null = null;
  private readonly baseUrl = "https://api.dasbudget.com";

  constructor(config: DasBudgetConfig) {
    this.refreshToken = config.refreshToken;
    this.apiKey = config.apiKey;
    this.debug = config.debug || false;
  }

  private log(message: string) {
    if (this.debug) {
      console.log(`[DasBudget SDK] ${message}`);
    }
  }

  private async refreshAccessToken(): Promise<void> {
    try {
      this.log("Refreshing access token...");
      const params = new URLSearchParams({
        grant_type: "refresh_token",
        refresh_token: this.refreshToken,
      });
      const response = await axios.post<TokenResponse>(
        `https://securetoken.googleapis.com/v1/token?key=${this.apiKey}`,
        params.toString(),
        {
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            Accept: "*/*",
          },
        },
      );

      const token = response.data.id_token ?? response.data.access_token;
      const expiresInSeconds = Number(response.data.expires_in);

      if (!token) {
        throw new Error(
          "Token refresh response did not include id_token or access_token",
        );
      }

      if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
        throw new Error(
          `Token refresh response has invalid expires_in: ${response.data.expires_in}`,
        );
      }

      this.accessToken = token;
      this.tokenExpiry = Date.now() + expiresInSeconds * 1000;
      this.userId = response.data.user_id ?? this.userId;
      this.refreshToken = response.data.refresh_token ?? this.refreshToken;
      this.log("Access token refreshed successfully");
    } catch (error) {
      this.log("Error refreshing access token");
      throw error;
    }
  }

  private async ensureValidToken(): Promise<void> {
    const FIVE_MINUTES = 300000; // Get new token if within 5 minutes of expiry
    if (
      !this.accessToken ||
      !this.tokenExpiry ||
      Date.now() >= this.tokenExpiry - FIVE_MINUTES
    ) {
      await this.refreshAccessToken();
    }
  }

  /**
   * Sets the budget ID to use for all future API calls.
   * If not set, the oldest budget will be used by default.
   * @param budgetId The ID of the budget to use
   */
  public setBudgetId(budgetId: string | null): void {
    this.budgetId = budgetId;
    this.log(`Set budget ID to: ${budgetId ?? "null"}`);
  }

  private getHeaders(options?: ApiOptions): Record<string, string> {
    return {
      Authorization: `Bearer ${this.accessToken}`,
      Accept: "*/*",
      "Cache-Control": "no-cache",
      Pragma: "no-cache",
      Origin: "https://app.dasbudget.com",
      Referer: "https://app.dasbudget.com/",
      "X-Das-Context-Id": options?.budgetId ?? this.budgetId ?? "null",
      "X-Das-Platform": "web",
      "X-Das-Build": "216",
      "X-Das-Version": "0.12.0",
      "User-Agent": `klinquist/das-budget-sdk/${version}`,
    };
  }

  public async initialize(): Promise<void> {
    this.log("Initializing SDK...");
    await this.refreshAccessToken();
    this.log("SDK initialized successfully");
  }

  public async transactions(
    options?: TransactionsOptions,
  ): Promise<Transaction[]> {
    await this.ensureValidToken();
    this.log("Fetching transactions...");

    const since = options?.since;
    if (since !== undefined) {
      // Validate the since parameter
      if (typeof since !== "number" || isNaN(since)) {
        throw new Error(
          "since parameter must be a valid number (seconds since epoch)",
        );
      }

      // Log the since value in different formats for debugging
      this.log(`since parameter value: ${since}`);
      this.log(`since as milliseconds: ${since * 1000}`);
      this.log(`since as date: ${new Date(since * 1000).toISOString()}`);
    }

    const allTransactions: Transaction[] = [];
    let currentPage = 1;
    const limit = 40; // Using the API's default limit
    let hasMorePages = true;
    let totalFetched = 0;

    while (hasMorePages) {
      try {
        this.log(`Fetching page ${currentPage} with limit ${limit}...`);
        const response = await axios.get<TransactionsResponse>(
          `${this.baseUrl}/api/transaction`,
          {
            params: {
              page: currentPage,
              limit,
              types: "checking,credit card",
            },
            headers: this.getHeaders({
              budgetId: options?.budgetId,
            }),
          },
        );

        const { transactions, total } = response.data;
        this.log(
          `API Response - Page: ${currentPage}, Total: ${total}, Fetched: ${transactions.length}`,
        );
        this.log(
          `Fetched ${transactions.length} transactions from page ${currentPage}`,
        );

        // If we have a since parameter, filter transactions
        if (since !== undefined) {
          const filteredTransactions = transactions.filter((tx) => {
            const createdAt = new Date(tx.created_at).getTime() / 1000;
            this.log(
              `Transaction ${tx.id} created at: ${new Date(
                tx.created_at,
              ).toISOString()} (${createdAt})`,
            );
            this.log(
              `Comparing: ${createdAt} >= ${since} = ${createdAt >= since}`,
            );
            return createdAt >= since;
          });

          this.log(
            `Found ${filteredTransactions.length} transactions after ${new Date(
              since * 1000,
            ).toISOString()}`,
          );
          allTransactions.push(...filteredTransactions);
          totalFetched += filteredTransactions.length;

          // Only stop pagination if we got no transactions in this page
          // or if we got fewer transactions than the limit and none of them match our filter
          if (
            transactions.length === 0 ||
            (transactions.length < limit && filteredTransactions.length === 0)
          ) {
            this.log(
              "Reached end of transactions - no more matching transactions found",
            );
            hasMorePages = false;
          } else {
            currentPage++;
            this.log(
              `Moving to page ${currentPage} - found ${filteredTransactions.length} matching transactions`,
            );
          }
        } else {
          // If no since parameter, just return the first page
          this.log("No since parameter provided, returning first page only");
          return transactions;
        }
      } catch (error) {
        this.log("Error fetching transactions");
        throw error;
      }
    }

    this.log(
      `Returning ${
        allTransactions.length
      } total transactions (${totalFetched} fetched across ${
        currentPage - 1
      } pages)`,
    );
    return allTransactions;
  }

  private async getBucketsByKind(
    kind: "expense" | "goal" | "vault",
    options?: ApiOptions,
  ): Promise<Bucket[]> {
    await this.ensureValidToken();
    this.log(`Fetching ${kind}s...`);

    try {
      const response = await axios.get<PaginatedResponse<Bucket>>(
        `${this.baseUrl}/api/bucket`,
        {
          params: {
            page: 1,
            limit: 1000,
            kind,
            sort: "schedule_date,name_clean",
          },
          headers: this.getHeaders(options),
        },
      );

      return response.data.items;
    } catch (error) {
      this.log(`Error fetching ${kind}s`);
      throw error;
    }
  }

  public async expenses(options?: ApiOptions): Promise<Bucket[]> {
    return this.getBucketsByKind("expense", options);
  }

  public async goals(options?: ApiOptions): Promise<Bucket[]> {
    return this.getBucketsByKind("goal", options);
  }

  public async vaults(options?: ApiOptions): Promise<Bucket[]> {
    return this.getBucketsByKind("vault", options);
  }

  public async accounts(options?: ApiOptions): Promise<Account[]> {
    await this.ensureValidToken();
    this.log("Fetching accounts...");

    try {
      const response = await axios.get<AccountsResponse>(
        `${this.baseUrl}/api/item/account`,
        {
          params: {
            types: "checking,credit card",
          },
          headers: this.getHeaders(options),
        },
      );

      return response.data.items;
    } catch (error) {
      this.log("Error fetching accounts");
      throw error;
    }
  }

  public async assignTransactionToBucket(
    options: AssignTransactionOptions,
  ): Promise<Transaction> {
    await this.ensureValidToken();
    this.log(
      `Assigning transaction ${options.transactionId} to bucket ${options.bucketId}...`,
    );

    try {
      const actualBucketId =
        options.bucketId === FREE_TO_SPEND ? "fts" : options.bucketId;
      const response = await axios.post<Transaction>(
        `${this.baseUrl}/api/item/swap/${options.transactionId}/${actualBucketId}`,
        {},
        {
          headers: this.getHeaders({ budgetId: options.budgetId }),
        },
      );

      return response.data;
    } catch (error) {
      this.log("Error assigning transaction to bucket");
      throw error;
    }
  }

  public async refreshes(options?: ApiOptions): Promise<RefreshesResponse> {
    await this.ensureValidToken();
    this.log("Fetching refresh information...");

    try {
      const response = await axios.get<RefreshesResponse>(
        `${this.baseUrl}/api/item/refreshes`,
        {
          headers: this.getHeaders(options),
        },
      );

      return response.data;
    } catch (error) {
      this.log("Error fetching refresh information");
      throw error;
    }
  }

  /**
   * Refreshes the data for a specific account.
   *
   * @param options - The refresh options
   * @param options.itemId - The ID of the item to refresh (required)
   * @param options.usePremium - Whether to use premium refresh credits (optional, defaults to false)
   * @param options.budgetId - The ID of the budget to use (optional, defaults to the currently set budget)
   *
   * @throws {Error} If accountId is not provided
   * @throws {Error} If the account refresh fails
   *
   * @example
   * ```typescript
   * // Basic usage
   * await dasBudget.refresh({ itemId: "account-123" });
   *
   * // Using premium refresh
   * await dasBudget.refresh({
   *   itemId: "item-123",
   *   usePremium: true,
   *   budgetId: "budget-456"
   * });
   * ```
   */
  public async refresh(options: RefreshOptions): Promise<void> {
    if (!options?.itemId) {
      throw new Error("itemId is required for refresh");
    }

    if (typeof options.itemId !== "string" || options.itemId.trim() === "") {
      throw new Error("itemId must be a non-empty string");
    }

    if (
      options.usePremium !== undefined &&
      typeof options.usePremium !== "boolean"
    ) {
      throw new Error("usePremium must be a boolean if provided");
    }

    if (
      options.budgetId !== undefined &&
      (typeof options.budgetId !== "string" || options.budgetId.trim() === "")
    ) {
      throw new Error("budgetId must be a non-empty string if provided");
    }

    await this.ensureValidToken();
    this.log(`Refreshing item ${options.itemId}...`);

    try {
      await axios.post(
        `${this.baseUrl}/api/item/${options.itemId}/refresh`,
        {
          use_premium: options.usePremium ?? false,
          idempotency_key: crypto.randomUUID(),
          user_initiated: true,
        },
        {
          headers: this.getHeaders(),
        },
      );
    } catch (error) {
      this.log("Error refreshing account");
      throw error;
    }
  }

  public async budgets(): Promise<Budget[]> {
    await this.ensureValidToken();
    this.log("Fetching budgets...");

    try {
      const response = await axios.get<BudgetsResponse>(
        `${this.baseUrl}/api/context`,
        {
          headers: this.getHeaders(),
        },
      );

      return response.data.items;
    } catch (error) {
      this.log("Error fetching budgets");
      throw error;
    }
  }

  public async items(options?: ApiOptions): Promise<AccountItem[]> {
    await this.ensureValidToken();
    this.log("Fetching items...");

    try {
      const response = await axios.get<ItemsResponse>(
        `${this.baseUrl}/api/item`,
        {
          headers: this.getHeaders(options),
        },
      );

      return response.data.items;
    } catch (error) {
      this.log("Error fetching items");
      throw error;
    }
  }
}
