/*
 This file is part of GNU Taler
 (C) 2019-2025 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/>
 */

/**
 * Token and token family management requests.
 *
 * @author Iván Ávalos
 */
import {
  DiscountListDetail,
  EmptyObject,
  ListDiscountsResponse,
  ListSubscriptionsResponse,
  Logger,
  MerchantContractTokenKind,
  SubscriptionListDetail,
} from "@gnu-taler/taler-util";
import { WalletExecutionContext } from "./index.js";
import { TokenRecord } from "./db.js";
import {
    isTokenInUse,
  isTokenValid,
} from "./tokenSelection.js";

const logger = new Logger("tokenFamilies.ts");

// FIXME: unit test for discount grouping
function groupDiscounts(tokens: TokenRecord[]): DiscountListDetail[] {
  const groupedIdx: number[] = [];
  const items: DiscountListDetail[] = [];
  tokens = tokens
    .filter(t => t.tokenFamilyHash)
    .sort((a, b) => a.validBefore - b.validBefore);

  // compare tokens against each other,
  // except the ones already in a group.
  for (let a = 0; a < tokens.length; a++) {
    if (groupedIdx.includes(a)) continue;
    let tokenA = tokens[a];
    const item: DiscountListDetail = {
      tokenFamilyHash: tokenA.tokenFamilyHash!,
      tokenIssuePubHash: tokenA.tokenIssuePubHash,
      merchantBaseUrl: tokenA.merchantBaseUrl,
      name: tokenA.name,
      description: tokenA.description,
      descriptionI18n: tokenA.descriptionI18n,
      validityStart: tokenA.tokenIssuePub.signature_validity_start,
      validityEnd: tokenA.tokenIssuePub.signature_validity_end,
      tokensAvailable: 1,
    };

    for (let b = 0; b < tokens.length; b++) {
      if (b === a) continue;
      if (groupedIdx.includes(b)) continue;
      let tokenB = tokens[b];
      if (tokenA.tokenFamilyHash === tokenB.tokenFamilyHash) {
        item.tokensAvailable += 1;
        groupedIdx.push(b);
      }
    }

    groupedIdx.push(a);
    items.push(item);
  }

  return items;
}

// FIXME: unit test for subscription grouping
function groupSubscriptions(tokens: TokenRecord[]): SubscriptionListDetail[] {
  const groupedIdx: number[] = [];
  const items: SubscriptionListDetail[] = [];
  tokens = tokens
    .filter(t => t.tokenFamilyHash)
    .sort((a, b) => a.validBefore - b.validBefore);

  // compare tokens against each other,
  // except the ones already in a group.
  for (let a = 0; a < tokens.length; a++) {
    if (groupedIdx.includes(a)) continue;
    let tokenA = tokens[a];
    const item: SubscriptionListDetail = {
      tokenFamilyHash: tokenA.tokenFamilyHash!,
      tokenIssuePubHash: tokenA.tokenIssuePubHash,
      merchantBaseUrl: tokenA.merchantBaseUrl,
      name: tokenA.name,
      description: tokenA.description,
      descriptionI18n: tokenA.descriptionI18n,
      validityStart: tokenA.tokenIssuePub.signature_validity_start,
      validityEnd: tokenA.tokenIssuePub.signature_validity_end,
    };

    for (let b = 0; b < tokens.length; b++) {
      if (b === a) continue;
      if (groupedIdx.includes(b)) continue;
      let tokenB = tokens[b];
      if (tokenA.tokenFamilyHash === tokenB.tokenFamilyHash) {
        groupedIdx.push(b);
      }
    }

    groupedIdx.push(a);
    items.push(item);
  }

  return items;
}

export async function listDiscounts(
  wex: WalletExecutionContext,
  tokenIssuePubHash?: string,
  merchantBaseUrl?: string,
): Promise<ListDiscountsResponse> {
  const tokens: TokenRecord[] = await wex.db.runReadOnlyTx({
    storeNames: ["tokens"],
  }, async (tx) => {
    return (await tx.tokens.getAll())
      .filter(t => isTokenValid(t))
      .filter(t => t.kind === MerchantContractTokenKind.Discount)
      .filter(t => !tokenIssuePubHash || t.tokenIssuePubHash === tokenIssuePubHash)
      .filter(t => !merchantBaseUrl || t.merchantBaseUrl === merchantBaseUrl)
  });

  if (tokens.length === 0) {
    return { discounts: [] };
  }

  return {
    discounts: groupDiscounts(tokens),
  };
}

export async function listSubscriptions(
  wex: WalletExecutionContext,
  tokenIssuePubHash?: string,
  merchantBaseUrl?: string,
): Promise<ListSubscriptionsResponse> {
  const tokens: TokenRecord[] = await wex.db.runReadOnlyTx({
    storeNames: ["tokens"],
  }, async (tx) => {
    return (await tx.tokens.getAll())
      .filter(t => isTokenValid(t))
      .filter(t => t.kind === MerchantContractTokenKind.Subscription)
      .filter(t => !tokenIssuePubHash || t.tokenIssuePubHash === tokenIssuePubHash)
      .filter(t => !merchantBaseUrl || t.merchantBaseUrl === merchantBaseUrl);
  });

  if (tokens.length === 0) {
    return { subscriptions: [] };
  }

  return {
    subscriptions: groupSubscriptions(tokens),
  };
}

export async function deleteDiscount(
  wex: WalletExecutionContext,
  tokenFamilyHash: string,
): Promise<EmptyObject> {
  await wex.db.runReadWriteTx({
    storeNames: ["tokens"],
  }, async (tx) => {
    const tokens = (await tx.tokens.getAll())
      .filter(t => t.kind === MerchantContractTokenKind.Discount)
      .filter(t => t.tokenFamilyHash === tokenFamilyHash);

    let inUse: boolean = false;
    for (const token of tokens) {
      if (isTokenInUse(token)) {
        inUse = true;
        return;
      }
    }

    // FIXME: proper GANA error
    if (inUse) {
      throw Error("One or more tokens in this family are in use");
    }

    for (const token of tokens) {
      if (logger.shouldLogTrace()) {
        logger.trace(
          `deleting token in ${token.tokenIssuePubHash} token family`
        );
      }
      await tx.tokens.delete(token.tokenUsePub);
    }
  });

  return {};
}

export async function deleteSubscription(
  wex: WalletExecutionContext,
  tokenFamilyHash: string,
): Promise<EmptyObject> {
  await wex.db.runReadWriteTx({
    storeNames: ["tokens"],
  }, async (tx) => {
    const tokens = (await tx.tokens.getAll())
      .filter(t => t.kind === MerchantContractTokenKind.Subscription)
      .filter(t => t.tokenFamilyHash === tokenFamilyHash)
      .sort((a, b) => a.validBefore - b.validBefore);

    let inUse: boolean = false;
    for (const token of tokens) {
      if (isTokenInUse(token)) {
        inUse = true;
        return;
      }
    }

    // FIXME: proper GANA error
    if (inUse) {
      throw Error("One or more tokens in this family are in use");
    }
  });

  return {};
}
