import {
  scanBranch,
  GAP_LIMIT,
} from "./branch.js";
import { mapConcurrent } from "../concurrent.js";
import {
  getOutputDescriptorBranchIds,
  isOutputDescriptor,
} from "../derive/index.js";
import { isUsedAddress } from "./activity.js";

const BRANCH_SCAN_CONCURRENCY = 2;

const keyBranches = /** @type {const} */ ([
  { id: "receive", label: "Receive", path: [0] },
  { id: "change", label: "Change", path: [1] },
  { id: "direct", label: "Direct", path: [] },
]);

const descriptorBranches = /** @type {const} */ ([
  { id: "receive", label: "Receive", path: [] },
  { id: "change", label: "Change", path: [] },
]);

/**
 * @typedef {(typeof keyBranches[number] | typeof descriptorBranches[number])} WalletBranch
 * @typedef {WalletBranch["id"]} WalletBranchId
 */

/**
 * @typedef {import("../derive/address.js").AddressScript} AddressScript
 * @typedef {import("../lookup/index.js").AddressClient} AddressClient
 * @typedef {import("./branch.js").WalletAddress} WalletAddress
 */

/**
 * @typedef {WalletAddress & {
 *   branchId: WalletBranchId,
 *   branchLabel: string,
 * }} ScannedAddress
 */

/**
 * @typedef {Object} ScanProgress
 * @property {WalletBranchId} branchId
 * @property {string} branchLabel
 * @property {number} scannedCount
 * @property {number} unusedInRow
 */

/**
 * @typedef {Object} ScanBranchesOptions
 * @property {AddressScript} script
 * @property {(progress: ScanProgress) => void} [onProgress]
 */

/**
 * @typedef {Object} ScanBranchesResult
 * @property {ScannedAddress[]} addresses
 * @property {ScannedAddress | undefined} receiveAddress
 * @property {number} gapLimit
 * @property {boolean} maxed
 */

/**
 * @param {ScannedAddress} a
 * @param {ScannedAddress} b
 */
function compareWalletAddresses(a, b) {
  if (a.typeIndex === undefined) return 1;
  if (b.typeIndex === undefined) return -1;

  return b.typeIndex - a.typeIndex || a.index - b.index;
}

/**
 * @param {WalletAddress} address
 * @param {WalletBranch} branch
 * @returns {ScannedAddress}
 */
function addBranch(address, branch) {
  return {
    ...address,
    branchId: branch.id,
    branchLabel: branch.label,
  };
}

/**
 * @param {string} source
 * @returns {WalletBranch[]}
 */
function getWalletBranches(source) {
  if (!isOutputDescriptor(source)) return [...keyBranches];

  const branchIds = new Set(getOutputDescriptorBranchIds(source));
  const branches = descriptorBranches.filter((branch) => {
    return branchIds.has(branch.id);
  });

  return branches.length ? branches : [descriptorBranches[0]];
}

/**
 * @param {AddressClient} client
 * @param {string} source
 * @param {ScanBranchesOptions} options
 * @returns {Promise<ScanBranchesResult>}
 */
export async function scanBranches(client, source, options) {
  const addresses = /** @type {ScannedAddress[]} */ ([]);
  const branches = getWalletBranches(source);
  const receiveBranch =
    branches.find((branch) => branch.id === "receive") ?? branches[0];
  /** @type {ScannedAddress | undefined} */
  let receiveAddress;
  const scans = await mapConcurrent(
    branches,
    BRANCH_SCAN_CONCURRENCY,
    async (branch) => {
      const scan = await scanBranch(client, source, {
        script: options.script,
        path: branch.path,
        branchId: branch.id,
        onProgress(progress) {
          options.onProgress?.({
            branchId: branch.id,
            branchLabel: branch.label,
            scannedCount: progress.scannedCount,
            unusedInRow: progress.unusedInRow,
          });
        },
      });

      return { branch, scan };
    },
  );

  for (const { branch, scan } of scans) {
    for (const address of scan.addresses) {
      const branchedAddress = addBranch(address, branch);

      if (!isUsedAddress(address)) {
        if (!receiveAddress && branch.id === receiveBranch.id) {
          receiveAddress = branchedAddress;
        }
        continue;
      }

      addresses.push(branchedAddress);
    }
  }

  return {
    addresses: addresses.sort(compareWalletAddresses),
    receiveAddress,
    gapLimit: GAP_LIMIT,
    maxed: scans.some(({ scan }) => scan.maxed),
  };
}
