/**
 * QuickBooks Online API Client
 * Handles OAuth2 token management and all QBO REST API calls.
 */

import postgresClient from './postgres-client';
import {
  QboTokenRecord,
  QboTokenResponse,
  QboInvoice,
  QboPayment,
  QboDeposit,
  QboPurchase,
  QboJournalEntry,
  QboReport,
  QboQueryResponse,
  QboPaymentCreatePayload,
  QboDepositCreatePayload,
} from '@/lib/types/qbo';

const QBO_PRODUCTION_URL = 'https://quickbooks.api.intuit.com';
const QBO_SANDBOX_URL = 'https://sandbox-quickbooks.api.intuit.com';
const QBO_TOKEN_URL = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer';

export class QboClient {
  private clientId: string;
  private clientSecret: string;
  private realmId: string;
  private baseUrl: string;
  private _reportBasis: string | null = null;

  constructor() {
    this.clientId = process.env.QBO_CLIENT_ID || '';
    this.clientSecret = process.env.QBO_CLIENT_SECRET || '';
    this.realmId = process.env.QBO_REALM_ID || '';
    this.baseUrl = process.env.QBO_SANDBOX === 'true' ? QBO_SANDBOX_URL : QBO_PRODUCTION_URL;

    if (!this.clientId || !this.clientSecret || !this.realmId) {
      throw new Error('QBO_CLIENT_ID, QBO_CLIENT_SECRET, and QBO_REALM_ID must be set');
    }

    console.log(`[QboClient] Using ${process.env.QBO_SANDBOX === 'true' ? 'SANDBOX' : 'PRODUCTION'} environment`);
  }

  // ─── Token Management ───────────────────────────────────────────────────────

  async getValidAccessToken(): Promise<string> {
    const token = await this.loadToken();
    if (!token) {
      throw new Error('No QBO token found. Complete OAuth2 authorization first via /api/qbo/auth');
    }

    if (new Date() < new Date(token.access_token_expires_at)) {
      return token.access_token;
    }

    if (new Date() >= new Date(token.refresh_token_expires_at)) {
      throw new Error('QBO refresh token has expired. Re-authorization required via /api/qbo/auth');
    }

    return this.refreshAccessToken(token.refresh_token);
  }

  async loadToken(): Promise<QboTokenRecord | null> {
    const result = await postgresClient.query<QboTokenRecord>(
      `SELECT * FROM qbo_tokens WHERE realm_id = $1 LIMIT 1`,
      [this.realmId]
    );
    return result.rows[0] || null;
  }

  async saveToken(token: QboTokenResponse): Promise<void> {
    const now = new Date();
    const accessExpiry = new Date(now.getTime() + token.expires_in * 1000);
    const refreshExpiry = new Date(now.getTime() + token.x_refresh_token_expires_in * 1000);

    await postgresClient.query(
      `INSERT INTO qbo_tokens (realm_id, access_token, refresh_token, access_token_expires_at, refresh_token_expires_at, updated_at)
       VALUES ($1, $2, $3, $4, $5, NOW())
       ON CONFLICT (realm_id) DO UPDATE SET
         access_token = EXCLUDED.access_token,
         refresh_token = EXCLUDED.refresh_token,
         access_token_expires_at = EXCLUDED.access_token_expires_at,
         refresh_token_expires_at = EXCLUDED.refresh_token_expires_at,
         updated_at = NOW()`,
      [this.realmId, token.access_token, token.refresh_token, accessExpiry, refreshExpiry]
    );
  }

  async exchangeCodeForToken(authCode: string, redirectUri: string): Promise<QboTokenResponse> {
    const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
    const body = new URLSearchParams({
      grant_type: 'authorization_code',
      code: authCode,
      redirect_uri: redirectUri,
    });

    const res = await fetch(QBO_TOKEN_URL, {
      method: 'POST',
      headers: {
        Authorization: `Basic ${credentials}`,
        Accept: 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: body.toString(),
    });

    if (!res.ok) {
      const err = await res.text();
      throw new Error(`QBO token exchange failed: ${res.status} ${err}`);
    }

    const tokenData: QboTokenResponse = await res.json();
    await this.saveToken(tokenData);
    return tokenData;
  }

  private async refreshAccessToken(refreshToken: string): Promise<string> {
    const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
    const body = new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    });

