/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";

const { FIELD_STATES } = FormAutofillUtils;

class AutofillTelemetryBase {
  SUPPORTED_FIELDS = {};

  EVENT_CATEGORY = null;
  EVENT_OBJECT_FORM_INTERACTION = null;

  #initFormEventExtra(value) {
    let extra = {};
    for (const field of Object.values(this.SUPPORTED_FIELDS)) {
      extra[field] = value;
    }
    return extra;
  }

  #setFormEventExtra(extra, key, value) {
    if (!this.SUPPORTED_FIELDS[key]) {
      return;
    }

    extra[this.SUPPORTED_FIELDS[key]] = value;
  }

  recordFormDetected(flowId, fieldDetails) {
    let extra = this.#initFormEventExtra("false");

    let identified = new Set();
    fieldDetails.forEach(detail => {
      identified.add(detail.fieldName);

      if (detail.reason == "autocomplete") {
        this.#setFormEventExtra(extra, detail.fieldName, "true");
      } else {
        // confidence exists only when a field is identified by fathom.
        let confidence =
          detail.confidence > 0 ? Math.floor(100 * detail.confidence) / 100 : 0;

        this.#setFormEventExtra(
          extra,
          detail.fieldName,
          confidence ? confidence.toString() : "0"
        );
      }
    });

    this.recordFormEvent("detected", flowId, extra);
    try {
      this.recordIframeLayoutDetection(flowId, fieldDetails);
    } catch {}
  }

  recordPopupShown(flowId, fieldDetails) {
    const extra = { field_name: fieldDetails[0].fieldName };
    this.recordFormEvent("popup_shown", flowId, extra);
  }

  setUpFormFilledExtra(fieldDetails, data) {
    // Calculate values for telemetry
    const extra = this.#initFormEventExtra("unavailable");

    for (const fieldDetail of fieldDetails) {
      // It is possible that we don't autofill a field because it is cross-origin.
      // When that happens, the data will not include that element.
      let { filledState, filledValue, isFilledOnFieldsUpdate } =
        data.get(fieldDetail.elementId) ?? {};
      switch (filledState) {
        case FIELD_STATES.AUTO_FILLED:
          filledState = isFilledOnFieldsUpdate
            ? "filled_on_fields_update"
            : "filled";
          break;
        case FIELD_STATES.NORMAL:
        default:
          filledState =
            fieldDetail.localName == "select" || filledValue?.length
              ? "user_filled"
              : "not_filled";
          break;
      }
      this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
    }
    return extra;
  }

  recordFormFilled(flowId, fieldDetails, data) {
    const extra = this.setUpFormFilledExtra(fieldDetails, data);
    this.recordFormEvent("filled", flowId, extra);
  }

  recordFormFilledOnFieldsUpdate(flowId, fieldDetails, data) {
    const extra = this.setUpFormFilledExtra(fieldDetails, data);
    this.recordFormEvent("filled_on_fields_update", flowId, extra);
  }

  recordFilledModified(flowId, fieldDetails) {
    const extra = { field_name: fieldDetails[0].fieldName };
    this.recordFormEvent("filled_modified", flowId, extra);
  }

  recordFormSubmitted(flowId, fieldDetails, data) {
    const extra = this.#initFormEventExtra("unavailable");

    for (const fieldDetail of fieldDetails) {
      let { filledState, filledValue } = data.get(fieldDetail.elementId) ?? {};
      switch (filledState) {
        case FIELD_STATES.AUTO_FILLED:
          filledState = "autofilled";
          break;
        case FIELD_STATES.NORMAL:
        default:
          filledState =
            fieldDetail.localName == "select" || filledValue?.length
              ? "user_filled"
              : "not_filled";
          break;
      }
      this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
    }

    this.recordFormEvent("submitted", flowId, extra);
  }

  recordFormCleared(flowId, fieldDetails) {
    const extra = { field_name: fieldDetails[0].fieldName };

    // Note that when a form is cleared, we also record `filled_modified` events
    // for all the fields that have been cleared.
    this.recordFormEvent("cleared", flowId, extra);
  }

  recordFormEvent(_method, _flowId, _extra) {
    throw new Error("Not implemented.");
  }

  recordFormInteractionEvent(method, flowId, fieldDetails, data) {
    if (!this.EVENT_OBJECT_FORM_INTERACTION) {
      return undefined;
    }
    switch (method) {
      case "detected":
        return this.recordFormDetected(flowId, fieldDetails);
      case "popup_shown":
        return this.recordPopupShown(flowId, fieldDetails);
      case "filled":
        return this.recordFormFilled(flowId, fieldDetails, data);
      case "filled_on_fields_update":
        return this.recordFormFilledOnFieldsUpdate(flowId, fieldDetails, data);
      case "filled_modified":
        return this.recordFilledModified(flowId, fieldDetails);
      case "submitted":
        return this.recordFormSubmitted(flowId, fieldDetails, data);
      case "cleared":
        return this.recordFormCleared(flowId, fieldDetails);
    }
    return undefined;
  }

  recordDoorhangerEvent(method, object, flowId) {
    const eventName = `${method}_${object}`.replace(/(_[a-z])/g, c =>
      c[1].toUpperCase()
    );
    Glean[this.EVENT_CATEGORY][eventName]?.record({ value: flowId });
  }

  recordManageEvent(method) {
    const eventName =
      method.replace(/(_[a-z])/g, c => c[1].toUpperCase()) + "Manage";
    Glean[this.EVENT_CATEGORY][eventName]?.record();
  }

  recordAutofillProfileCount(_count) {
    throw new Error("Not implemented.");
  }

  recordIframeLayoutDetection(flowId, fieldDetails) {
    const fieldsInMainFrame = [];
    const fieldsInIframe = [];
    const fieldsInSandboxedIframe = [];
    const fieldsInCrossOrignIframe = [];

    const iframes = new Set();
    for (const fieldDetail of fieldDetails) {
      const bc = BrowsingContext.get(fieldDetail.browsingContextId);
      if (bc.top == bc) {
        fieldsInMainFrame.push(fieldDetail);
        continue;
      }

      iframes.add(bc);
      fieldsInIframe.push(fieldDetail);
      if (bc.sandboxFlags != 0) {
        fieldsInSandboxedIframe.push(fieldDetail);
      }

      if (!FormAutofillUtils.isBCSameOriginWithTop(bc)) {
        fieldsInCrossOrignIframe.push(fieldDetail);
      }
    }

    const extra = {
      category: this.EVENT_CATEGORY,
      flow_id: flowId,
      iframe_count: iframes.size,
      main_frame: fieldsInMainFrame.map(f => f.fieldName).toString(),
      iframe: fieldsInIframe.map(f => f.fieldName).toString(),
      cross_origin: fieldsInCrossOrignIframe.map(f => f.fieldName).toString(),
      sandboxed: fieldsInSandboxedIframe.map(f => f.fieldName).toString(),
    };

    Glean.formautofill.iframeLayoutDetection.record(extra);
  }
}

