/**
 *  Copyright (C) 2018 Basalt
    This file is part of Knapsack.
    Knapsack is free software; you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by the Free
    Software Foundation; either version 2 of the License, or (at your option)
    any later version.

    Knapsack is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
    more details.

    You should have received a copy of the GNU General Public License along
    with Knapsack; if not, see <https://www.gnu.org/licenses>.
 */
import { DocumentNode } from 'graphql';
import md5 from 'md5';
import { apiUrlBase, graphqlBase } from '../lib/constants';
import { createDemoUrl } from '../lib/routes';
import { KnapsackTemplateDemo } from '../schemas/patterns';
import * as Files from '../schemas/api/files';
import * as Plugins from '../schemas/api/plugins';
import { AppState } from './store';
import {
  KsRenderResults,
  KsTemplateRenderedMeta,
  KsTemplateUrls,
} from '../schemas/knapsack-config';
import { timer } from '../lib/utils';
import { FileResponse } from '../schemas/misc';
import { PatternRenderData } from '../schemas/api/render';

export { Files };

/**
 * GraphQL Query Object to String
 * @param gqlQueryObject -  GraphQL query made from `gql` - https://github.com/apollographql/graphql-tag/issues/150
 * @return {string}
 */
export function gqlToString(gqlQueryObject: DocumentNode): string {
  return gqlQueryObject.loc.source.body;
}

export interface GraphQlResponse {
  data?: any;
  errors?: {
    message: string;
    extensions: {
      code: string;
      stacktrace: string[];
    };
    locations: {
      line: number;
      column: number;
    }[];
  }[];
}

/**
 * GraphQL Query
 * Must pass in `query` OR `gqlQuery`
 */
export function gqlQuery({
  query,
  gqlQueryObj,
  variables = {},
}: {
  /**
   * Plain GraphQL query
   */
  query?: string | DocumentNode;
  /**
   * GraphQL query made from `gql`
   */
  gqlQueryObj?: DocumentNode;
  /**
   * GraphQL variables
   */
  variables?: object;
}): Promise<GraphQlResponse> {
  if (!query && !gqlQueryObj) {
    throw new Error('Must provide either "query" or "gqlQueryObj".');
  }

  if (typeof query !== 'string') {
    if (gqlQueryObj.kind !== 'Document') {
      throw new Error('"gqlQueryObj" not a valid GraphQL document.');
    }
    // get the plain string from the `gql` parsed object
    query = gqlToString(gqlQueryObj); // eslint-disable-line no-param-reassign
  }

  return window
    .fetch(graphqlBase, {
      method: 'POST',
      headers: {
        'Accept-Encoding': 'gzip, deflate, br',
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Connection: 'keep-alive',
        Dnt: '1',
      },
      body: JSON.stringify({
        query,
        variables,
      }),
    })
    .then(res => res.json())
    .catch(console.log.bind(console));
}

type KnapsackDesignToken = import('@knapsack/core/types').KnapsackDesignToken;

export function getDesignTokens(): Promise<KnapsackDesignToken[]> {
  return window.fetch(`${apiUrlBase}/design-tokens`).then(res => res.json());
}

export function files(x: Files.Actions): Promise<Files.ActionResponses> {
  return window
    .fetch(Files.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify(x),
    })
    .then(res => {
      if (!res.ok) {
        const { status, statusText } = res;
        console.error(`Error in calling files endpoint`, {
          status,
          statusText,
        });
      }
      return res.json();
    });
}

export function getPluginContent({
  pluginId,
}: {
  pluginId: string;
}): Promise<Plugins.GetContentResponse> {
  const body: Plugins.GetContentRequest = {
    pluginId,
    type: Plugins.ACTIONS.getContent,
  };
  return fetch(Plugins.endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify(body),
  }).then(res => res.json());
}

/**
 * Save data up on server to be used in template rendering with `dataId` query param later
 * @returns dataId that is md5 hash
 */
export function saveData(data: object): Promise<string> {
  return window
    .fetch(`${apiUrlBase}/data`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify(data),
    })
    .then(res => res.json())
    .then(results => {
      if (!results.ok) {
        console.error(
          `Uh oh! Tried to save data by uploading it to '${apiUrlBase}/data' with no luck.`,
          {
            data,
            results,
          },
        );
      }
      return results.data.hash;
    })
    .catch(console.log.bind(console));
}

export function uploadFile(file: File): Promise<FileResponse> {
  const body = new FormData();
  body.append('file', file);

  return window
    .fetch(`${apiUrlBase}/upload`, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
      },
      body,
    })
    .then(res => res.json())
    .then(response => {
      if (!response.ok) {
        console.error('uh oh: upload crap out', response);
      }
      return response;
    });
}

export function getInitialState(): Promise<AppState> {
  const getTime = timer();
  return window
    .fetch(`${apiUrlBase}/data-store`)
    .then(res => res.json())
    .then(initialState => {
      console.debug(`KS: initial state fetch took: ${getTime()}s`);
      // console.log({ initialState });
      return initialState;
    })
    .catch(console.log.bind(console));
}

export function renderTemplate(
  options: PatternRenderData,
): Promise<KsRenderResults> {
  return window
    .fetch(`${apiUrlBase}/render`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify(options),
    })
    .then(res => res.json())
    .catch(console.log.bind(console));
}

const prevDataIds = new Map<string, string>();

/**
 * Get a URL where this Pattern's Template can be viewed
 */
export async function getTemplateUrls({
  patternId,
  templateId,
  assetSetId,
  demo,
  cacheBuster,
}: {
  patternId: string;
  templateId?: string;
  assetSetId?: string;
  demo: KnapsackTemplateDemo;
  cacheBuster?: string;
}): Promise<{
  dataId: string;
  urls: KsTemplateUrls;
}> {
  const hash = md5(JSON.stringify(demo));
  const dataId = prevDataIds.has(hash)
    ? prevDataIds.get(hash)
    : await saveData(demo);
  if (!prevDataIds.has(hash)) {
    prevDataIds.set(hash, dataId);
  }

  return {
    dataId,
    urls: {
      external: createDemoUrl({
        patternId,
        templateId,
        assetSetId,
        dataId,
        cacheBuster,
        isInIframe: false,
        wrapHtml: true,
      }),
      iframeSrc: createDemoUrl({
        patternId,
        templateId,
        assetSetId,
        dataId,
        cacheBuster,
        isInIframe: true,
        wrapHtml: true,
      }),
    },
  };
}

/**
 * Get a URL where this Pattern's Template can be viewed
 */
export async function getTemplateInfo({
  patternId,
  templateId,
  assetSetId,
  demo,
  cacheBuster,
}: {
  patternId: string;
  templateId?: string;
  assetSetId?: string;
  demo: KnapsackTemplateDemo;
  cacheBuster?: string;
}): Promise<KsTemplateRenderedMeta> {
  const { dataId, urls } = await getTemplateUrls({
    patternId,
    templateId,
    assetSetId,
    demo,
    cacheBuster,
  });
  const renderResults = await renderTemplate({
    patternId,
    templateId,
    dataId,
    assetSetId,
    isInIframe: false,
    wrapHtml: false,
  });

  return {
    ...renderResults,
    urls,
    dataId,
  };
}
