import { GraphQLClient, GraphQLRequestClient, PageInfo } from '@sitecore-jss/sitecore-jss/graphql';
import { debug } from '@sitecore-jss/sitecore-jss';
import {getScPersonalizedVariantIds, getScPersonalizedRewrite} from '../../lib/personalizationUtils'
//@ts-ignore
import { getRuleEngineInstance } from 'sitecore-jss-rule-engine'


/** @private */

export const languageError = 'The list of languages cannot be empty';
export const siteError = 'The service needs a site name';

/**
 * @param {string} siteName to inject into error text
 * @private
 */
export function getSiteEmptyError(siteName: string) {
  return `Site "${siteName}" does not exist or site item tree is missing`;
}

const languageEmptyError = 'The language must be a non-empty string';

/**
 * GQL query made dynamic based on schema differences between SXP and XM Cloud
 * @param {boolean} usesPersonalize flag to detrmine which variation of a query to run
 * @returns GraphQL query to fetch site paths with
 */
const defaultQuery = () => /* GraphQL */ `
query SitecorePersonalizeSitemapQuery(
  $siteName: String!
  $language: String!
  $includedPaths: [String]
  $excludedPaths: [String]
  $pageSize: Int = 100
  $after: String
) {
  site {
    siteInfo(site: $siteName) {
      routes(
        language: $language
        includedPaths: $includedPaths
        excludedPaths: $excludedPaths
        first: $pageSize
        after: $after
      ){
        total
        pageInfo {
          endCursor
          hasNext
        }
        results {
          path: routePath
          route: route{
            personalizationRule:field(name:"PersonalizationRules"){
              value
            },
            personalizeOnEdge:field(name:"PersonalizeOnEdge"){
              value
            },
            isStaticRender:field(name:"IsStaticRender"){
              value
            }
          }
        }
      }
    }
  }
}`;
/**
 * type for input variables for the site routes query
 */
interface SiteRouteQueryVariables {
  /**
   * Required. The name of the site being queried.
   */
  siteName: string;
  /**
   * Required. The language to return routes/pages for.
   */
  language: string;
  /**
   * Optional. Only paths starting with these provided prefixes will be returned.
   */
  includedPaths?: string[];
  /**
   * Optional. Paths starting with these provided prefixes will be excluded from returned results.
   */
  excludedPaths?: string[];

  /** common variable for all GraphQL queries
   * it will be used for every type of query to regulate result batch size
   * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination.
   * @default 100
   */
  pageSize?: number;
}

/**
 * Schema of data returned in response to a "site" query request
 * @template T The type of objects being requested.
 */
export interface SiteRouteQueryResult<T> {
  site: {
    siteInfo: {
      routes: {
        /**
         * Data needed to paginate the site results
         */
        pageInfo: PageInfo;
        results: T[];
      };
    };
  };
}

/**
 * The schema of data returned in response to a routes list query request
 */
export type RouteListQueryResult = {
  path: string;
  route?: {
    personalizationRule: {
      value: string;
    };
    personalizeOnEdge: { 
      value: string; 
    };
    isStaticRender: {
      value: string;
    }
  };
};

/**
 * Configuration options for @see GraphQLSitemapService instances
 */
export interface BaseGraphQLSitemapServiceConfig
  extends Omit<SiteRouteQueryVariables, 'language' | 'siteName'> {
  /**
   * Your Graphql endpoint
   */
  endpoint: string;

  /**
   * The API key to use for authentication.
   */
  apiKey: string;

  /**
   * A flag for whether to include personalized routes in service output - only works on XM Cloud
   * turned off by default
   */
  includePersonalizedRoutes?: boolean;
}

/**
 * Object model of a site page item.
 */
export type StaticPath = {
  params: {
    path: string[];
    isStaticRender: boolean;
  };
  locale?: string;
};

/**
 * Service that fetches the list of site pages using Sitecore's GraphQL API.
 * Used to handle a single site
 * This list is used for SSG and Export functionality.
 * @mixes SearchQueryService<PageListQueryResult>
 */
export abstract class BasePersonalizeGraphQLSitemapService {
  private _graphQLClient: GraphQLClient;

  /**
   * GraphQL client accessible by descendant classes when needed
   */
  protected get graphQLClient() {
    return this._graphQLClient;
  }

  /**
   * Gets the default query used for fetching the list of site pages
   */
  protected get query(): string {
    return defaultQuery();
  }

  /**
   * Creates an instance of graphQL sitemap service with the provided options
   * @param {GraphQLSitemapServiceConfig} options instance
   */
  constructor(public options: BaseGraphQLSitemapServiceConfig) {
    this._graphQLClient = this.getGraphQLClient();    
  }