export class AddressTelemetry extends AutofillTelemetryBase {
  EVENT_CATEGORY = "address";
  EVENT_OBJECT_FORM_INTERACTION = "AddressForm";
  EVENT_OBJECT_FORM_INTERACTION_EXT = "AddressFormExt";

  // Fields that are recorded in `address_form` and `address_form_ext` telemetry
  SUPPORTED_FIELDS = {
    "street-address": "street_address",
    "address-line1": "address_line1",
    "address-line2": "address_line2",
    "address-line3": "address_line3",
    "address-level1": "address_level1",
    "address-level2": "address_level2",
    "postal-code": "postal_code",
    country: "country",
    name: "name",
    "given-name": "given_name",
    "additional-name": "additional_name",
    "family-name": "family_name",
    email: "email",
    organization: "organization",
    tel: "tel",
  };

  // Fields that are recorded in `address_form` event telemetry extra_keys
  static SUPPORTED_FIELDS_IN_FORM = [
    "street_address",
    "address_line1",
    "address_line2",
    "address_line3",
    "address_level2",
    "address_level1",
    "postal_code",
    "country",
  ];

  // Fields that are recorded in `address_form_ext` event telemetry extra_keys
  static SUPPORTED_FIELDS_IN_FORM_EXT = [
    "name",
    "given_name",
    "additional_name",
    "family_name",
    "email",
    "organization",
    "tel",
  ];

  recordFormEvent(method, flowId, extra) {
    let extExtra = {};
    if (["detected", "filled", "submitted"].includes(method)) {
      for (const [key, value] of Object.entries(extra)) {
        if (AddressTelemetry.SUPPORTED_FIELDS_IN_FORM_EXT.includes(key)) {
          extExtra[key] = value;
          delete extra[key];
        }
      }
    }

    const eventMethod = method.replace(/(_[a-z])/g, c => c[1].toUpperCase());
    Glean.address[eventMethod + this.EVENT_OBJECT_FORM_INTERACTION]?.record({
      value: flowId,
      ...extra,
    });

    if (Object.keys(extExtra).length) {
      Glean.address[
        eventMethod + this.EVENT_OBJECT_FORM_INTERACTION_EXT
      ]?.record({ value: flowId, ...extExtra });
    }
  }

