// 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 type {
  SolidDataset,
  UrlString,
  WebId,
  WithServerResourceInfo,
} from "..";
import { asIri, getIriAll, getSolidDataset, getThing, getThingAll } from "..";
import { foaf, pim, rdfs } from "../constants";
import { getSourceIri } from "../resource/resource";

export type ProfileAll<T extends SolidDataset & WithServerResourceInfo> = {
  webIdProfile: T;
  altProfileAll: Array<SolidDataset & WithServerResourceInfo>;
};

/**
 * List all the alternative profiles IRI found in a given WebID profile.
 *
 * Note that some of these profiles may be private, and you may not have access to
 * the resulting resource.
 *
 * @param webId The WebID of the user's whose alternative profiles you are discovering.
 * @param webIdProfile The WebID profile obtained dereferencing the provided WebID.
 * @returns A list of URLs of the user's alternative profiles.
 * @since 1.20.0
 */
export function getAltProfileUrlAllFrom(
  webId: WebId,
  webIdProfile: SolidDataset,
): UrlString[] {
  const webIdThing = getThing(webIdProfile, webId);

  const altProfileUrlAll = getThingAll(webIdProfile)
    .filter((thing) => getIriAll(thing, foaf.primaryTopic).length > 0)
    .map(asIri)
    .concat(webIdThing ? getIriAll(webIdThing, rdfs.seeAlso) : [])
    .concat(webIdThing ? getIriAll(webIdThing, foaf.isPrimaryTopicOf) : [])
    .filter((profileIri) => profileIri !== getSourceIri(webIdProfile));

  // Deduplicate the results.
  return Array.from(new Set(altProfileUrlAll));
}

/**
 * Get all the Profile Resources discoverable from a WebID Profile.
 *
 * A WebID Profile may be any RDF resource on the Web, it doesn't have
 * to be a Solid resource. That is why, in order to expose a Solid-enabled part
 * of their profile, some WebID profiles link to one or more Extended Profile Resources,
 * which may are Solid resources. A WebID resource should be public, so `getProfileAll` will
 * issue an unauthenticated request to the WebID, and only use the provided
 * authenticated `fetch` (if any) to access extended profile documents.
 *
 * @param webId WebID of the agent you want the profile of.
 * @param options Optional parameter
 *  - `options.webIdProfile`: The data retrieved when looking up the WebID. This
 *    will be fetched if not provided.
 *  - `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 Promise resolving to an array of [[SolidDataset]], each corresponding
 *  to a personal profile document discoverable from the WebID Profile Document.
 *  If none are found, the WebID profile document itself is returned.
 * @since 1.16.0
 */
export async function getProfileAll<
  T extends SolidDataset & WithServerResourceInfo,
>(
  webId: WebId,
  options?: {
    fetch?: typeof fetch;
    webIdProfile?: T;
  },
): Promise<ProfileAll<T>>;
export async function getProfileAll(
  webId: WebId,
  options?: { fetch?: typeof fetch; webIdProfile: undefined },
): Promise<ProfileAll<SolidDataset & WithServerResourceInfo>>;
export async function getProfileAll<
  T extends SolidDataset & WithServerResourceInfo,
>(
  webId: WebId,
  options?: {
    fetch?: typeof fetch;
    webIdProfile?: T;
  },
): Promise<ProfileAll<T | (SolidDataset & WithServerResourceInfo)>> {
  const authFetch = options?.fetch ?? fetch;
  const webIdProfile =
    options?.webIdProfile ??
    // This should always use an unauthenticated fetch.
    (await getSolidDataset(webId));
  const altProfileAll = (
    await Promise.allSettled(
      getAltProfileUrlAllFrom(webId, webIdProfile).map((uniqueProfileIri) =>
        getSolidDataset(uniqueProfileIri, { fetch: authFetch }),
      ),
    )
  )
    // Ignore the alternative profiles lookup which failed.
    .filter(
      (result): result is PromiseFulfilledResult<T> =>
        result.status === "fulfilled",
    )
    .map((successfulResult) => successfulResult.value);

  return {
    webIdProfile,
    altProfileAll,
  };
}

/**
 * Discover the Pods an agent advertises for in their profile resources. Both the
 * agent's WebID and alternative profiles are fetched. Note that this function will
 * only return URLs of Pods linked to using the `pim:storage`, i.e. a triple
 * looking like <myWebid, pim:storage, myPodUrl> should appear in the profile
 * resources.
 *
 * @param webId The WebID of the agent whose Pods should be discovered
 * @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 Promise resolving to an array containing the URLs of all the Pods
 * linked from the agent's profile resource using the `pim:storage` predicate.
 * @since 1.18.0
 */
export async function getPodUrlAll(
  webId: WebId,
  options?: { fetch?: typeof fetch },
): Promise<UrlString[]> {
  const profiles = await getProfileAll(webId, options);
  return getPodUrlAllFrom(profiles, webId);
}

/**
 * Discover the Pods advertised for in the provided profile resources. Note that
 * this function will only return URLs of Pods linked to using the `pim:storage`
 * predicate, i.e. a triple looking like <myWebid, pim:storage, myPodUrl>
 * should appear in the profile resources.
 *
 * @param profiles The profile resources in which the Pods should be discovered
 * @param webId The WebID of the agent whose Pods should be discovered
 * @returns An array containing the URLs of all the Pods linked from the agent's
 * profile resource using the `pim:storage` predicate.
 * @since 1.18.0
 */
export function getPodUrlAllFrom(
  profiles: ProfileAll<SolidDataset & WithServerResourceInfo>,
  webId: WebId,
): UrlString[] {
  const result: Set<string> = new Set();
  [profiles.webIdProfile, ...profiles.altProfileAll].forEach(
    (profileResource) => {
      const webIdThing = getThing(profileResource, webId);
      if (webIdThing !== null) {
        getIriAll(webIdThing, pim.storage).forEach((podIri) =>
          result.add(podIri),
        );
      }
    },
  );
  return Array.from(result);
}

/**
 * Get the WebID Profile document dataset.
 *
 * @example
 * ```
 * const webId = "https://example.org/people/me";
 * const profile = await getWebIdDataset(webId);
 * const podRoot = getPodUrlAllFrom({ webIdProfile: profile, altProfileAll: [] }, webId);
 * const profileThing = getThing(profile, webId);
 * const name = getStringNoLocale(profileThing, FOAF.name);
 * ```
 *
 * @param webId The WebID of the agent whose WebID Profile dataset is to be fetched.
 * @returns a SolidDataset for the WebID Profile document.
 * @since 1.24.0
 */
export async function getWebIdDataset(
  webId: WebId,
): Promise<ReturnType<typeof getSolidDataset>> {
  return getSolidDataset(webId);
}
