/**
 * Xero API Client
 * Handles authentication, rate limiting, pagination, and all CRUD operations
 */

import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import type {
  XeroClientConfig,
  XeroRequestOptions,
  XeroApiResponse,
  XeroErrorResponse,
  Invoice,
  Contact,
  Account,
  BankTransaction,
  Payment,
  CreditNote,
  PurchaseOrder,
  Quote,
  ManualJournal,
  Item,
  TaxRate,
  Employee,
  Organisation,
  TrackingCategory,
  BrandingTheme,
  ContactGroup,
  Prepayment,
  Overpayment,
  BankTransfer,
  Attachment,
  Report,
  InvoiceId,
  ContactId,
  AccountId,
  BankTransactionId,
  PaymentId,
  CreditNoteId,
  PurchaseOrderId,
  QuoteId,
  ManualJournalId,
  ItemId,
  EmployeeId,
  TrackingCategoryId,
  PrepaymentId,
  OverpaymentId,
  BankTransferId
} from '../types/index.js';

export class XeroClient {
  private client: AxiosInstance;
  private config: XeroClientConfig;
  private requestCount = 0;
  private requestWindowStart = Date.now();
  private readonly RATE_LIMIT_PER_MINUTE = 60;
  private readonly RATE_LIMIT_PER_DAY = 5000;
  private dailyRequestCount = 0;
  private dailyWindowStart = Date.now();

  constructor(config: XeroClientConfig) {
    this.config = {
      baseUrl: 'https://api.xero.com/api.xro/2.0',
      timeout: 30000,
      retryAttempts: 3,
      retryDelayMs: 1000,
      ...config
    };

    this.client = axios.create({
      baseURL: this.config.baseUrl,
      timeout: this.config.timeout,
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'xero-tenant-id': this.config.tenantId,
        'Authorization': `Bearer ${this.config.accessToken}`
      }
    });