  /**
   * Fetch sitemap which could be used for generation of static pages during `next export`.
   * The `locale` parameter will be used in the item query, but since i18n is not supported,
   * the output paths will not include a `language` property.
   * @param {string} locale which application supports
   * @returns an array of @see StaticPath objects
   */
  async fetchExportSitemap(locale: string): Promise<StaticPath[]> {
    const formatPath = (path: string[], _language: string, isStaticRender: boolean) => ({
      params: {
        path,
        isStaticRender
      }
    });

    return this.fetchSitemap([locale], formatPath);
  }

  /**
   * Fetch sitemap which could be used for generation of static pages using SSG mode
   * @param {string[]} locales locales which application supports
   * @returns an array of @see StaticPath objects
   */
  async fetchSSGSitemap(locales: string[]): Promise<StaticPath[]> {
    const formatPath = (path: string[], locale: string, isStaticRender: boolean) => ({
      params: {
        path,
        isStaticRender
      },
      locale
    });

    return this.fetchSitemap(locales, formatPath);
  }

  protected async getTranformedPaths(
    siteName: string,
    languages: string[],
    formatStaticPath: (path: string[], language: string, isStaticRender: boolean) => StaticPath
  ) {
    const paths = new Array<StaticPath>();

    for (const language of languages) {
      if (language === '') {
        throw new RangeError(languageEmptyError);
      }

      debug.sitemap('fetching sitemap data for %s %s', language, siteName);

      const results = await this.fetchLanguageSitePaths(language, siteName);
      const transformedPaths = await this.transformLanguageSitePaths(
        results,
        formatStaticPath,
        language
      );

      paths.push(...transformedPaths);
    }

    return paths;
  }

  protected async transformLanguageSitePaths(
    sitePaths: RouteListQueryResult[],
    formatStaticPath: (path: string[], language: string, isStaticRender: boolean) => StaticPath,
    language: string
  ): Promise<StaticPath[]> {

    const formatPath = (path: string, isStaticRender: boolean) => {
      return formatStaticPath(path.replace(/^\/|\/$/g, '').split('/'), language, isStaticRender);
    }
      

    const aggregatedPaths: StaticPath[] = [];    

    sitePaths.forEach((item) => {

      if (!item) return;

      aggregatedPaths.push(formatPath(item.path, item.route?.isStaticRender?.value == "1"));

      // check for type safety's sake - personalize may be empty depending on query type
      if (item.route?.personalizationRule?.value?.length && 
          item.route?.personalizeOnEdge?.value == "1")
      {
        const ruleEngineInstance = getRuleEngineInstance();
        const ruleEngineContext = ruleEngineInstance.getRuleEngineContext();
        const parsedRule = ruleEngineInstance.parseRuleXml(item.route?.personalizationRule?.value, ruleEngineContext);
        const personalizationVariationIds = getScPersonalizedVariantIds(parsedRule);                        
        if(personalizationVariationIds)
        {
          aggregatedPaths.push(                      
              ...(personalizationVariationIds.map((varId:string) =>             
              formatPath(getScPersonalizedRewrite(item.path, varId), item?.route?.isStaticRender?.value == "1")
            ) || {})
          );
        }
      }
    });

    return aggregatedPaths;
  }


  protected async fetchLanguageSitePaths(
    language: string,
    siteName: string
  ): Promise<RouteListQueryResult[]> {
    const args: SiteRouteQueryVariables = {
      siteName: siteName,
      language: language,
      pageSize: this.options.pageSize,
      includedPaths: this.options.includedPaths,
      excludedPaths: this.options.excludedPaths,
    };
    let results: RouteListQueryResult[] = [];
    let hasNext = true;
    let after = '';

    while (hasNext) {
      const fetchResponse = await this.graphQLClient.request<
        SiteRouteQueryResult<RouteListQueryResult>
      >(this.query, {
        ...args,
        after,
      });

      if (!fetchResponse?.site?.siteInfo) {
        throw new RangeError(getSiteEmptyError(siteName));
      } else {
        results = results.concat(fetchResponse.site.siteInfo.routes?.results);
        hasNext = fetchResponse.site.siteInfo.routes?.pageInfo.hasNext;
        after = fetchResponse.site.siteInfo.routes?.pageInfo.endCursor;
      }
    }

    return results;
  }

  /**
   * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default
   * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you
   * want to use something else.
   * @returns {GraphQLClient} implementation
   */
  protected getGraphQLClient(): GraphQLClient {
    return new GraphQLRequestClient(this.options.endpoint, {
      apiKey: this.options.apiKey,
      debugger: debug.sitemap,
    });
  }

  /**
   * Fetch a flat list of all pages that belong to the specificed site and have a
   * version in the specified language(s).
   * @param {string[]} languages Fetch pages that have versions in this language(s).
   * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types.
   * @returns list of pages
   * @throws {RangeError} if the list of languages is empty.
   * @throws {RangeError} if the any of the languages is an empty string.
   */
  protected abstract fetchSitemap(
    languages: string[],
    formatStaticPath: (path: string[], language: string, isStaticRender: boolean) => StaticPath
  ): Promise<StaticPath[]>;
}