import { ApplicaDataProviderConfig, AttachmentParserResult, IApplicaDataProvider } from './types';
import { createFormData, createGetQuery, fetchJson } from './utils';
import { stringify } from 'query-string';
import {
  CreateParams,
  CreateResult,
  DeleteManyParams,
  DeleteManyResult,
  DeleteParams,
  DeleteResult,
  GetListParams,
  GetListResult,
  GetManyParams,
  GetManyReferenceParams,
  GetManyReferenceResult,
  GetManyResult,
  GetOneParams,
  GetOneResult,
  Identifier,
  UpdateManyParams,
  UpdateManyResult,
  UpdateParams,
  UpdateResult
} from 'ra-core';

class ApplicaDataProvider implements IApplicaDataProvider {
  config: ApplicaDataProviderConfig;
  prepareData: (data: any, resource?: string, params?: CreateParams | UpdateParams | any) => any;
  prepareAttachments: (data: any) => Promise<AttachmentParserResult>;

  constructor(config: ApplicaDataProviderConfig) {
    this.config = config;
    this.prepareData = config.prepareData || ((data: any) => data);
    this.prepareAttachments = config.attachmentsParser;
  }

  /**
   * @inheritdoc
   */
  getApiUrl(): string {
    return this.config.apiUrl;
  }

  /**
   * @inheritdoc
   */
  async getFile(resource: string): Promise<string> {
    const headers = await this.config.getHeaders();
    return fetch(`${this.config.apiUrl}${resource}`, { headers })
      .then((response) => response.blob())
      .then((blob) => URL.createObjectURL(blob));
  }