    // Response interceptor for error handling
    this.client.interceptors.response.use(
      response => response,
      error => this.handleError(error)
    );
  }

  /**
   * Rate limiting: 60 calls/min, 5000/day
   */
  private async checkRateLimit(): Promise<void> {
    const now = Date.now();
    const minuteElapsed = now - this.requestWindowStart;
    const dayElapsed = now - this.dailyWindowStart;

    // Reset minute window
    if (minuteElapsed >= 60000) {
      this.requestCount = 0;
      this.requestWindowStart = now;
    }

    // Reset daily window
    if (dayElapsed >= 86400000) {
      this.dailyRequestCount = 0;
      this.dailyWindowStart = now;
    }

    // Check limits
    if (this.requestCount >= this.RATE_LIMIT_PER_MINUTE) {
      const waitTime = 60000 - minuteElapsed;
      await this.sleep(waitTime);
      this.requestCount = 0;
      this.requestWindowStart = Date.now();
    }

    if (this.dailyRequestCount >= this.RATE_LIMIT_PER_DAY) {
      throw new Error('Daily rate limit exceeded (5000 requests/day)');
    }

    this.requestCount++;
    this.dailyRequestCount++;
  }

  /**
   * Handle API errors with retry logic
   */
  private async handleError(error: AxiosError): Promise<never> {
    if (error.response?.status === 429) {
      // Rate limit hit - wait and retry
      const retryAfter = parseInt(error.response.headers['retry-after'] || '60', 10);
      await this.sleep(retryAfter * 1000);
      throw error; // Will be retried by makeRequest
    }

    const xeroError = error.response?.data as XeroErrorResponse | undefined;
    if (xeroError) {
      const message = xeroError.Detail || xeroError.Title || 'Unknown Xero API error';
      throw new Error(`Xero API Error: ${message}`);
    }

    throw error;
  }

  /**
   * Make an API request with retry logic
   */
  private async makeRequest<T>(
    config: AxiosRequestConfig,
    options?: XeroRequestOptions,
    attempt = 1
  ): Promise<T> {
    await this.checkRateLimit();

    try {
      // Add If-Modified-Since header if provided
      if (options?.ifModifiedSince) {
        config.headers = {
          ...config.headers,
          'If-Modified-Since': options.ifModifiedSince.toUTCString()
        };
      }

      // Add query parameters
      if (options) {
        const params: Record<string, string | number | boolean> = {};
        if (options.page !== undefined) params.page = options.page;
        if (options.pageSize !== undefined) params.pageSize = options.pageSize;
        if (options.where) params.where = options.where;
        if (options.order) params.order = options.order;
        if (options.includeArchived) params.includeArchived = true;
        if (options.summarizeErrors) params.summarizeErrors = true;

        config.params = { ...config.params, ...params };
      }

      const response = await this.client.request<XeroApiResponse<T>>(config);
      
      // Extract the actual data from the Xero response wrapper
      // Xero wraps responses in objects like { Invoices: [...] }
      const data = response.data;
      const keys = Object.keys(data).filter(k => !['Id', 'Status', 'ProviderName', 'DateTimeUTC'].includes(k));
      const dataKey = keys[0];
      
      return (dataKey ? data[dataKey] : data) as T;
    } catch (error) {
      if (attempt < (this.config.retryAttempts || 3)) {
        await this.sleep((this.config.retryDelayMs || 1000) * attempt);
        return this.makeRequest<T>(config, options, attempt + 1);
      }
      throw error;
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // ==================== INVOICES ====================

  async getInvoices(options?: XeroRequestOptions): Promise<Invoice[]> {
    return this.makeRequest<Invoice[]>({ method: 'GET', url: '/Invoices' }, options);
  }

  async getInvoice(invoiceId: InvoiceId): Promise<Invoice> {
    const invoices = await this.makeRequest<Invoice[]>({
      method: 'GET',
      url: `/Invoices/${invoiceId}`
    });
    return invoices[0];
  }

  async createInvoices(invoices: Invoice[]): Promise<Invoice[]> {
    return this.makeRequest<Invoice[]>({
      method: 'PUT',
      url: '/Invoices',
      data: { Invoices: invoices }
    });
  }

  async createInvoice(invoice: Invoice): Promise<Invoice> {
    const result = await this.createInvoices([invoice]);
    return result[0];
  }

  async updateInvoice(invoiceId: InvoiceId, invoice: Partial<Invoice>): Promise<Invoice> {
    const invoices = await this.makeRequest<Invoice[]>({
      method: 'POST',
      url: `/Invoices/${invoiceId}`,
      data: { Invoices: [invoice] }
    });
    return invoices[0];
  }

  async deleteInvoice(invoiceId: InvoiceId): Promise<void> {
    await this.updateInvoice(invoiceId, { Status: 'DELETED' as any });
  }

  // ==================== CONTACTS ====================

  async getContacts(options?: XeroRequestOptions): Promise<Contact[]> {
    return this.makeRequest<Contact[]>({ method: 'GET', url: '/Contacts' }, options);
  }

  async getContact(contactId: ContactId): Promise<Contact> {
    const contacts = await this.makeRequest<Contact[]>({
      method: 'GET',
      url: `/Contacts/${contactId}`
    });
    return contacts[0];
  }

  async createContacts(contacts: Contact[]): Promise<Contact[]> {
    return this.makeRequest<Contact[]>({
      method: 'PUT',
      url: '/Contacts',
      data: { Contacts: contacts }
    });
  }

  async createContact(contact: Contact): Promise<Contact> {
    const result = await this.createContacts([contact]);
    return result[0];
  }

  async updateContact(contactId: ContactId, contact: Partial<Contact>): Promise<Contact> {
    const contacts = await this.makeRequest<Contact[]>({
      method: 'POST',
      url: `/Contacts/${contactId}`,
      data: { Contacts: [contact] }
    });
    return contacts[0];
  }

  // ==================== ACCOUNTS ====================

  async getAccounts(options?: XeroRequestOptions): Promise<Account[]> {
    return this.makeRequest<Account[]>({ method: 'GET', url: '/Accounts' }, options);
  }

  async getAccount(accountId: AccountId): Promise<Account> {
    const accounts = await this.makeRequest<Account[]>({
      method: 'GET',
      url: `/Accounts/${accountId}`
    });
    return accounts[0];
  }

  async createAccount(account: Account): Promise<Account> {
    const accounts = await this.makeRequest<Account[]>({
      method: 'PUT',
      url: '/Accounts',
      data: { Accounts: [account] }
    });
    return accounts[0];
  }

  async updateAccount(accountId: AccountId, account: Partial<Account>): Promise<Account> {
    const accounts = await this.makeRequest<Account[]>({
      method: 'POST',
      url: `/Accounts/${accountId}`,
      data: { Accounts: [account] }
    });
    return accounts[0];
  }

  async deleteAccount(accountId: AccountId): Promise<void> {
    await this.updateAccount(accountId, { Status: 'DELETED' });
  }

  // ==================== BANK TRANSACTIONS ====================

  async getBankTransactions(options?: XeroRequestOptions): Promise<BankTransaction[]> {
    return this.makeRequest<BankTransaction[]>({
      method: 'GET',
      url: '/BankTransactions'
    }, options);
  }

  async getBankTransaction(bankTransactionId: BankTransactionId): Promise<BankTransaction> {
    const transactions = await this.makeRequest<BankTransaction[]>({
      method: 'GET',
      url: `/BankTransactions/${bankTransactionId}`
    });
    return transactions[0];
  }

  async createBankTransactions(transactions: BankTransaction[]): Promise<BankTransaction[]> {
    return this.makeRequest<BankTransaction[]>({
      method: 'PUT',
      url: '/BankTransactions',
      data: { BankTransactions: transactions }
    });
  }

  async createBankTransaction(transaction: BankTransaction): Promise<BankTransaction> {
    const result = await this.createBankTransactions([transaction]);
    return result[0];
  }

  async updateBankTransaction(
    bankTransactionId: BankTransactionId,
    transaction: Partial<BankTransaction>
  ): Promise<BankTransaction> {
    const transactions = await this.makeRequest<BankTransaction[]>({
      method: 'POST',
      url: `/BankTransactions/${bankTransactionId}`,
      data: { BankTransactions: [transaction] }
    });
    return transactions[0];
  }

  // ==================== PAYMENTS ====================

  async getPayments(options?: XeroRequestOptions): Promise<Payment[]> {
    return this.makeRequest<Payment[]>({ method: 'GET', url: '/Payments' }, options);
  }

  async getPayment(paymentId: PaymentId): Promise<Payment> {
    const payments = await this.makeRequest<Payment[]>({
      method: 'GET',
      url: `/Payments/${paymentId}`
    });
    return payments[0];
  }

  async createPayments(payments: Payment[]): Promise<Payment[]> {
    return this.makeRequest<Payment[]>({
      method: 'PUT',
      url: '/Payments',
      data: { Payments: payments }
    });
  }

  async createPayment(payment: Payment): Promise<Payment> {
    const result = await this.createPayments([payment]);
    return result[0];
  }

  async deletePayment(paymentId: PaymentId): Promise<void> {
    await this.makeRequest({
      method: 'POST',
      url: `/Payments/${paymentId}`,
      data: { Payments: [{ Status: 'DELETED' }] }
    });
  }

  // ==================== CREDIT NOTES ====================

  async getCreditNotes(options?: XeroRequestOptions): Promise<CreditNote[]> {
    return this.makeRequest<CreditNote[]>({ method: 'GET', url: '/CreditNotes' }, options);
  }

  async getCreditNote(creditNoteId: CreditNoteId): Promise<CreditNote> {
    const creditNotes = await this.makeRequest<CreditNote[]>({
      method: 'GET',
      url: `/CreditNotes/${creditNoteId}`
    });
    return creditNotes[0];
  }

  async createCreditNotes(creditNotes: CreditNote[]): Promise<CreditNote[]> {
    return this.makeRequest<CreditNote[]>({
      method: 'PUT',
      url: '/CreditNotes',
      data: { CreditNotes: creditNotes }
    });
  }

  async createCreditNote(creditNote: CreditNote): Promise<CreditNote> {
    const result = await this.createCreditNotes([creditNote]);
    return result[0];
  }

  async updateCreditNote(
    creditNoteId: CreditNoteId,
    creditNote: Partial<CreditNote>
  ): Promise<CreditNote> {
    const creditNotes = await this.makeRequest<CreditNote[]>({
      method: 'POST',
      url: `/CreditNotes/${creditNoteId}`,
      data: { CreditNotes: [creditNote] }
    });
    return creditNotes[0];
  }

  // ==================== PURCHASE ORDERS ====================

  async getPurchaseOrders(options?: XeroRequestOptions): Promise<PurchaseOrder[]> {
    return this.makeRequest<PurchaseOrder[]>({
      method: 'GET',
      url: '/PurchaseOrders'
    }, options);
  }

  async getPurchaseOrder(purchaseOrderId: PurchaseOrderId): Promise<PurchaseOrder> {
    const orders = await this.makeRequest<PurchaseOrder[]>({
      method: 'GET',
      url: `/PurchaseOrders/${purchaseOrderId}`
    });
    return orders[0];
  }

  async createPurchaseOrders(purchaseOrders: PurchaseOrder[]): Promise<PurchaseOrder[]> {
    return this.makeRequest<PurchaseOrder[]>({
      method: 'PUT',
      url: '/PurchaseOrders',
      data: { PurchaseOrders: purchaseOrders }
    });
  }

  async createPurchaseOrder(purchaseOrder: PurchaseOrder): Promise<PurchaseOrder> {
    const result = await this.createPurchaseOrders([purchaseOrder]);
    return result[0];
  }

  async updatePurchaseOrder(
    purchaseOrderId: PurchaseOrderId,
    purchaseOrder: Partial<PurchaseOrder>
  ): Promise<PurchaseOrder> {
    const orders = await this.makeRequest<PurchaseOrder[]>({
      method: 'POST',
      url: `/PurchaseOrders/${purchaseOrderId}`,
      data: { PurchaseOrders: [purchaseOrder] }
    });
    return orders[0];
  }

  // ==================== QUOTES ====================

  async getQuotes(options?: XeroRequestOptions): Promise<Quote[]> {
    return this.makeRequest<Quote[]>({ method: 'GET', url: '/Quotes' }, options);
  }

  async getQuote(quoteId: QuoteId): Promise<Quote> {
    const quotes = await this.makeRequest<Quote[]>({
      method: 'GET',
      url: `/Quotes/${quoteId}`
    });
    return quotes[0];
  }

  async createQuotes(quotes: Quote[]): Promise<Quote[]> {
    return this.makeRequest<Quote[]>({
      method: 'PUT',
      url: '/Quotes',
      data: { Quotes: quotes }
    });
  }

  async createQuote(quote: Quote): Promise<Quote> {
    const result = await this.createQuotes([quote]);
    return result[0];
  }

  async updateQuote(quoteId: QuoteId, quote: Partial<Quote>): Promise<Quote> {
    const quotes = await this.makeRequest<Quote[]>({
      method: 'POST',
      url: `/Quotes/${quoteId}`,
      data: { Quotes: [quote] }
    });
    return quotes[0];
  }

  // ==================== MANUAL JOURNALS ====================

  async getManualJournals(options?: XeroRequestOptions): Promise<ManualJournal[]> {
    return this.makeRequest<ManualJournal[]>({
      method: 'GET',
      url: '/ManualJournals'
    }, options);
  }

  async getManualJournal(manualJournalId: ManualJournalId): Promise<ManualJournal> {
    const journals = await this.makeRequest<ManualJournal[]>({
      method: 'GET',
      url: `/ManualJournals/${manualJournalId}`
    });
    return journals[0];
  }

  async createManualJournals(manualJournals: ManualJournal[]): Promise<ManualJournal[]> {
    return this.makeRequest<ManualJournal[]>({
      method: 'PUT',
      url: '/ManualJournals',
      data: { ManualJournals: manualJournals }
    });
  }

  async createManualJournal(manualJournal: ManualJournal): Promise<ManualJournal> {
    const result = await this.createManualJournals([manualJournal]);
    return result[0];
  }

  async updateManualJournal(
    manualJournalId: ManualJournalId,
    manualJournal: Partial<ManualJournal>
  ): Promise<ManualJournal> {
    const journals = await this.makeRequest<ManualJournal[]>({
      method: 'POST',
      url: `/ManualJournals/${manualJournalId}`,
      data: { ManualJournals: [manualJournal] }
    });
    return journals[0];
  }

  // ==================== ITEMS ====================

  async getItems(options?: XeroRequestOptions): Promise<Item[]> {
    return this.makeRequest<Item[]>({ method: 'GET', url: '/Items' }, options);
  }

  async getItem(itemId: ItemId): Promise<Item> {
    const items = await this.makeRequest<Item[]>({
      method: 'GET',
      url: `/Items/${itemId}`
    });
    return items[0];
  }

  async createItems(items: Item[]): Promise<Item[]> {
    return this.makeRequest<Item[]>({
      method: 'PUT',
      url: '/Items',
      data: { Items: items }
    });
  }

  async createItem(item: Item): Promise<Item> {
    const result = await this.createItems([item]);
    return result[0];
  }

  async updateItem(itemId: ItemId, item: Partial<Item>): Promise<Item> {
    const items = await this.makeRequest<Item[]>({
      method: 'POST',
      url: `/Items/${itemId}`,
      data: { Items: [item] }
    });
    return items[0];
  }

  // ==================== TAX RATES ====================

  async getTaxRates(options?: XeroRequestOptions): Promise<TaxRate[]> {
    return this.makeRequest<TaxRate[]>({ method: 'GET', url: '/TaxRates' }, options);
  }

  async createTaxRates(taxRates: TaxRate[]): Promise<TaxRate[]> {
    return this.makeRequest<TaxRate[]>({
      method: 'PUT',
      url: '/TaxRates',
      data: { TaxRates: taxRates }
    });
  }

  async createTaxRate(taxRate: TaxRate): Promise<TaxRate> {
    const result = await this.createTaxRates([taxRate]);
    return result[0];
  }

  // ==================== EMPLOYEES ====================

  async getEmployees(options?: XeroRequestOptions): Promise<Employee[]> {
    return this.makeRequest<Employee[]>({ method: 'GET', url: '/Employees' }, options);
  }

  async getEmployee(employeeId: EmployeeId): Promise<Employee> {
    const employees = await this.makeRequest<Employee[]>({
      method: 'GET',
      url: `/Employees/${employeeId}`
    });
    return employees[0];
  }

  async createEmployees(employees: Employee[]): Promise<Employee[]> {
    return this.makeRequest<Employee[]>({
      method: 'PUT',
      url: '/Employees',
      data: { Employees: employees }
    });
  }

  async createEmployee(employee: Employee): Promise<Employee> {
    const result = await this.createEmployees([employee]);
    return result[0];
  }

  // ==================== ORGANISATION ====================

  async getOrganisations(): Promise<Organisation[]> {
    return this.makeRequest<Organisation[]>({ method: 'GET', url: '/Organisation' });
  }

  // ==================== TRACKING CATEGORIES ====================

  async getTrackingCategories(options?: XeroRequestOptions): Promise<TrackingCategory[]> {
    return this.makeRequest<TrackingCategory[]>({
      method: 'GET',
      url: '/TrackingCategories'
    }, options);
  }

  async getTrackingCategory(trackingCategoryId: TrackingCategoryId): Promise<TrackingCategory> {
    const categories = await this.makeRequest<TrackingCategory[]>({
      method: 'GET',
      url: `/TrackingCategories/${trackingCategoryId}`
    });
    return categories[0];
  }

  async createTrackingCategory(trackingCategory: TrackingCategory): Promise<TrackingCategory> {
    const categories = await this.makeRequest<TrackingCategory[]>({
      method: 'PUT',
      url: '/TrackingCategories',
      data: { TrackingCategories: [trackingCategory] }
    });
    return categories[0];
  }

  // ==================== BRANDING THEMES ====================

  async getBrandingThemes(): Promise<BrandingTheme[]> {
    return this.makeRequest<BrandingTheme[]>({ method: 'GET', url: '/BrandingThemes' });
  }

  // ==================== CONTACT GROUPS ====================

  async getContactGroups(options?: XeroRequestOptions): Promise<ContactGroup[]> {
    return this.makeRequest<ContactGroup[]>({
      method: 'GET',
      url: '/ContactGroups'
    }, options);
  }

  async createContactGroup(contactGroup: ContactGroup): Promise<ContactGroup> {
    const groups = await this.makeRequest<ContactGroup[]>({
      method: 'PUT',
      url: '/ContactGroups',
      data: { ContactGroups: [contactGroup] }
    });
    return groups[0];
  }

  // ==================== PREPAYMENTS ====================

  async getPrepayments(options?: XeroRequestOptions): Promise<Prepayment[]> {
    return this.makeRequest<Prepayment[]>({ method: 'GET', url: '/Prepayments' }, options);
  }

  async getPrepayment(prepaymentId: PrepaymentId): Promise<Prepayment> {
    const prepayments = await this.makeRequest<Prepayment[]>({
      method: 'GET',
      url: `/Prepayments/${prepaymentId}`
    });
    return prepayments[0];
  }

  // ==================== OVERPAYMENTS ====================

  async getOverpayments(options?: XeroRequestOptions): Promise<Overpayment[]> {
    return this.makeRequest<Overpayment[]>({ method: 'GET', url: '/Overpayments' }, options);
  }

  async getOverpayment(overpaymentId: OverpaymentId): Promise<Overpayment> {
    const overpayments = await this.makeRequest<Overpayment[]>({
      method: 'GET',
      url: `/Overpayments/${overpaymentId}`
    });
    return overpayments[0];
  }

  // ==================== BANK TRANSFERS ====================

  async getBankTransfers(options?: XeroRequestOptions): Promise<BankTransfer[]> {
    return this.makeRequest<BankTransfer[]>({ method: 'GET', url: '/BankTransfers' }, options);
  }

  async getBankTransfer(bankTransferId: BankTransferId): Promise<BankTransfer> {
    const transfers = await this.makeRequest<BankTransfer[]>({
      method: 'GET',
      url: `/BankTransfers/${bankTransferId}`
    });
    return transfers[0];
  }

  async createBankTransfer(bankTransfer: BankTransfer): Promise<BankTransfer> {
    const transfers = await this.makeRequest<BankTransfer[]>({
      method: 'PUT',
      url: '/BankTransfers',
      data: { BankTransfers: [bankTransfer] }
    });
    return transfers[0];
  }

  // ==================== REPORTS ====================

  async getReport(reportUrl: string, options?: XeroRequestOptions): Promise<Report> {
    const reports = await this.makeRequest<Report[]>({
      method: 'GET',
      url: reportUrl
    }, options);
    return reports[0];
  }

  async getBalanceSheet(date?: string, periods?: number): Promise<Report> {
    const params: Record<string, string | number> = {};
    if (date) params.date = date;
    if (periods) params.periods = periods;
    
    return this.getReport('/Reports/BalanceSheet', { ...params } as any);
  }

  async getProfitAndLoss(fromDate?: string, toDate?: string): Promise<Report> {
    const params: Record<string, string> = {};
    if (fromDate) params.fromDate = fromDate;
    if (toDate) params.toDate = toDate;
    
    return this.getReport('/Reports/ProfitAndLoss', { ...params } as any);
  }

  async getTrialBalance(date?: string): Promise<Report> {
    const params: Record<string, string> = {};
    if (date) params.date = date;
    
    return this.getReport('/Reports/TrialBalance', { ...params } as any);
  }

  async getBankSummary(fromDate?: string, toDate?: string): Promise<Report> {
    const params: Record<string, string> = {};
    if (fromDate) params.fromDate = fromDate;
    if (toDate) params.toDate = toDate;
    
    return this.getReport('/Reports/BankSummary', { ...params } as any);
  }

  async getExecutiveSummary(date?: string): Promise<Report> {
    const params: Record<string, string> = {};
    if (date) params.date = date;
    
    return this.getReport('/Reports/ExecutiveSummary', { ...params } as any);
  }

  // ==================== ATTACHMENTS ====================

  async getAttachments(endpoint: string, entityId: string): Promise<Attachment[]> {
    return this.makeRequest<Attachment[]>({
      method: 'GET',
      url: `/${endpoint}/${entityId}/Attachments`
    });
  }

  async uploadAttachment(
    endpoint: string,
    entityId: string,
    fileName: string,
    fileContent: Buffer,
    mimeType: string
  ): Promise<Attachment> {
    const attachments = await this.makeRequest<Attachment[]>({
      method: 'PUT',
      url: `/${endpoint}/${entityId}/Attachments/${fileName}`,
      data: fileContent,
      headers: {
        'Content-Type': mimeType
      }
    });
    return attachments[0];
  }
}
