import fetch, { BodyInit } from 'node-fetch';
import {
  IExecuteFunctions,
  IHookFunctions,
  ILoadOptionsFunctions,
  IDataObject,
  IHttpRequestMethods,
  GenericValue,
  IN8nHttpResponse,
  IHttpRequestOptions,
} from 'n8n-workflow';

import { ICompany, ICustomField, IOpportunity } from '@src/resources';
import { IContact } from '@src/resources/contact/contact.types';
import { THttpFullResponse } from '@src/types/httpRequest';
import { prettyLog } from './pretty-log';

interface ContactSearchParams {
  locationId: string;
  page: number;
  pageLimit: number;
  searchAfter?: Array<string | number>;
  filters?: Array<{
    group?: 'AND' | 'OR';
    filters?: Array<{
      field: string;
      operator: string;
      value: any;
    }>;
    field?: string;
    operator?: string;
    value?: any;
  }>;
  sort?: Array<{
    field: string;
    direction: 'asc' | 'desc';
  }>;
}

interface GoHighLevelCredentials {
  grantType: string;
  authUrl: string;
  accessTokenUrl: string;
  clientId: string;
  clientSecret: string;
  scope: string;
  authQueryParameters: string;
  authentication: string;
  oauthTokenData: {
    access_token: string;
    token_type: string;
    expires_in: number;
    refresh_token: string;
    scope: string;
    userType: string;
    companyId: string;
    locationId: string;
    userId: string;
  };
}

type TBody = FormData | GenericValue | GenericValue[] | Buffer | URLSearchParams;
type TQueryString = IDataObject;

interface OpportunitySearchParams {
  assigned_to?: string;
  campaignId?: string;
  contact_id?: string;
  country?: string;
  date?: string;
  endDate?: string;
  getCalendarEvents?: boolean;
  getNotes?: boolean;
  getTasks?: boolean;
  id?: string;
  limit?: number;
  order?: string;
  page?: number;
  pipeline_id?: string;
  pipeline_stage_id?: string;
  q?: string;
  startAfter?: string;
  startAfterId?: string;
  status?: 'open' | 'won' | 'lost' | 'abandoned' | 'all';
  location_id?: string; // required
}

export class GoHighLevelManager {
  static instance: GoHighLevelManager;
  private baseURL = 'https://services.leadconnectorhq.com';
  private n8n: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions;
  private locationId: string;

  constructor(n8n: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, locationId: string) {
    this.n8n = n8n;
    this.locationId = locationId;
  }

  static async getInstance(n8n: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions) {


    const credentials = await n8n.getCredentials('goHighLevelOAuth2Api') as unknown as GoHighLevelCredentials;
    prettyLog({ credentials });
    const instance = new GoHighLevelManager(n8n, String(credentials.oauthTokenData.locationId));
    return instance;


    // if (!this.instance) {
    //   const credentials = await n8n.getCredentials('goHighLevelOAuth2Api') as unknown as GoHighLevelCredentials;
    //   prettyLog({ credentials });
    //   this.instance = new GoHighLevelManager(n8n, String(credentials.oauthTokenData.locationId));
    // }

    // const credentialsInstance = await this.instance.n8n.getCredentials('goHighLevelOAuth2Api') as unknown as GoHighLevelCredentials

    // prettyLog({ credentialsInstance });


    // return this.instance;
  }

