import { BaseState } from './base-state.js';
import { parseLink } from '../http/util.js';
import { Link } from '../link.js';
import Client from '../client.js';

/**
 * Turns a HTTP response into a JsonApiState
 */
export const factory = async (client: Client, uri: string, response: Response): Promise<BaseState<JsonApiTopLevelObject>> => {

  const body = await response.json();

  const links = parseLink(uri, response.headers.get('Link'));
  links.add(
    ...parseJsonApiLinks(uri, body),
    ...parseJsonApiCollection(uri, body),
  );

  return new BaseState({
    client,
    uri,
    data: body,
    headers: response.headers,
    links: links,
  });

};
/**
 * A JSON:API link can either be a string, or an object with at least a href
 * property.
 */
type JsonApiLink = string | { href: string };

/**
 * This type is a full 'links' object, which might appear on the top level
 * or on resource objects.
 */
type JsonApiLinksObject = {
  self?: JsonApiLink;
  profile?: JsonApiLink;
  [rel: string]: JsonApiLink | JsonApiLink[] | undefined;
};

/**
 * This is a single JSON:API resource. Its type contains just the properties
 * we care about.
 */
type JsonApiResource = {
  type: string;
  id: string;
  links?: JsonApiLinksObject;
};


/**
 * This type represents a valid JSON:API response. We're only interested
 * in the links object at the moment, so everything else is (for now)
 * untyped.
 */
type JsonApiTopLevelObject = {
  links?: JsonApiLinksObject;
  data: JsonApiResource | JsonApiResource[] | null;
  [s: string]: any;
};

/**
 * This function takes a JSON:API object, and extracts the links property.
 */
function parseJsonApiLinks(contextUri: string, body: JsonApiTopLevelObject): Link[] {

  const result: Link[] = [];

  if (body.links === undefined) {
    return result;
  }

  for (const [rel, linkValue] of Object.entries(body.links)) {

    if (Array.isArray(linkValue)) {
      result.push(...linkValue.map( link => parseJsonApiLink(contextUri, rel, link)));
    } else {
      result.push(parseJsonApiLink(contextUri, rel, linkValue!));
    }

  }

  return result;

}

/**
 * Find collection members in JSON:API objects.
 *
 * A JSON:API top-level object might represent a collection that has 0 or more
 * members.
 *
 * Members of this collection should appear as an 'item' link to the parent.
 */
function parseJsonApiCollection(contextUri: string, body: JsonApiTopLevelObject): Link[] {

  if (!Array.isArray(body.data)) {
    // Not a collection
    return [];
  }

  const result: Link[] = [];
  for (const member of body.data) {

    if ('links' in member && 'self' in member.links!) {

      const selfLink = parseJsonApiLink(contextUri, 'self', member.links!.self!);
      result.push({
        context: contextUri,
        href: selfLink.href,
        rel: 'item'
      });

    }
  }

  return result;

}

/**
 * This function takes a single link value from a JSON:API link object, and
 * returns a object of type Link
 */
function parseJsonApiLink(contextUri: string, rel: string, link: JsonApiLink): Link {

  return ({
    context: contextUri,
    rel,
    href: typeof link === 'string' ? link : link.href,
  });

}
