import { addHours, subYears } from 'date-fns';
import { GraphQLError } from 'graphql';
import { Inject, Injectable, Scope } from 'graphql-modules';
import { Currency } from '../../../shared/enums.js';
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
import { ENVIRONMENT } from '../../../shared/tokens.js';
import type { Environment, TimelessDateString } from '../../../shared/types/index.js';
import { ProviderCredentialsProvider } from '../../provider-credentials/providers/provider-credentials.provider.js';
import {
  ContractSchema,
  downloadInvoicePdfSchema,
  Invoice,
  retrieveInvoicesSchema,
  retrievePaymentBreakdownSchema,
  retrievePaymentReceiptsSchema,
} from './schemas.js';

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class DeelClientProvider {
  private host = 'https://api.letsdeel.com/rest/v2';
  private tokenPromise: Promise<string> | null = null;

  constructor(
    @Inject(ENVIRONMENT) private env: Environment,
    private credentialsProvider: ProviderCredentialsProvider,
  ) {}

  // This is a workaround for the Deel API returning PST dates as UTC dates.
  private timeZoneFix(dateString: string) {
    const date = new Date(dateString);
    return addHours(date, 7).toUTCString();
  }

  private getApiToken(): Promise<string> {
    this.tokenPromise ??= this._fetchToken();
    return this.tokenPromise;
  }

  private async _fetchToken(): Promise<string> {
    const fromDb = await this.credentialsProvider.getDeelCredentials();
    const token = fromDb?.apiToken ?? null;

    if (!token) {
      throw new GraphQLError('Deel credentials not configured for this tenant', {
        extensions: { code: 'PROVIDER_NOT_CONFIGURED' },
      });
    }

    return token;
  }

  public async getPaymentReceipts() {
    try {
      const queryVars: {
        date_from: TimelessDateString;
        date_to: TimelessDateString;
        currencies?: Currency;
        entities?: 'company' | 'individual';
      } = {
        date_from: dateToTimelessDateString(subYears(new Date(), 1)),
        date_to: dateToTimelessDateString(new Date()),
      };
      const url = new URL(`${this.host}/payments`);
      Object.entries(queryVars).map(([key, value]) => {
        url.searchParams.set(key, value as string);
      });
      const res = await fetch(url, {
        headers: {
          accept: 'application/json',
          authorization: `Bearer ${await this.getApiToken()}`,
        },
      });

      if (!res.ok) {
        console.error(await res.text());
        throw new Error(`Deel API returned status ${res.status}`);
      }
      const rawData = await res.json();

      if ('errors' in rawData) {
        console.log(rawData);
        throw new Error('Deel API returned an error');
      }

      const data = retrievePaymentReceiptsSchema.parse(rawData);

      return data;
    } catch (error) {
      const message = 'Failed to fetch Deel payment receipts';
      console.error(`${message}: ${error}`);
      throw new Error(message, { cause: error });
    }
  }

  public async getPaymentBreakdown(paymentId: string) {
    try {
      const url = new URL(`${this.host}/payments/${paymentId}/breakdown`);
      const res = await fetch(url, {
        headers: {
          accept: 'application/json',
          authorization: `Bearer ${await this.getApiToken()}`,
        },
      });

      if (!res.ok) {
        console.error(await res.text());
        throw new Error(`Deel API returned status ${res.status}`);
      }
      const rawData = await res.json();

      if ('errors' in rawData) {
        console.log(rawData);
        throw new Error('Deel API returned an error');
      }

      const data = retrievePaymentBreakdownSchema.parse(rawData);

      return data;
    } catch (error) {
      const message = 'Failed to fetch Deel payment breakdown';
      console.error(`${message}: ${error}`);
      throw new Error(message, { cause: error });
    }
  }

  public async getSalaryInvoices() {
    const invoices: Invoice[] = [];
    try {
      const apiToken = await this.getApiToken();
      const queryVars: {
        issued_from_date?: TimelessDateString;
        issued_to_date?: TimelessDateString;
        entities?: 'company' | 'individual';
        limit?: number;
        offset?: number;
      } = {
        limit: 50,
        offset: 0,
        issued_from_date: dateToTimelessDateString(subYears(new Date(), 1)),
      };
      for (let i = 0; i < 10; i++) {
        const url = new URL(`${this.host}/invoices`);
        Object.entries({ ...queryVars, offset: i * 50 }).map(([key, value]) => {
          url.searchParams.set(key, value as string);
        });
        const res = await fetch(url, {
          headers: {
            accept: 'application/json',
            authorization: `Bearer ${apiToken}`,
          },
        });

        if (!res.ok) {
          console.error(await res.text());
          throw new Error(`Deel API returned status ${res.status}`);
        }
        const rawData = await res.json();

        if ('errors' in rawData) {
          console.log(rawData);
          throw new Error('Deel API returned an error');
        }

        const data = retrieveInvoicesSchema.parse(rawData);

        if (data.data.length) {
          data.data = data.data.map(invoice => {
            return {
              ...invoice,
              issued_at: this.timeZoneFix(invoice.issued_at),
              due_date: this.timeZoneFix(invoice.due_date),
            };
          });
        } else {
          break;
        }
        invoices.push(...data.data);
      }

      return invoices;
    } catch (error) {
      const message = 'Failed to fetch Deel salary invoices';
      console.error(`${message}: ${error}`);
      throw new Error(message, { cause: error });
    }
  }

  public async getSalaryInvoiceFile(id: string) {
    try {
      const url = new URL(`${this.host}/invoices/${id}/download`);
      const res = await fetch(url, {
        headers: {
          accept: 'application/json',
          authorization: `Bearer ${await this.getApiToken()}`,
        },
      });

      if (!res.ok) {
        console.error(await res.text());
        throw new Error(`Deel API returned status ${res.status}`);
      }
      const rawData = await res.json();

      const data = downloadInvoicePdfSchema.parse(rawData);

      if (!data.data.url) {
        throw new Error('Deel invoice file download URL is missing');
      }

      const invoiceFileRes = await fetch(data.data.url);

      if (!invoiceFileRes.ok) {
        throw new Error('Deel invoice file download failed');
      }

      const invoiceFileBuffer = await invoiceFileRes.arrayBuffer();
      const blob = new Blob([invoiceFileBuffer], { type: 'application/pdf' });

      return blob;
    } catch (error) {
      const message = 'Failed to fetch Deel invoice file';
      console.error(`${message}: ${error}`);
      throw new Error(message, { cause: error });
    }
  }

  public async getContractDetails(contractId: string) {
    try {
      const url = new URL(`${this.host}/contracts/${contractId}`);
      const res = await fetch(url, {
        headers: {
          accept: 'application/json',
          authorization: `Bearer ${await this.getApiToken()}`,
        },
      });

      if (!res.ok) {
        console.error(await res.text());
        throw new Error(`Deel API returned status ${res.status}`);
      }
      const rawData = await res.json();

      if ('errors' in rawData) {
        console.log(rawData);
        throw new Error('Deel API returned an error');
      }

      const contract = ContractSchema.safeParse(rawData);
      if (!contract.success) {
        let errorMessage = 'Deel contract data validation failed';
        if (contract.error.issues) {
          for (const issue of contract.error.issues) {
            let value = rawData;
            for (const pathPart of issue.path) {
              value = value ? value[pathPart] : undefined;
            }
            errorMessage += `\n - Path: ${issue.path.join('.')}, Message: ${issue.message}, value: ${value}`;
          }
        }
        throw new Error(errorMessage);
      }
      return contract.data;
    } catch (error) {
      const message = 'Failed to fetch Deel contract details';
      console.error(`${message}: ${error}`);
      throw new Error(message, { cause: error });
    }
  }
}