  recordAutofillProfileCount(count) {
    Glean.formautofillAddresses.autofillProfilesCount.set(count);
  }
}

class CreditCardTelemetry extends AutofillTelemetryBase {
  EVENT_CATEGORY = "creditcard";
  EVENT_OBJECT_FORM_INTERACTION = "CcFormV2";

  // Mapping of field name used in formautofill code to the field name
  // used in the telemetry.
  SUPPORTED_FIELDS = {
    "cc-name": "cc_name",
    "cc-number": "cc_number",
    "cc-type": "cc_type",
    "cc-exp": "cc_exp",
    "cc-exp-month": "cc_exp_month",
    "cc-exp-year": "cc_exp_year",
  };
  recordFormEvent(method, flowId, aExtra) {
    // Don't modify the passed-in aExtra as it's reused.
    const extra = Object.assign({ value: flowId }, aExtra);
    const eventMethod = method.replace(/(_[a-z])/g, c => c[1].toUpperCase());
    Glean.creditcard[eventMethod + this.EVENT_OBJECT_FORM_INTERACTION]?.record(
      extra
    );
  }

  recordFormDetected(flowId, fieldDetails) {
    super.recordFormDetected(flowId, fieldDetails);
    this.recordCcNumberFieldsCount(fieldDetails);
  }

  /**
   * Collect the amount of consecutive cc number fields to help decide
   * whether to support filling other field counts besides 1 and 4 fields
   */
  recordCcNumberFieldsCount(fieldDetails) {
    const recordCount = count => {
      const label = "cc_number_fields_" + (count > 4 ? "other" : count);
      Glean.creditcard.detectedCcNumberFieldsCount[label].add(1);
    };

    let consecutiveCcNumberCount = 0;
    for (const { fieldName, reason } of fieldDetails) {
      if (fieldName == "cc-number" && reason == "autocomplete") {
        consecutiveCcNumberCount++;
      } else if (consecutiveCcNumberCount) {
        recordCount(consecutiveCcNumberCount);
        consecutiveCcNumberCount = 0;
      }
    }

    if (consecutiveCcNumberCount) {
      recordCount(consecutiveCcNumberCount);
    }
  }

  recordAutofillProfileCount(count) {
    Glean.formautofillCreditcards.autofillProfilesCount.set(count);
  }
}

export class AutofillTelemetry {
  static #creditCardTelemetry = new CreditCardTelemetry();
  static #addressTelemetry = new AddressTelemetry();

  // const for `type` parameter used in the utility functions
  static ADDRESS = "address";
  static CREDIT_CARD = "creditcard";

  static #getTelemetryByFieldDetail(fieldDetail) {
    return FormAutofillUtils.isAddressField(fieldDetail.fieldName)
      ? this.#addressTelemetry
      : this.#creditCardTelemetry;
  }

  static #getTelemetryByType(type) {
    return type == AutofillTelemetry.CREDIT_CARD
      ? this.#creditCardTelemetry
      : this.#addressTelemetry;
  }

  /**
   * Utility functions for `doorhanger` event (defined in Events.yaml)
   *
   * Category: address or creditcard
   * Event name: doorhanger
   */
  static recordDoorhangerShown(type, object, flowId) {
    const telemetry = this.#getTelemetryByType(type);
    telemetry.recordDoorhangerEvent("show", object, flowId);
  }

  static recordDoorhangerClicked(type, method, object, flowId) {
    const telemetry = this.#getTelemetryByType(type);

    // We don't have `create` method in telemetry, we treat `create` as `save`
    switch (method) {
      case "create":
        method = "save";
        break;
      case "open-pref":
        method = "pref";
        break;
      case "learn-more":
        method = "learn_more";
        break;
    }

    telemetry.recordDoorhangerEvent(method, object, flowId);
  }

  /**
   * Utility functions for form event (defined in Events.yaml)
   *
   * Category: address or creditcard
   * Event name: cc_form_v2, or address_form
   */

  static recordFormInteractionEvent(method, flowId, fieldDetails, data) {
    const telemetry = this.#getTelemetryByFieldDetail(fieldDetails[0]);
    telemetry.recordFormInteractionEvent(method, flowId, fieldDetails, data);
  }

  static recordManageEvent(type, method) {
    const telemetry = this.#getTelemetryByType(type);
    telemetry.recordManageEvent(method);
  }

  static recordAutofillProfileCount(type, count) {
    const telemetry = this.#getTelemetryByType(type);
    telemetry.recordAutofillProfileCount(count);
  }

  static recordFormSubmissionHeuristicCount(label) {
    Glean.formautofill.formSubmissionHeuristic[label].add(1);
  }
}
