import { Buffer } from 'node:buffer';

export class FinanceClient {
  constructor(config) {
    if (!config?.baseUrl) {
      throw new Error('FinanceClient requires a baseUrl');
    }

    this.baseUrl = config.baseUrl.replace(/\/$/, '');
    this.apiKey = config.apiKey;
    this.basicAuth = validateBasicAuth(config.basicAuth);
    this.timeoutMs = config.timeoutMs;
  }

  async listAccounts(params = {}) {
    return this.get('/api/finance/accounts', params);
  }

  async listBudgets() {
    return this.get('/api/finance/budgets');
  }

  async createBudget(payload) {
    return this.post('/api/finance/budgets', payload);
  }

  async updateBudget(id, payload) {
    return this.put(`/api/finance/budgets/${id}`, payload);
  }

  async deleteBudget(id) {
    return this.delete(`/api/finance/budgets/${id}`);
  }

  async listCategories(params = {}) {
    return this.get('/api/finance/categories', params);
  }

  async createCategory(payload) {
    return this.post('/api/finance/categories', payload);
  }

  async updateCategory(id, payload) {
    return this.put(`/api/finance/categories/${id}`, payload);
  }

  async deleteCategory(id) {
    return this.delete(`/api/finance/categories/${id}`);
  }

  async listCurrencies() {
    return this.get('/api/finance/currencies');
  }

  async listExchangeRates(params = {}) {
    const query = {};
    if (params.fromCurrency) query.from = params.fromCurrency;
    if (params.toCurrency) query.to = params.toCurrency;
    if (params.date) query.date = params.date;
    return this.get('/api/finance/exchange-rates', query);
  }

  async listTransactions(params = {}) {
    const query = {};
    if (params.type) query.type = params.type;
    if (params.statuses?.length) {
      query.statuses = params.statuses.join(',');
    }
    if (params.includeDeleted !== undefined) {
      query.includeDeleted = params.includeDeleted;
    }
    return this.get('/api/finance/transactions', query);
  }

  async createTransaction(payload) {
    return this.post('/api/finance/transactions', payload);
  }

  async updateTransaction(id, payload) {
    return this.put(`/api/finance/transactions/${id}`, payload);
  }

  async deleteTransaction(id) {
    return this.delete(`/api/finance/transactions/${id}`);
  }

  async listReimbursements(params = {}) {
    const query = {};
    if (params.type) query.type = params.type;
    if (params.statuses?.length) {
      query.statuses = params.statuses.join(',');
    }
    if (params.includeDeleted !== undefined) {
      query.includeDeleted = params.includeDeleted;
    }
    return this.get('/api/finance/reimbursements', query);
  }

  async createReimbursement(payload) {
    return this.post('/api/finance/reimbursements', payload);
  }

  async updateReimbursement(id, payload) {
    return this.put(`/api/finance/reimbursements/${id}`, payload);
  }

  async listMedia(params = {}) {
    const query = {};
    if (params.limit !== undefined) query.limit = params.limit;
    if (params.fileTypes?.length) query.types = params.fileTypes.join(',');
    return this.get('/api/finance/media', query);
  }

  async getMediaById(id) {
    return this.get(`/api/finance/media/${id}`);
  }

  async downloadMedia(id) {
    const url = this.createUrl(`/api/finance/media/${id}/download`);
    const response = await this.performFetch(url, {
      method: 'GET',
      headers: this.buildHeaders(false),
    });

    if (!response.ok) {
      const payload = await this.safeParseEnvelope(response);
      if (payload) {
        throw new Error(payload.message || 'Failed to download media file');
      }
      throw new Error(`Failed to download media file (HTTP ${response.status})`);
    }

    const arrayBuffer = await response.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);

    return {
      fileName: this.extractFileName(response.headers.get('content-disposition')),
      mimeType: response.headers.get('content-type') ?? 'application/octet-stream',
      size: buffer.byteLength,
      base64: buffer.toString('base64'),
    };
  }

  async get(path, query) {
    return this.request('GET', path, { query });
  }

  async post(path, body) {
    return this.request('POST', path, { body });
  }

  async put(path, body) {
    return this.request('PUT', path, { body });
  }

  async delete(path) {
    return this.request('DELETE', path);
  }

  async request(method, path, options = {}) {
    const url = this.createUrl(path);

    if (options.query) {
      for (const [key, value] of Object.entries(options.query)) {
        if (value === undefined || value === null) continue;
        if (Array.isArray(value)) {
          if (value.length > 0) url.searchParams.set(key, value.join(','));
        } else if (typeof value === 'boolean') {
          url.searchParams.set(key, value ? 'true' : 'false');
        } else {
          url.searchParams.set(key, String(value));
        }
      }
    }

    const response = await this.performFetch(url, {
      method,
      headers: this.buildHeaders(method !== 'GET' && method !== 'DELETE'),
      body: options.body ? JSON.stringify(options.body) : undefined,
    });

    const payload = await this.parseEnvelope(response, path);

    if (payload.code !== 0) {
      throw new Error(payload.message || 'Finance API returned an error');
    }

    return payload.data;
  }

  createUrl(path) {
    if (!path.startsWith('/')) {
      path = `/${path}`;
    }
    return new URL(path, this.baseUrl);
  }

  buildHeaders(json) {
    const headers = { Accept: 'application/json' };
    if (json) headers['Content-Type'] = 'application/json';
    if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
    else if (this.basicAuth) headers.Authorization = `Basic ${createBasicToken(this.basicAuth)}`;
    return headers;
  }

  async performFetch(url, init) {
    const controller = this.timeoutMs ? new AbortController() : undefined;
    let timer;

    if (controller) {
      init.signal = controller.signal;
      timer = setTimeout(() => controller.abort(), this.timeoutMs);
    }

    try {
      return await fetch(url, init);
    } catch (error) {
      if (error?.name === 'AbortError') {
        throw new Error(`Request to ${url.pathname} timed out`);
      }
      throw error;
    } finally {
      if (timer) clearTimeout(timer);
    }
  }

  async parseEnvelope(response, path) {
    const payload = await this.safeParseEnvelope(response);

    if (!payload) {
      const text = await response.text();
      throw new Error(`Unexpected response from ${path}: ${text || response.statusText}`);
    }

    if (!response.ok) {
      throw new Error(payload.message || `Finance API request failed (HTTP ${response.status})`);
    }

    return payload;
  }

  async safeParseEnvelope(response) {
    const contentType = response.headers.get('content-type') || '';
    if (!contentType.includes('application/json')) return null;

    try {
      return await response.clone().json();
    } catch {
      return null;
    }
  }

  extractFileName(contentDisposition) {
    if (!contentDisposition) return undefined;

    const filenameStar = contentDisposition.match(/filename\*=([^;]+)/i);
    if (filenameStar?.[1]) {
      const value = filenameStar[1].replace(/^UTF-8''/, '');
      try {
        return decodeURIComponent(value);
      } catch {
        return value;
      }
    }

    const filename = contentDisposition.match(/filename="?([^";]+)"?/i);
    return filename?.[1];
  }
}

const validateBasicAuth = (credentials) => {
  if (!credentials) return undefined;
  const username = credentials.username ?? credentials.user ?? credentials.login;
  const password = credentials.password ?? credentials.pass;

  if (!username && !password) return undefined;
  if (!username || !password) {
    throw new Error('FinanceClient basicAuth requires both username and password');
  }

  return { username: String(username), password: String(password) };
};

const createBasicToken = ({ username, password }) =>
  Buffer.from(`${username}:${password}`, 'utf8').toString('base64');