  /**
   * @inheritdoc
   */
  async getList(resource: string, params: GetListParams | any): Promise<GetListResult | any> {
    const query = createGetQuery(params);
    const url = `${this.config.apiUrl}/${resource}/find`;
    const headers = await this.config.getHeaders();
    const options = {
      headers,
      body: JSON.stringify(query),
      method: 'POST'
    };

    return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
      data: json?.value?.rows,
      total: parseInt(json?.value?.totalRows)
    }));
  }

  /**
   * @inheritdoc
   */
  async getOne(resource: string, params: GetOneParams | any): Promise<GetOneResult | any> {
    const url = `${this.config.apiUrl}/${resource}` + (params.id ? `/${params.id}` : '');
    const headers = await this.config.getHeaders();
    const options = { headers };
    return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
      data: json.value
    }));
  }

  /**
   * @inheritdoc
   */
  async getMany(resource: string, params: GetManyParams | any): Promise<GetManyResult | any> {
    const url = `${this.config.apiUrl}/${resource}/find`;
    const headers = await this.config.getHeaders();
    const options = {
      headers,
      body: JSON.stringify({
        filters: [
          {
            property: 'id',
            value: params.ids,
            type: 'in'
          }
        ]
      }),
      method: 'POST'
    };
    return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
      data: json?.value?.rows,
      total: parseInt(json?.value?.totalRows)
    }));
  }

  /**
   * @inheritdoc
   */
  async getManyReference(resource: string, params: GetManyReferenceParams): Promise<GetManyReferenceResult> {
    const query = createGetQuery(params);
    const url = `${this.config.apiUrl}/${resource}/find`;
    const headers = await this.config.getHeaders();
    query.filters.push({
      property: params.target,
      value: params.id,
      type: 'eq'
    });
    const options = {
      headers,
      body: JSON.stringify(query),
      method: 'POST'
    };

    return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
      data: json?.value?.rows,
      total: parseInt(json?.value?.totalRows)
    }));
  }

  /**
   * @inheritdoc
   */
  async create(resource: string, params: CreateParams): Promise<CreateResult> {
    return this.prepareAttachments(params.data).then(async ({ data, parts }) => {
      const url = `${this.config.apiUrl}/${resource}`;
      const token = await this.config.getToken();
      let body: any = null;
      let headers: any = {};
      if (this.config.mobile === true) {
        body = this.prepareData(data, resource, params);
        body = JSON.stringify(body);
        headers = {
          'Content-Type': 'application/json'
        };
      } else {
        body = new FormData();
        const jsonData = JSON.stringify(this.prepareData(data, resource, params));
        const entity = new Blob([jsonData], { type: 'application/json' });
        body.append('entity', entity);
        parts.forEach((p) => body.append(p.id, p.file));
      }
      const options = this.createOptions(
        {
          method: 'POST',
          body,
          headers
        },
        token
      );
      return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
        data: { ...(json?.value || params.data), id: json?.value.id }
      }));
    });
  }

  /**
   * @inheritdoc
   */
  async update(resource: string, params: UpdateParams): Promise<UpdateResult> {
    return this.prepareAttachments(params.data).then(async ({ data, parts }) => {
      const url = `${this.config.apiUrl}/${resource}`;
      const token = await this.config.getToken();
      let body: any = null;
      let headers: any = {};
      if (this.config.mobile === true) {
        body = this.prepareData(data, resource, params);
        body = JSON.stringify(body);
        headers = {
          'Content-Type': 'application/json'
        };
      } else {
        body = new FormData();
        const jsonData = JSON.stringify(this.prepareData(data, resource, params));
        const entity = new Blob([jsonData], { type: 'application/json' });
        body.append('entity', entity);
        parts.forEach((p) => body.append(p.id, p.file));
      }
      const options = this.createOptions(
        {
          method: 'POST',
          body,
          headers
        },
        token
      );
      return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
        data: { ...(json?.value || params.data), id: json?.value.id }
      }));
    });
  }

  /**
   * @inheritdoc
   */
  async updateMany(
    resource: string,
    params: UpdateManyParams & {
      rows?: any[];
    }
  ): Promise<UpdateManyResult> {
    const token = await this.config.getToken();
    return Promise.all(
      params.ids.map((id: Identifier) =>
        fetchJson(
          `${this.config.apiUrl}/${resource}`,
          {
            method: 'POST',
            body: (() => {
              if (this.config.mobile === true) {
                const row = params?.rows?.[id as number] || params?.rows?.find((r: any) => r.id === id);
                const entity = JSON.stringify(this.prepareData({ ...row, ...params.data, id }));
                return entity;
              } else {
                const row = params?.rows?.[id as number] || params?.rows?.find((r: any) => r.id === id);
                const jsonData = JSON.stringify(this.prepareData({ ...row, ...params.data, id }));
                const entity = new Blob([jsonData], { type: 'application/json' });
                const formData = new FormData();
                formData.append('entity', entity);
                return formData;
              }
            })(),
            headers: (() =>
              this.config.mobile === true
                ? this.createHeaders(
                    {
                      'Content-Type': 'application/json'
                    },
                    token
                  )
                : this.createHeaders({}, token))()
          },
          this.config.HttpErrorClass
        ).then(({ json }) => ({
          data: { ...(json?.value || params.data), id: json?.value.id }
        }))
      )
    ).then((responses) => ({
      data: responses.map((response) => response.data)
    }));
  }

  /**
   * @inheritdoc
   */
  async delete(resource: string, params: DeleteParams | any): Promise<DeleteResult | any> {
    const url = `${this.config.apiUrl}/${resource}/delete`;
    const token = await this.config.getToken();
    const body = createFormData({ ids: params.id });
    const options = this.createOptions(
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
          Accept: 'application/json'
        },
        body
      },
      token
    );
    return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
      data: json
    }));
  }

  /**
   * @inheritdoc
   */
  async deleteMany(resource: string, params: DeleteManyParams | any): Promise<DeleteManyResult | any> {
    const token = await this.config.getToken();
    const url = `${this.config.apiUrl}/${resource}/delete`;
    const options = this.createOptions(
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
          Accept: 'application/json'
        },
        body: createFormData({ ids: params.ids })
      },
      token
    );
    return fetchJson(url, options, this.config.HttpErrorClass).then(() => ({
      data: params.ids
    }));
  }

  private async _call(method: string, resource: string, params: any): Promise<any> {
    let url = `${this.config.apiUrl}/${resource}`;
    if (method === 'GET' && params) {
      const queryString = stringify(params);
      url = `${this.config.apiUrl}/${resource}?${queryString}`;
    }
    const headers = await this.config.getHeaders();
    const options = {
      method,
      headers,
      body: method !== 'GET' ? JSON.stringify(params) : undefined
    };
    return fetchJson(url, options, this.config.HttpErrorClass).then(({ json }) => ({
      data: json
    }));
  }

  /**
   * @inheritdoc
   */
  async post(resource: string, params: object): Promise<any> {
    return this._call('POST', resource, params);
  }

  /**
   * @inheritdoc
   */
  async get(resource: string, params: object): Promise<any> {
    return this._call('GET', resource, params);
  }

  private createHeaders(headers: any, token?: string) {
    const authHeaders =
      typeof token === 'string' && token !== undefined && token !== null ? { Authorization: `Bearer ${token}` } : {};
    return {
      ...headers,
      ...authHeaders
    };
  }

  private createOptions(options: any, token?: string): any {
    const headers = this.createHeaders(options?.headers, token);
    return {
      ...options,
      headers
    };
  }
}

export { ApplicaDataProvider };