    const res = await fetch(QBO_TOKEN_URL, {
      method: 'POST',
      headers: {
        Authorization: `Basic ${credentials}`,
        Accept: 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: body.toString(),
    });

    if (!res.ok) {
      const err = await res.text();
      throw new Error(`QBO token refresh failed: ${res.status} ${err}`);
    }

    const tokenData: QboTokenResponse = await res.json();
    await this.saveToken(tokenData);
    console.log('[QboClient] Access token refreshed successfully');
    return tokenData.access_token;
  }

  getAuthorizationUrl(redirectUri: string, state: string): string {
    const params = new URLSearchParams({
      client_id: this.clientId,
      scope: 'com.intuit.quickbooks.accounting',
      redirect_uri: redirectUri,
      response_type: 'code',
      state,
    });
    return `https://appcenter.intuit.com/connect/oauth2?${params.toString()}`;
  }

  // ─── Core API Request ────────────────────────────────────────────────────────

  private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
    const accessToken = await this.getValidAccessToken();
    const url = `${this.baseUrl}/v3/company/${this.realmId}${path}`;

    const res = await fetch(url, {
      ...options,
      redirect: 'follow',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        Accept: 'application/json',
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    const intuitTid = res.headers.get('intuit_tid') || res.headers.get('intuit-tid') || 'unknown';

    if (!res.ok) {
      const err = await res.text();
      throw new Error(`QBO API error ${res.status} on ${path} [intuit_tid=${intuitTid}]: ${err}`);
    }

    return res.json() as Promise<T>;
  }

  // ─── Paginated Query ─────────────────────────────────────────────────────────

  private async queryAll<T>(entity: string, extraWhere = ''): Promise<T[]> {
    const all: T[] = [];
    let startPos = 1;
    const pageSize = 1000;

    while (true) {
      const where = extraWhere ? ` WHERE ${extraWhere}` : '';
      const sql = encodeURIComponent(
        `SELECT * FROM ${entity}${where} STARTPOSITION ${startPos} MAXRESULTS ${pageSize}`
      );
      const data = await this.request<QboQueryResponse<T>>(`/query?query=${sql}&minorversion=65`);
      const items: T[] = (data.QueryResponse[entity] as T[]) || [];
      all.push(...items);

      if (items.length < pageSize) break;
      startPos += pageSize;
    }

    return all;
  }

  // ─── Entity Fetchers ─────────────────────────────────────────────────────────

  async getInvoices(updatedSince?: Date): Promise<QboInvoice[]> {
    const where = updatedSince
      ? `MetaData.LastUpdatedTime > '${updatedSince.toISOString()}'`
      : undefined;
    return this.queryAll<QboInvoice>('Invoice', where);
  }

  async getPayments(updatedSince?: Date): Promise<QboPayment[]> {
    const where = updatedSince
      ? `MetaData.LastUpdatedTime > '${updatedSince.toISOString()}'`
      : undefined;
    return this.queryAll<QboPayment>('Payment', where);
  }

  async getDeposits(updatedSince?: Date): Promise<QboDeposit[]> {
    const where = updatedSince
      ? `MetaData.LastUpdatedTime > '${updatedSince.toISOString()}'`
      : undefined;
    return this.queryAll<QboDeposit>('Deposit', where);
  }

  // ─── Entity Creators ─────────────────────────────────────────────────────────

  /**
   * Create a Receive Payment in QBO and apply it to one or more invoices.
   * Returns the created Payment as QBO echoes it back (includes Id, SyncToken).
   */
  async createPayment(payload: QboPaymentCreatePayload): Promise<QboPayment> {
    const data = await this.request<{ Payment: QboPayment }>(
      `/payment?minorversion=65`,
      {
        method: 'POST',
        body: JSON.stringify(payload),
      },
    );
    if (!data?.Payment?.Id) {
      throw new Error(`QBO createPayment returned no Payment: ${JSON.stringify(data).slice(0, 500)}`);
    }
    return data.Payment;
  }

  /**
   * Create a Deposit in QBO that groups one or more existing Payments into a
   * single bank-deposit line (so QBO's bank-feed reconciliation matches the
   * bank's actual deposit slip).
   */
  async createDeposit(payload: QboDepositCreatePayload): Promise<QboDeposit> {
    const data = await this.request<{ Deposit: QboDeposit }>(
      `/deposit?minorversion=65`,
      {
        method: 'POST',
        body: JSON.stringify(payload),
      },
    );
    if (!data?.Deposit?.Id) {
      throw new Error(`QBO createDeposit returned no Deposit: ${JSON.stringify(data).slice(0, 500)}`);
    }
    return data.Deposit;
  }

  async getPurchases(updatedSince?: Date): Promise<QboPurchase[]> {
    const where = updatedSince
      ? `MetaData.LastUpdatedTime > '${updatedSince.toISOString()}'`
      : undefined;
    return this.queryAll<QboPurchase>('Purchase', where);
  }

  async getJournalEntries(updatedSince?: Date): Promise<QboJournalEntry[]> {
    const where = updatedSince
      ? `MetaData.LastUpdatedTime > '${updatedSince.toISOString()}'`
      : undefined;
    return this.queryAll<QboJournalEntry>('JournalEntry', where);
  }

  async getPreferences(): Promise<{ reportBasis: string }> {
    if (this._reportBasis) return { reportBasis: this._reportBasis };
    const data = await this.request<{ Preferences?: { ReportPrefs?: { ReportBasis?: string } } }>('/preferences?minorversion=65');
    this._reportBasis = data?.Preferences?.ReportPrefs?.ReportBasis ?? 'Accrual';
    return { reportBasis: this._reportBasis };
  }

  async getProfitAndLoss(startDate: string, endDate: string, accountingMethod?: string): Promise<QboReport> {
    const params = new URLSearchParams({
      start_date: startDate,
      end_date: endDate,
      accounting_method: accountingMethod ?? 'Accrual',
      showrows: 'all',
      showcols: 'all',
      minorversion: '65',
    });
    return this.request<QboReport>(`/reports/ProfitAndLoss?${params.toString()}`);
  }

  async getBalanceSheet(startDate: string, endDate: string, accountingMethod?: string): Promise<QboReport> {
    const params = new URLSearchParams({
      start_date: startDate,
      end_date: endDate,
      accounting_method: accountingMethod ?? 'Accrual',
      showrows: 'all',
      showcols: 'all',
      minorversion: '65',
    });
    return this.request<QboReport>(`/reports/BalanceSheet?${params.toString()}`);
  }

  async getCashFlow(startDate: string, endDate: string): Promise<QboReport> {
    const params = new URLSearchParams({
      start_date: startDate,
      end_date: endDate,
      minorversion: '65',
    });
    return this.request<QboReport>(`/reports/CashFlow?${params.toString()}`);
  }

  // A/R Aging Detail — one row per open invoice, with customer + balance + days
  // past due.  Used to reconcile Pulse's qbo_invoices against QBO's view of A/R.
  async getAgedReceivableDetail(asOfDate?: string): Promise<QboReport> {
    const params = new URLSearchParams({
      report_date: asOfDate ?? new Date().toISOString().slice(0, 10),
      aging_method: 'Report_Date',
      minorversion: '65',
    });
    return this.request<QboReport>(`/reports/AgedReceivableDetail?${params.toString()}`);
  }

  async getAgedReceivableSummary(asOfDate?: string): Promise<QboReport> {
    const params = new URLSearchParams({
      report_date: asOfDate ?? new Date().toISOString().slice(0, 10),
      aging_method: 'Report_Date',
      minorversion: '65',
    });
    return this.request<QboReport>(`/reports/AgedReceivables?${params.toString()}`);
  }
}

let _instance: QboClient | null = null;

export function getQboClient(): QboClient {
  if (!_instance) {
    _instance = new QboClient();
  }
  return _instance;
}
