// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { acp } from "../constants";
import type {
  SolidDataset,
  File,
  Url,
  UrlString,
  WithServerResourceInfo,
  WithResourceInfo,
} from "../interfaces";
import { hasServerResourceInfo } from "../interfaces";
import { internal_toIriString } from "../interfaces.internal";
import { getFile } from "../resource/file";
import { getResourceInfo, getSourceUrl } from "../resource/resource";
import type { WithAcl } from "../acl/acl";
import { hasAccessibleAcl } from "../acl/acl";
import { internal_fetchAcl, internal_setAcl } from "../acl/acl.internal";
import { getSolidDataset, saveSolidDatasetAt } from "../resource/solidDataset";
import type { AccessControlResource } from "./control";
import {
  getAcrPolicyUrlAll,
  getMemberAcrPolicyUrlAll,
  getMemberPolicyUrlAll,
  getPolicyUrlAll,
  hasLinkedAcr,
} from "./control";
import { internal_getAcr, internal_setAcr } from "./control.internal";
import { normalizeServerSideIri } from "../resource/iri.internal";
import { isAcr } from "./acp.internal";

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Fetch a SolidDataset and its associated Access Control Resource (if available to the current user).
 *
 * @param url URL of the SolidDataset to fetch.
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @returns A SolidDataset and the ACR that applies to it, if available to the authenticated user.
 * @since 1.6.0
 */
