import Axios, { AxiosRequestConfig, Method, AxiosResponse, AxiosAdapter, AxiosPromise } from 'axios';
import AppConfig from './AppConfig';
import IContent from './Models/IContent';
import ContentLink, { ContentReference, ContentLinkService } from './Models/ContentLink';
import ActionResponse, { ResponseType } from './Models/ActionResponse';
import WebsiteList from './Models/WebsiteList';
import Website from './Models/Website';
import PathProvider from './PathProvider';
import Property from './Property';

export type PathResponse<T = any, C extends IContent = IContent> = C | ActionResponse<T, C>;

export type NetworkErrorData<T = any> = IContent & {
  error: Property<T>;
}

export function PathResponseIsIContent(iContent: PathResponse): iContent is IContent {
  if ((iContent as ActionResponse<any>).actionName) {
    return false;
  }
  return true;
}
export function PathResponseIsActionResponse<P extends any = any>(actionResponse: PathResponse): actionResponse is ActionResponse<P>
{
  if ((actionResponse as ActionResponse<P>).actionName) {
    return true;
  }
  return false;
}
export function getIContentFromPathResponse<IContentType extends IContent = IContent>(response: PathResponse<any, IContentType>) : IContentType | null
{
  if (PathResponseIsActionResponse(response)) {
    return response.currentContent;
  }
  if (PathResponseIsIContent(response)) {
    return response;
  }
  return null;
}

/**
 * ContentDelivery API Wrapper
 * 
 * @deprecated
 */
export class ContentDeliveryAPI {
  protected config: AppConfig;
  protected componentService: string = '/api/episerver/v2.0/content/';
  protected websiteService: string = '/api/episerver/v3/site/';
  protected methodService: string = '/api/episerver/v3/action/';
  protected debug: boolean = false;
  protected pathProvider: PathProvider;

  /**
   * Marker to keep if we're in edit mode
   */
  protected inEditMode: boolean = false;

  /**
   * Internal cache of the websites retrieved from the ContentDelivery API
   *
   * @private
   */
  private websites!: WebsiteList;

  /**
   * Internal cache of the current website, as retrieved from the ContentDelivery API
   *
   * @private
   */
  private website!: Website;

  /**
   * ContentDelivery API Wrapper
   * 
   * @deprecated
   */
  constructor(pathProvider: PathProvider, config: AppConfig) {
    this.pathProvider = pathProvider;
    this.config = config;
    this.debug = this.config.enableDebug === true;
  }

  public get currentPathProvider() : PathProvider
  {
    return this.pathProvider;
  }

  public get currentConfig() : AppConfig
  {
    return this.config;
  }

  public isInEditMode(): boolean {
    return this.inEditMode;
  }

  public setInEditMode(editMode: boolean): ContentDeliveryAPI {
    this.inEditMode = editMode === true;
    return this;
  }

  public isDisabled(): boolean {
    return this.config.noAjax === true;
  }

  /**
   * Invoke an ASP.Net MVC controller method using the generic content API. This is intended
   * to be used only when attaching a SPA to an existing code-base.
   *
   * @param content The content for which the controller must be loaded
   * @param method  The (case sensitive) method name to invoke on the controller
   * @param verb    The HTTP verb to use when invoking the controller
   * @param data    The data (if any) to send to the controller for the method
   */
  public async invokeControllerMethod(
    content: ContentLink,
    method: string,
    verb?: Method,
    data?: object,
  ): Promise<any> {
    let options = this.getRequestSettings(verb);
    options.data = data;
    return await this.doRequest<any>(this.getMethodServiceUrl(content, method), options);
  }

