/*
 This file is part of GNU Taler
 (C) 2022 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

import {
  CoreApiResponse,
  TalerError,
  TalerErrorCode,
} from "@gnu-taler/taler-util";

import type { MessageFromBackend } from "./platform/api.js";
// FIXME: mem leak problems
// import { encodeCrockForURI } from "@gnu-taler/web-util/browser";


/**
 * This will modify all the pages that the user load when navigating with Web Extension enabled
 *
 * Can't do useful integration since it run in ISOLATED (or equivalent) mode.
 *
 * If taler support is expected, it will inject a script which will complete the integration.
 */

// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#content_script_environment

// ISOLATED mode in chromium browsers
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world
// X-Ray vision in Firefox
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts#xray_vision_in_firefox

// *** IMPORTANT ***

// Content script lifecycle during navigation
// In Firefox: Content scripts remain injected in a web page after the user has navigated away,
// however, window object properties are destroyed.
// In Chrome: Content scripts are destroyed when the user navigates away from a web page.

const documentDocTypeIsHTML =
  window.document.doctype && window.document.doctype.name === "html";
const suffixIsNotXMLorPDF =
  !window.location.pathname.endsWith(".xml") &&
  !window.location.pathname.endsWith(".pdf");
const rootElementIsHTML =
  document.documentElement.nodeName &&
  document.documentElement.nodeName.toLowerCase() === "html";
// const pageAcceptsTalerSupport = document.head.querySelector(
//   "meta[name=taler-support]",
// );

function validateTalerUri(uri: string): boolean {
  return (
    !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
  );
}

function convertURIToWebExtensionPath(uri: string) {
  const url = new URL(
    chrome.runtime.getURL(
      // FIXME: mem leak problems
      // `static/wallet.html#/taler-uri/${encodeCrockForURI(uri)}`,
      `static/wallet.html#/taler-uri-simple/${encodeURIComponent(uri)}`,
    ),
  );
  return url.href;
}

// safe check, if one of this is true then taler handler is not useful
// or not expected
const shouldNotInject =
  !documentDocTypeIsHTML ||
  !suffixIsNotXMLorPDF ||
  // !pageAcceptsTalerSupport ||
  !rootElementIsHTML;

const logger = {
  debug: (...msg: any[]) => { },
  info: (...msg: any[]) =>
    console.log(`${new Date().toISOString()} TALER`, ...msg),
  error: (...msg: any[]) =>
    console.error(`${new Date().toISOString()} TALER`, ...msg),
};

// logger.debug = logger.info

/**
 */
function redirectToTalerActionHandler(element: HTMLMetaElement) {
  const name = element.getAttribute("name");
  if (!name) return;
  if (name !== "taler-uri") return;
  const uri = element.getAttribute("content");
  if (!uri) return;

  if (!validateTalerUri(uri)) {
    logger.error(`taler:// URI is invalid: ${uri}`);
    return;
  }

  const walletPage = convertURIToWebExtensionPath(uri);
  window.location.replace(walletPage);
}

function injectTalerSupportScript(head: HTMLHeadElement, trusted: boolean) {
  const meta = head.querySelector("meta[name=taler-support]");
  if (!meta) return;
  const content = meta.getAttribute("content");
  if (!content) return;
  const features = content.split(",");

  const debugEnabled = meta.getAttribute("debug") === "true";
  const hijackEnabled = features.indexOf("uri") !== -1;
  const talerApiEnabled = features.indexOf("api") !== -1 && trusted;

  const scriptTag = document.createElement("script");
  scriptTag.setAttribute("async", "false");
  const url = new URL(
    chrome.runtime.getURL("/dist/taler-wallet-interaction-support.js"),
  );
  url.searchParams.set("id", chrome.runtime.id);
  if (debugEnabled) {
    url.searchParams.set("debug", "true");
  }
  if (talerApiEnabled) {
    url.searchParams.set("api", "true");
  }
  if (hijackEnabled) {
    url.searchParams.set("hijack", "true");
  }
  scriptTag.src = url.href;

  try {
    head.insertBefore(
      scriptTag,
      head.children.length ? head.children[0] : null,
    );
  } catch (e) {
    logger.info("inserting link handler failed!");
    logger.error(e);
  }
}

export interface ExtensionOperations {
  isAutoOpenEnabled: {
    request: void;
    response: boolean;
  };
  isDomainTrusted: {
    request: {
      domain: string;
    };
    response: boolean;
  };
}

export type MessageFromExtension<Op extends keyof ExtensionOperations> = {
  channel: "extension";
  operation: Op;
  payload: ExtensionOperations[Op]["request"];
};

export type MessageResponse = CoreApiResponse;

async function callBackground<Op extends keyof ExtensionOperations>(
  operation: Op,
  payload: ExtensionOperations[Op]["request"],
): Promise<ExtensionOperations[Op]["response"]> {
  const message: MessageFromExtension<Op> = {
    channel: "extension",
    operation,
    payload,
  };

  const response = await sendMessageToBackground(message);
  if (response.type === "error") {
    throw new Error(`Background operation "${operation}" failed`);
  }
  return response.result as any;
}