  async request<T extends IN8nHttpResponse>({ method, path, body, params }: {
    method: IHttpRequestMethods, path: string, body?: TBody, params?: TQueryString;
  }) {
    const credentials = (await this.n8n.getCredentials('goHighLevelOAuth2Api')) as unknown as GoHighLevelCredentials;
    const accessToken = credentials.oauthTokenData.access_token;

    // console.log({ accessToken });

    const url = path.startsWith('/') ? `${this.baseURL}${path}` : path;

    const urlWithParams = new URL(url);
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        urlWithParams.searchParams.set(key, String(value));
      });
    }

    const formattedBody = body ? (body instanceof FormData || body instanceof Buffer ? body : JSON.stringify(body)) : undefined;

    // curl log 
    // console.log(`curl -X ${method} ${urlWithParams.toString()}\n -H "Authorization: Bearer ${accessToken}"\n -H "Version: 2021-07-28"\n${formattedBody ? ` -d '${JSON.stringify(formattedBody, null, 2)}'` : ''}`);
    console.log(`curl -X ${method} ${urlWithParams.toString()}\n${body ? ` -d '${JSON.stringify(body, null, 2)}'` : ''}`);

    const response = await fetch(urlWithParams.toString(), {
      method,
      body: formattedBody as BodyInit,
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`,
        'Version': '2021-07-28',
      },
    });

    console.log('response infos', response.status, response.statusText);

    const data = await response.json() as T;
    console.log('response', data);

    return data;
  }

  async httpRequestWithAuthentication<T extends IN8nHttpResponse>({ method, path, body, params, verbose = false }: {
    method: IHttpRequestMethods, path: string, body?: TBody, params?: TQueryString, verbose?: boolean;
  }) {
    const url = path.startsWith('/') ? `${this.baseURL}${path}` : path;

    const options: IHttpRequestOptions = {
      method,
      body,
      headers: {
        'Content-Type': 'application/json',
        'Version': '2021-07-28',
      },
      qs: params,
      url: url,
      returnFullResponse: true
    };

    if (verbose) {
      console.log(`curl -X ${method} ${url}\n${body ? ` -d '${JSON.stringify(body, null, 2)}'` : ''}`);
      prettyLog(options);
    }

    const response = (await this.n8n.helpers.httpRequestWithAuthentication.call(this.n8n, 'goHighLevelOAuth2Api', options)) as THttpFullResponse<T>;

    if (verbose) {
      prettyLog({
        infos: {
          statusCode: response.statusCode,
          statusMessage: response.statusMessage,
        },
        body: response.body,
      });
    }

    return response;
  }

  async get<T extends IN8nHttpResponse>(path: string, options?: { params?: TQueryString, verbose?: boolean }) {
    const response = await this.httpRequestWithAuthentication<T>({
      method: 'GET',
      path,
      ...options,
      verbose: false
    });

    return response.body;
  }

  async post<T extends IN8nHttpResponse>(path: string, options?: { body: TBody, verbose?: boolean }) {
    const response = await this.httpRequestWithAuthentication<T>({
      method: 'POST',
      path,
      ...options,
      verbose: true
    });

    return response.body;
  }

  async put<T extends IN8nHttpResponse>(path: string, options?: { body: TBody, verbose?: boolean }) {
    const response = await this.httpRequestWithAuthentication<T>({
      method: 'PUT',
      path,
      ...options,
      verbose: true,
    });

    return response.body;
  }

  async delete<T extends IN8nHttpResponse>(path: string, options?: { body: TBody, verbose?: boolean }) {
    const response = await this.httpRequestWithAuthentication<T>({
      method: 'DELETE',
      path,
      ...options,
    });

    return response.body;
  }

  async findContact(params: ContactSearchParams) {
    const { contacts } = await this.post<{ contacts: IContact[] }>('/contacts/search', {
      body: params,
    });

    return contacts[0];
  }

  async findContactByEmail(email: string) {
    const contact = await this.findContact({
      locationId: this.locationId,
      page: 1,
      pageLimit: 50,
      filters: [{
        field: 'email',
        operator: 'eq',
        value: email,
      }],
    });

    return contact;
  }



  #enrichContact(contact: IContact, customFields: ICustomField[]) {
    const customFieldsMap = new Map(customFields.map((customField) => [customField.id, customField.name]));

    return {
      ...contact,
      customFields: contact.customFields?.map((customField) => ({
        name: customFieldsMap.get(customField.id),
        ...customField,
      })),
    };
  }

  #enrichOpportunity(opportunity: IOpportunity, customFields: ICustomField[]) {
    const customFieldsMap = new Map(customFields.map((customField) => [customField.id, customField.name]));

    return {
      ...opportunity,
      customFields: opportunity.customFields?.map((customField) => ({
        name: customFieldsMap.get(customField.id),
        ...customField,
      })),
    };
  }

  async createContact(contact: IContact) {
    const { contact: createdContact } = await this.post<{ contact: IContact }>('/contacts', {
      body: { locationId: this.locationId, ...contact },
    });

    const customFields = await this.getCustomFields('contact');
    const enrichedContact = this.#enrichContact(createdContact, customFields);
    return enrichedContact;
  }

  async getContacts(options?: { params?: TQueryString }) {
    const locationId = this.locationId;
    const { contacts } = await this.get<{ contacts: IContact[] }>('/contacts', {
      params: {
        locationId,
        ...options?.params,
      },
    });

    const customFields = await this.getCustomFields('contact');
    const enrichedContacts = contacts.map((contact) => this.#enrichContact(contact, customFields));

    return enrichedContacts;
  }

  async getContact(contactId: string) {
    const { contact } = await this.get<{ contact: IContact }>(`/contacts/${contactId}`);

    const customFields = await this.getCustomFields('contact');
    return this.#enrichContact(contact, customFields);
  }

  async updateContact(contactId: string, contact: IContact) {
    const customFields = await this.getCustomFields('contact');

    const { contact: updatedContact } = await this.put<{ contact: IContact }>(`/contacts/${contactId}`, {
      body: contact,
    });

    const enrichedContact = this.#enrichContact(updatedContact, customFields);
    return enrichedContact;
  }

  async upsertContact(contact: IContact) {
    const { contact: upsertedContact } = await this.post<{ contact: IContact }>('/contacts/upsert', {
      body: contact,
    });

    const customFields = await this.getCustomFields('contact');
    const enrichedContact = this.#enrichContact(upsertedContact, customFields);

    return enrichedContact;
  }

  async deleteContact(contactId: string) {
    const { contact } = await this.delete<{ contact: IContact }>(`/contacts/${contactId}`);

    return contact;
  }

  async addTagToContact(contactId: string, tags: string[]) {
    const { contact: updatedContact } = await this.post<{ contact: IContact }>(`/contacts/${contactId}/tags`, {
      body: { tags },
    });

    return updatedContact;
  }

  async removeTagFromContact(contactId: string, tags: string[]) {
    const { contact: updatedContact } = await this.delete<{ contact: IContact }>(`/contacts/${contactId}/tags`, {
      body: { tags },
    });

    return updatedContact;
  }

  async findDuplicateContacts(contact: IContact) {
    const { contacts } = await this.post<{ contacts: IContact[] }>('/contacts/search/duplicate', {
      body: contact,
    });

    return contacts;
  }

  // CRUD for company
  async createCompany(company: ICompany) {
    const { company: createdCompany } = await this.post<{ company: ICompany }>('/companies', {
      body: company,
    });

    return createdCompany;
  }

  async getCompany(companyId: string) {
    const { company } = await this.get<{ company: ICompany }>(`/companies/${companyId}`);

    return company;
  }

  async getCompanies(options?: { params?: TQueryString }) {
    const { companies } = await this.get<{ companies: ICompany[] }>('/companies', options);

    return companies;
  }

  async updateCompany(companyId: string, company: ICompany) {
    const { company: updatedCompany } = await this.put<{ company: ICompany }>(`/companies/${companyId}`, {
      body: company,
    });

    return updatedCompany;
  }

  async deleteCompany(companyId: string) {
    const { company } = await this.delete<{ company: ICompany }>(`/companies/${companyId}`);

    return company;
  }

  async addTagsToCompany(companyId: string, tags: string[]) {
    const { company: updatedCompany } = await this.post<{ company: ICompany }>(`/companies/${companyId}/tags`, {
      body: { tags },
    });

    return updatedCompany;
  }

  async removeTagsFromCompany(companyId: string, tags: string[]) {
    const { company: updatedCompany } = await this.delete<{ company: ICompany }>(`/companies/${companyId}/tags`, {
      body: { tags },
    });

    return updatedCompany;
  }

  // CRUD for opportunity

  async findOpportunity(params: OpportunitySearchParams) {
    const locationId = this.locationId;

    const { opportunities } = await this.post<{ opportunities: IOpportunity[] }>('/opportunities/search', {
      body: { location_id: locationId, ...params },
    });

    const opportunity = opportunities[0];

    const customFields = await this.getCustomFields('opportunity')
    const enrichedOpportunity = this.#enrichOpportunity(opportunity, customFields)

    return enrichedOpportunity;
  }

  async createOpportunity(opportunity: IOpportunity) {
    const { opportunity: createdOpportunity } = await this.post<{ opportunity: IOpportunity }>('/opportunities/', {
      body: {
        locationId: this.locationId,
        ...opportunity
      },
    });

    const customFields = await this.getCustomFields('opportunity');
    const enrichedOpportunity = this.#enrichOpportunity(createdOpportunity, customFields);

    return enrichedOpportunity;
  }

  async getOpportunity(opportunityId: string) {
    const { opportunity } = await this.get<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}`);

    const customFields = await this.getCustomFields('opportunity');
    return this.#enrichOpportunity(opportunity, customFields);
  }

  async getOpportunities(options?: { params?: TQueryString }) {
    const response = await this.get<{ opportunities: IOpportunity[] }>('/opportunities/search', {
      params: {
        location_id: this.locationId,
        ...options?.params,
      },
    });

    const customFields = await this.getCustomFields('opportunity');

    console.log('response', response);

    const opportunities = response.opportunities || [];

    return opportunities.map((opportunity) => this.#enrichOpportunity(opportunity, customFields));
  }

  async updateOpportunity(opportunityId: string, opportunity: Partial<IOpportunity>) {
    const customFields = await this.getCustomFields('opportunity');

    const { opportunity: updatedOpportunity } = await this.put<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}`, {
      body: opportunity,
    });

    const enrichedOpportunity = this.#enrichOpportunity(updatedOpportunity, customFields);

    return enrichedOpportunity;
  }

  async deleteOpportunity(opportunityId: string) {
    const { opportunity } = await this.delete<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}`);

    return opportunity;
  }

  async updateOpportunityStage(opportunityId: string, stageId: string) {
    const { opportunity: updatedOpportunity } = await this.put<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}/stage`, {
      body: { stageId },
    });

    return updatedOpportunity;
  }

  async updateOpportunityStatus(opportunityId: string, status: string) {
    const { opportunity: updatedOpportunity } = await this.put<{ opportunity: IOpportunity }>(`/opportunities/${opportunityId}/status`, {
      body: { status },
    });

    return updatedOpportunity;
  }

  // CRUD for custom field
  async getCustomFields(model: 'contact' | 'opportunity' | 'all' = 'all') {
    const { customFields } = await this.get<{ customFields: ICustomField[] }>(`/locations/${this.locationId}/customFields`, {
      params: {
        model,
      },
    });

    return customFields;
  }

  async createCustomField(model: 'contact' | 'opportunity' | 'company', customField: ICustomField) {
    const { customField: createdCustomField } = await this.post<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields`, {
      body: { ...customField, model },
    });

    return createdCustomField;
  }

  async getCustomField(customFieldId: string) {
    const { customField } = await this.get<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields/${customFieldId}`);

    return customField;
  }

  async updateCustomField(customFieldId: string, customField: Partial<ICustomField>) {
    const { customField: updatedCustomField } = await this.put<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields/${customFieldId}`, {
      body: customField,
    });

    return updatedCustomField;
  }

  async deleteCustomField(customFieldId: string) {
    const { customField } = await this.delete<{ customField: ICustomField }>(`/locations/${this.locationId}/customFields/${customFieldId}`);

    return customField;
  }
}