  /**
   * Strongly typed variant of invokeControllerMethod
   *
   * @see   invokeControllerMethod()
   * @param content The content for which the controller must be loaded
   * @param method  The (case sensitive) method name to invoke on the controller
   * @param verb    The HTTP verb to use when invoking the controller
   * @param data    The data (if any) to send to the controller for the method
   */
  public async invokeTypedControllerMethod<TypeOut, TypeIn>(
    content: ContentLink,
    method: string,
    verb?: Method,
    data?: TypeIn,
  ): Promise<ActionResponse<TypeOut>> {
    let options = this.getRequestSettings(verb);
    options.data = data;
    return await this.doRequest<ActionResponse<TypeOut>>(this.getMethodServiceUrl(content, method), options);
  }

  /**
   * Retrieve a list of all websites registered within Episerver
   */
  public async getWebsites(): Promise<WebsiteList> {
    if (!this.websites) {
      this.websites = await this.doRequest<WebsiteList>(this.config.epiBaseUrl + this.websiteService);
    }
    return this.websites;
  }

  /**
   * Retrieve the first website registered within Episerver
   */
  public async getWebsite(): Promise<Website> {
    const list = await this.getWebsites();
    return list[0];
  }

  public async getContent(content: ContentLink, forceGuid: boolean = false): Promise<IContent | null> {
    if (!(content && (content.guidValue || content.url))) {
      if (this.config.enableDebug) {
        console.warn('Loading content for an empty reference ', content);
      }
      return null;
    }
    let useGuid = content.guidValue ? this.config.preferGuid || forceGuid : false;
    let serviceUrl: URL;
    if (useGuid) {
      serviceUrl = new URL(this.config.epiBaseUrl + this.componentService + content.guidValue);
    } else {
      try {
        serviceUrl = new URL(
          this.config.epiBaseUrl +
            (content.url ? content.url : this.componentService + ContentLinkService.createApiId(content)),
        );
      } catch (e) {
        serviceUrl = new URL(this.config.epiBaseUrl + this.componentService + ContentLinkService.createApiId(content));
      }
    }
    //serviceUrl.searchParams.append('currentPageUrl', this.pathProvider.getCurrentPath());
    if (this.config.autoExpandRequests) {
      serviceUrl.searchParams.append('expand', '*');
    }
    return this.doRequest<PathResponse>(serviceUrl.href).catch((r) => {
      return this.buildNetworkError(r);
    }).then(r => getIContentFromPathResponse(r));
  }

  public async getContentsByRefs(refs: Array<string>): Promise<Array<IContent>> {
    if (!refs || refs.length == 0) {
      return Promise.resolve<Array<IContent>>([]);
    }
    let serviceUrl: URL = new URL(this.config.epiBaseUrl + this.componentService);
    serviceUrl.searchParams.append('references', refs.join(','));
    if (this.config.autoExpandRequests) {
      serviceUrl.searchParams.append('expand', '*');
    }
    return this.doRequest<Array<IContent>>(serviceUrl.href).catch((r) => {
      return [];
    });
  }

  public async getContentByRef(ref: string): Promise<IContent> {
    let serviceUrl: URL = new URL(this.config.epiBaseUrl + this.componentService + ref);
    if (this.config.autoExpandRequests) {
      serviceUrl.searchParams.append('expand', '*');
    }
    return this.doRequest<IContent>(serviceUrl.href).catch((r) => {
      return this.buildNetworkError(r);
    });
  }

  public async getContentByPath(path: string): Promise<PathResponse> {
    let serviceUrl: URL = new URL(this.config.epiBaseUrl + path);
    if (this.config.autoExpandRequests) {
      serviceUrl.searchParams.append('expand', '*');
    }
    //serviceUrl.searchParams.append('currentPageUrl', this.pathProvider.getCurrentPath());
    return this.doRequest<PathResponse>(serviceUrl.href).catch((r) => {
      return this.buildNetworkError(r, path);
    });
  }

  public async getContentChildren<T extends IContent>(id: ContentReference): Promise<Array<T>> {
    let itemId: string = ContentLinkService.createApiId(id);
    let serviceUrl: URL = new URL(this.config.epiBaseUrl + this.componentService + itemId + '/children');
    if (this.config.autoExpandRequests) {
      serviceUrl.searchParams.append('expand', '*');
    }
    return this.doRequest<Array<T>>(serviceUrl.href).catch((r) => {
      return [];
    });
  }