let nextMessageIndex = 0;
/**
 *
 * @param message
 * @returns
 */
async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
  message: MessageFromExtension<Op>,
): Promise<MessageResponse> {
  const messageWithId = { ...message, id: `ld:${nextMessageIndex++ % 1000}` };

  if (!chrome.runtime.id) {
    return Promise.reject(
      TalerError.fromDetail(TalerErrorCode.WALLET_CORE_NOT_AVAILABLE, {}),
    );
  }
  return new Promise<any>((resolve, reject) => {
    logger.debug(
      "send operation to the wallet background",
      message,
      chrome.runtime.id,
    );
    let timedout = false;
    const timerId = setTimeout(() => {
      timedout = true;
      reject(
        TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
          requestMethod: "wallet",
          requestUrl: message.operation,
          timeoutMs: 20 * 1000,
        }),
      );
    }, 20 * 1000); //five seconds
    try {
      chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
        if (timedout) {
          return false; //already rejected
        }
        clearTimeout(timerId);
        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError.message);
        } else {
          resolve(backgroundResponse);
        }
        // return true to keep the channel open
        return true;
      });
    } catch (e) {
      console.log(e);
    }
  });
}

let notificationPort: chrome.runtime.Port | undefined;
function listenToWalletBackground(listener: (m: any) => void): () => void {
  if (notificationPort === undefined) {
    notificationPort = chrome.runtime.connect({ name: "notifications" });
  }
  notificationPort.onMessage.addListener(listener);
  function removeListener(): void {
    if (notificationPort !== undefined) {
      notificationPort.onMessage.removeListener(listener);
    }
  }
  return removeListener;
}

const loaderSettings = {
  isAutoOpenEnabled: false,
  isDomainTrusted: false,
};

function start(
  onTalerMetaTagFound: (listener: (el: HTMLMetaElement) => void) => void,
  onHeadReady: (listener: (el: HTMLHeadElement) => void) => void,
) {
  // do not run everywhere, this is just expected to run on site
  // that are aware of taler
  if (shouldNotInject) return;

  const isAutoOpenEnabled_promise = callBackground(
    "isAutoOpenEnabled",
    undefined,
  ).then((result) => {
    loaderSettings.isAutoOpenEnabled = result;
    return result;
  });
  const isDomainTrusted_promise = callBackground("isDomainTrusted", {
    domain: window.location.origin,
  }).then((result) => {
    loaderSettings.isDomainTrusted = result;
    return result;
  });

  onTalerMetaTagFound(async (el) => {
    await isAutoOpenEnabled_promise;
    if (!loaderSettings.isAutoOpenEnabled) {
      return;
    }
    redirectToTalerActionHandler(el);
  });

  onHeadReady(async (el) => {
    const trusted = await isDomainTrusted_promise;
    injectTalerSupportScript(el, trusted);
  });

  listenToWalletBackground((e: MessageFromBackend) => {
    if (
      e.type === "web-extension" &&
      e.notification.type === "settings-change"
    ) {
      const settings = e.notification.currentValue;
      loaderSettings.isAutoOpenEnabled = settings.autoOpen;
    }
  });
}

function isCorrectMetaElement(el: HTMLMetaElement): boolean {
  const name = el.getAttribute("name");
  if (!name) return false;
  if (name !== "taler-uri") return false;
  const uri = el.getAttribute("content");
  if (!uri) return false;
  return true;
}

/**
 * Tries to find taler meta tag ASAP and report
 * @param notify
 * @returns
 */
function notifyWhenTalerUriIsFound(notify: (el: HTMLMetaElement) => void) {
  if (document.head) {
    const element = document.head.querySelector("meta[name=taler-uri]");
    if (!element) return;
    if (!(element instanceof HTMLMetaElement)) return;

    if (isCorrectMetaElement(element)) {
      notify(element);
    }
    return;
  }
  const obs = new MutationObserver(async function (mutations) {
    try {
      mutations.forEach((mut) => {
        if (mut.type === "childList") {
          mut.addedNodes.forEach((added) => {
            if (added instanceof HTMLMetaElement) {
              if (isCorrectMetaElement(added)) {
                notify(added);
                obs.disconnect();
              }
            }
          });
        }
      });
    } catch (e) {
      console.error(e);
    }
  });

  obs.observe(document, {
    childList: true,
    subtree: true,
    attributes: false,
  });
}

/**
 * Tries to find HEAD tag ASAP and report
 * @param notify
 * @returns
 */
function notifyWhenHeadIsFound(notify: (el: HTMLHeadElement) => void) {
  if (document.head) {
    notify(document.head);
    return;
  }
  const obs = new MutationObserver(async function (mutations) {
    try {
      mutations.forEach((mut) => {
        if (mut.type === "childList") {
          mut.addedNodes.forEach((added) => {
            if (added instanceof HTMLHeadElement) {
              notify(added);
              obs.disconnect();
            }
          });
        }
      });
    } catch (e) {
      console.error(e);
    }
  });

  obs.observe(document, {
    childList: true,
    subtree: true,
    attributes: false,
  });
}

start(notifyWhenTalerUriIsFound, notifyWhenHeadIsFound);