export async function getSolidDatasetWithAcr(
  url: Url | UrlString,
  options?: { fetch?: typeof fetch },
): Promise<SolidDataset & WithServerResourceInfo & WithAcp> {
  const urlString = internal_toIriString(url);
  const solidDataset = await getSolidDataset(urlString, options);
  const acp = await fetchAcr(solidDataset, options);
  return { ...solidDataset, ...acp };
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Fetch a file and its associated Access Control Resource (if available to the current user).
 *
 * @param url URL of the file to fetch.
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @returns A file and the ACR that applies to it, if available to the authenticated user.
 * @since 1.6.0
 */
export async function getFileWithAcr(
  url: Url | UrlString,
  options?: { fetch: typeof fetch },
): Promise<File & WithAcp> {
  const urlString = internal_toIriString(url);

  const file = await getFile(urlString, options);
  const acp = await fetchAcr(file, options);
  return Object.assign(file, acp);
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Retrieve information about a Resource and its associated Access Control Resource (if available to
 * the current user), without fetching the Resource itself.
 *
 * @param url URL of the Resource about which to fetch its information.
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @returns Metadata describing a Resource, and the ACR that applies to it, if available to the authenticated user.
 * @since 1.6.0
 */
export async function getResourceInfoWithAcr(
  url: Url | UrlString,
  options?: { fetch?: typeof fetch },
): Promise<WithServerResourceInfo & WithAcp> {
  const urlString = internal_toIriString(url);
  const resourceInfo = await getResourceInfo(urlString, options);
  const acp = await fetchAcr(resourceInfo, options);
  return { ...resourceInfo, ...acp };
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Fetch a SolidDataset, and:
 * - if the Resource is governed by an ACR: its associated Access Control Resource (if available to
 *                                          the current user), and all the Access Control Policies
 *                                          referred to therein, if available to the current user.
 * - if the Resource is governed by an ACL: its associated Resource ACL (if available to the current
 *                                          user), or its Fallback ACL if it does not exist.
 *
 * @param url URL of the SolidDataset to fetch.
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @returns A SolidDataset and either the ACL access data or the ACR access data, if available to the current user.
 * @since 1.6.0
 */
export async function getSolidDatasetWithAccessDatasets(
  url: Url | UrlString,
  options?: { fetch?: typeof fetch },
): Promise<SolidDataset & (WithAcp | WithAcl)> {
  const urlString = internal_toIriString(url);
  const solidDataset = await getSolidDataset(urlString, options);
  if (hasAccessibleAcl(solidDataset)) {
    const acl = await internal_fetchAcl(solidDataset, options);
    return internal_setAcl(solidDataset, acl);
  }
  const acr = await fetchAcr(solidDataset, options);
  return { ...solidDataset, ...acr };
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Fetch a File, and:
 * - if the Resource is governed by an ACR: its associated Access Control Resource (if available to
 *                                          the current user), and all the Access Control Policies
 *                                          referred to therein, if available to the current user.
 * - if the Resource is governed by an ACL: its associated Resource ACL (if available to the current
 *                                          user), or its Fallback ACL if it does not exist.
 *
 * @param url URL of the File to fetch.
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @returns A File and either the ACL access data or the ACR access data, if available to the current user.
 * @since 1.6.0
 */
export async function getFileWithAccessDatasets(
  url: Url | UrlString,
  options?: { fetch?: typeof fetch },
): Promise<File & (WithAcp | WithAcl)> {
  const urlString = internal_toIriString(url);
  const file = await getFile(urlString, options);
  if (hasAccessibleAcl(file)) {
    const acl = await internal_fetchAcl(file, options);
    return internal_setAcl(file, acl);
  }
  const acr = await fetchAcr(file, options);
  return Object.assign(file, acr);
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Fetch information about a Resource, and:
 * - if the Resource is governed by an ACR: its associated Access Control Resource (if available to
 *                                          the current user), and all the Access Control Policies
 *                                          referred to therein, if available to the current user.
 * - if the Resource is governed by an ACL: its associated Resource ACL (if available to the current
 *                                          user), or its Fallback ACL if it does not exist.
 *
 * @param url URL of the Resource information about which to fetch.
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @returns Information about a Resource and either the ACL access data or the ACR access data, if available to the current user.
 * @since 1.6.0
 */
export async function getResourceInfoWithAccessDatasets(
  url: Url | UrlString,
  options?: { fetch?: typeof fetch },
): Promise<WithServerResourceInfo & (WithAcp | WithAcl)> {
  const urlString = internal_toIriString(url);
  const resourceInfo = await getResourceInfo(urlString, options);
  if (hasAccessibleAcl(resourceInfo)) {
    const acl = await internal_fetchAcl(resourceInfo, options);
    return internal_setAcl(resourceInfo, acl);
  }
  const acr = await fetchAcr(resourceInfo, options);
  return { ...resourceInfo, ...acr };
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Save a Resource's Access Control Resource.
 *
 * @param resource Resource with an Access Control Resource that should be saved.
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @since 1.6.0
 */
export async function saveAcrFor<ResourceExt extends WithAccessibleAcr>(
  resource: ResourceExt,
  options?: { fetch?: typeof fetch },
): Promise<ResourceExt> {
  const acr = internal_getAcr(resource);
  const savedAcr = await saveSolidDatasetAt(getSourceUrl(acr), acr, options);
  return internal_setAcr(resource, savedAcr);
}

/**
 * The Access Control Resource of Resources that conform to this type were attempted to be fetched together with those Resources. This might not have been successful; see [[hasAccessibleAcr]] to check.
 * @since 1.6.0
 */
export type WithAcp = {
  internal_acp: {
    acr: AccessControlResource | null;
  };
};
/**
 * Resources that conform to this type have an Access Control Resource attached. See [[hasAccessibleAcr]].
 * @since 1.6.0
 */
export type WithAccessibleAcr = WithAcp & {
  internal_acp: {
    acr: Exclude<WithAcp["internal_acp"]["acr"], null>;
  };
};

/**
 * @param resource Resource of which to check whether it has an Access Control Resource attached.
 * @returns Boolean representing whether the given Resource has an Access Control Resource attached for use in e.g. [[getPolicyUrlAll]].
 * @since 1.6.0
 */
export function hasAccessibleAcr(
  resource: WithAcp,
): resource is WithAccessibleAcr {
  return (
    typeof resource.internal_acp === "object" &&
    resource.internal_acp !== null &&
    typeof resource.internal_acp.acr === "object" &&
    resource.internal_acp.acr !== null
  );
}

async function fetchAcr(
  resource: WithServerResourceInfo,
  options?: { fetch?: typeof fetch },
): Promise<WithAcp> {
  let acrUrl: UrlString | undefined;
  if (hasLinkedAcr(resource)) {
    // Whereas a Resource can generally have multiple linked Resources for the same relation,
    // it can only have one Access Control Resource for that ACR to be valid.
    // Hence the accessing of [0] directly:
    const { linkedResources } = resource.internal_resourceInfo;
    [acrUrl] = linkedResources[acp.accessControl];
  } else if (hasAccessibleAcl(resource)) {
    // The ACP proposal will be updated to expose the Access Control Resource
    // via a Link header with rel="acl", just like WAC. That means that if
    // an ACL is advertised, we can still fetch its metadata — if that indicates
    // that it's actually an ACP Access Control Resource, then we can fetch that
    // instead.
    let aclResourceInfo;
    try {
      aclResourceInfo = await getResourceInfo(
        resource.internal_resourceInfo.aclUrl,
        options,
      );
    } catch {
      // Since both ACL and ACR will be discovered through the same header, we
      // need to ignore errors here so that in the case of ACL not found, the
      // code can resume and a new ACL can be initialized. The case for ACR is
      // covered in the code below, since in this case the ACR is always present
    }

    if (aclResourceInfo && isAcr(aclResourceInfo)) {
      acrUrl = getSourceUrl(aclResourceInfo);
    }
  }
  // If the Resource doesn't advertise an ACR via the old Link header,
  // nor via a rel="acl" header, then return, indicating that no ACR could be
  // fetched:
  if (typeof acrUrl !== "string") {
    return {
      internal_acp: {
        acr: null,
      },
    };
  }
  let acr: SolidDataset & WithResourceInfo;
  try {
    acr = await getSolidDataset(acrUrl, options);
  } catch {
    return {
      internal_acp: {
        acr: null,
      },
    };
  }

  const acrDataset: AccessControlResource = {
    ...acr,
    accessTo: getSourceUrl(resource),
  };
  const acpInfo: WithAccessibleAcr = {
    internal_acp: {
      acr: acrDataset,
    },
  };
  return acpInfo;
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * To make it easy to fetch all the relevant Access Policy Resources,
 * this function returns all referenced Access Policy Resources referenced in an
 * Access Control Resource.
 * In other words, if Access Controls refer to different Policies in the same
 * Access Policy Resource, this function will only return that Access Policy
 * Resource's URL once.
 *
 * @param withAcr A Resource with an Access Control Resource attached.
 * @returns List of all unique Access Policy Resources that are referenced in the given Access Control Resource.
 * @since 1.6.0
 */
export function getReferencedPolicyUrlAll(
  withAcr: WithAccessibleAcr,
): UrlString[] {
  const policyUrls: UrlString[] = getPolicyUrlAll(withAcr)
    .map(normalizeServerSideIri)
    .concat(getMemberPolicyUrlAll(withAcr).map(normalizeServerSideIri))
    .concat(getAcrPolicyUrlAll(withAcr).map(normalizeServerSideIri))
    .concat(getMemberAcrPolicyUrlAll(withAcr).map(normalizeServerSideIri));

  const uniqueUrls = Array.from(new Set(policyUrls));
  return uniqueUrls;
}

/**
 * Verify whether the access to the given resource is controlled using the ACP
 * system.
 * @param resource The target resource
 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
 * @returns True if the access to the resource is controlled using ACP, false otherwise.
 * @since 1.14.0.
 */
export async function isAcpControlled(
  resource: Url | UrlString,
  options?: { fetch?: typeof fetch },
): Promise<boolean> {
  const urlString = internal_toIriString(resource);
  const resourceInfo = await getResourceInfo(urlString, options);
  return hasAccessibleAcr(await fetchAcr(resourceInfo, options));
}

/**
 * ```{note} The Web Access Control specification is not yet finalised. As such, this
 * function is still experimental and subject to change, even in a non-major release.
 * ```
 *
 * Given a Resource, find out the URL of its governing Access Control Resource.
 *
 * @param resource Resource which should be governed by Access Policies.
 * @returns The URL of the Access Control Resource, or undefined if not ACR is found.
 * @since 1.15.0
 */
export function getLinkedAcrUrl<Resource extends WithServerResourceInfo>(
  resource: Resource,
): UrlString | undefined {
  if (!hasServerResourceInfo(resource)) {
    return undefined;
  }
  // Two rels types are acceptable to indicate a link to an ACR.
  const acrLinks = [acp.accessControl, "acl"].map((rel) => {
    if (
      Array.isArray(resource.internal_resourceInfo.linkedResources[rel]) &&
      resource.internal_resourceInfo.linkedResources[rel].length === 1
    ) {
      return resource.internal_resourceInfo.linkedResources[rel][0];
    }

    return undefined;
  });
  return acrLinks.find((x) => x !== undefined);
}

// This file currently acts as an index for the `acp` module.
export { getVcAccess } from "./util/getVcAccess";
export { setVcAccess } from "./util/setVcAccess";