  public async getContentAncestors(link: ContentReference): Promise<Array<IContent>> {
    let itemId: string = ContentLinkService.createApiId(link);
    let serviceUrl: URL = new URL(`${this.config.epiBaseUrl}${this.componentService}${itemId}/ancestors`);
    if (this.config.autoExpandRequests) {
      serviceUrl.searchParams.append('expand', '*');
    }
    return this.doRequest<Array<IContent>>(serviceUrl.href).catch((r) => {
      return [];
    });
  }

  /**
   * Perform the actual request
   *
   * @param url The URL to request the data from
   * @param options The Request options to use
   */
  protected async doRequest<T>(url: string, options?: AxiosRequestConfig): Promise<T> {
    if (this.isDisabled()) {
      return Promise.reject('The Content Delivery API has been disabled');
    }
    if (this.isInEditMode()) {
      let urlObj = new URL(url);
      urlObj.searchParams.append('epieditmode', 'True');
      //Add channel...
      //Add project...
      urlObj.searchParams.append('preventCache', Math.round(Math.random() * 100000000).toString());
      url = urlObj.href;
    }

    options = options ? options : this.getRequestSettings();
    if (this.debug) console.debug('Requesting: ' + url);
    options.url = url;
    return Axios.request<any, AxiosResponse<T>>(options)
      .then((response) => {
        if (this.debug) console.debug(`Response from ${url}:`, response.data);
        return response.data;
      })
      .catch((reason) => {
        if (this.debug) console.error(`Response from ${url}: HTTP Fetch error `, reason);
        throw reason;
      });
  }

  protected getMethodServiceUrl(content: ContentLink, method: string): string {
    let contentUrl: string = this.config.epiBaseUrl;
    contentUrl = contentUrl + this.methodService;
    contentUrl = contentUrl + content.guidValue + '/' + method;
    return contentUrl;
  }

  /**
   * Build the request parameters needed to perform the call to the Content Delivery API
   *
   * @param verb The verb for the generated configuration
   */
  protected getRequestSettings(verb?: Method): AxiosRequestConfig {

    let options: AxiosRequestConfig = {
      method: verb ? verb : 'get',
      baseURL: this.config.epiBaseUrl,
      withCredentials: true,
      headers: { ...this.getHeaders() },
      transformRequest: [
        (data: any, headers: any) => {
          if (data) {
            headers['Content-Type'] = 'application/json';
            return JSON.stringify(data);
          }
          return data;
        },
      ],
      responseType: 'json',
    };
    if (this.config.networkAdapter) {
      options.adapter = this.config.networkAdapter;
    }
    return options;
  }

  protected getHeaders(customHeaders?: object): object {
    let defaultHeaders = {
      Accept: 'application/json',
      'Accept-Language': this.config.defaultLanguage, //@ToDo: Convert to context call, with default
    };
    if (!customHeaders) return defaultHeaders;

    return {
      ...defaultHeaders,
      ...customHeaders,
    };
  }

  public static IsActionResponse(response: PathResponse): response is ActionResponse<any> {
    if (
      response &&
      (response as ActionResponse<any>).responseType &&
      (response as ActionResponse<any>).responseType == ResponseType.ActionResult
    ) {
      return true;
    }
    return false;
  }

  private counter: number = 0;

  protected buildNetworkError(reason: any, path: string = ''): NetworkErrorData {
    const errorId = ++this.counter;
    return {
      name: {
        propertyDataType: 'String',
        value: 'Error',
      },
      contentType: ['Errors', 'NetworkError'],
      contentLink: {
        guidValue: '',
        id: errorId,
        providerName: 'ContentDeliveryAPI_Errors',
        url: path,
        workId: 0,
      },
      error: {
        propertyDataType: 'Unknown',
        value: '', //reason,
      },
    };
  }
}

export default ContentDeliveryAPI;