--
-- This file is part of TALER
-- Copyright (C) 2023-2025 Taler Systems SA
--
-- 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.
--
-- 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
-- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>

DROP FUNCTION IF EXISTS exchange_do_withdraw;

CREATE FUNCTION exchange_do_withdraw(
  IN in_amount_with_fee taler_amount,
  IN in_reserve_pub BYTEA,
  IN in_reserve_sig BYTEA,
  IN in_now INT8,
  IN in_min_reserve_gc INT8,
  IN in_planchets_h BYTEA,
  IN in_maximum_age_committed INT2, -- in years ϵ [0,1..), possibly NULL
  IN in_noreveal_index INT2, -- possibly NULL (if not age-withdraw)
  IN in_selected_h BYTEA, -- possibly NULL (if not age-withdraw)
  IN in_denom_serials INT8[],
  IN in_denom_sigs BYTEA[],
  IN in_blinding_seed BYTEA, -- possibly NULL (if no CS denominations)
  IN in_cs_r_values BYTEA[], -- possibly NULL (if no CS denominations)
  IN in_cs_r_choices INT8, -- possibly NULL (if no CS denominations)
  OUT out_reserve_found BOOLEAN,
  OUT out_balance_ok BOOLEAN,
  OUT out_reserve_balance taler_amount,
  OUT out_age_ok BOOLEAN,
  OUT out_required_age INT2, -- in years ϵ [0,1..)
  OUT out_reserve_birthday INT4,
  OUT out_idempotent BOOLEAN,
  OUT out_noreveal_index INT2, -- possibly NULL (if not age-withdraw)
  OUT out_nonce_reuse BOOLEAN)
LANGUAGE plpgsql
AS $$
DECLARE
  my_reserve RECORD;
  my_difference RECORD;
  my_balance taler_amount;
  my_not_before DATE;
  my_earliest_date DATE;
BEGIN
-- Shards: reserves by reserve_pub (SELECT)
--         reserves by reserve_pub (UPDATE)

-- First, find the reserve
SELECT current_balance
      ,birthday
      ,gc_date
  INTO my_reserve
  FROM reserves
 WHERE reserve_pub=in_reserve_pub;
out_reserve_found = FOUND;

IF NOT out_reserve_found
THEN
  out_age_ok = FALSE;
  out_required_age = -1;
  out_idempotent = FALSE;
  out_noreveal_index = -1;
  out_reserve_balance.val = 0;
  out_reserve_balance.frac = 0;
  out_balance_ok = FALSE;
  out_nonce_reuse = FALSE;
  out_reserve_birthday = 0;
  RETURN;
END IF;

out_reserve_balance = my_reserve.current_balance;
out_reserve_birthday = my_reserve.birthday;

-- FIXME-performance: probably better to INSERT and on-conflict check for idempotency...

-- Next, check for idempotency of the withdraw
SELECT noreveal_index
  INTO out_noreveal_index
  FROM withdraw
 WHERE reserve_pub = in_reserve_pub
   AND planchets_h = in_planchets_h;
out_idempotent = FOUND;

IF out_idempotent
THEN
  -- out_idempotent set, out_noreveal_index possibly set, report.
  out_balance_ok = TRUE;
  out_age_ok = TRUE;
  out_required_age = -1;
  out_nonce_reuse = FALSE;
  RETURN;
END IF;

out_noreveal_index = -1;

-- Check age requirements
IF (my_reserve.birthday <> 0)
THEN
  my_not_before=date '1970-01-01' + my_reserve.birthday;
  my_earliest_date = current_date - make_interval(in_maximum_age_committed);
  --
  -- 1970-01-01 + birthday == my_not_before                 now
  --     |                     |                          |
  -- <.......not allowed......>[<.....allowed range......>]
  --     |                     |                          |
  -- ____*_____________________*_________*________________*  timeline
  --                                     |
  --                            my_earliest_date ==
  --                                now - maximum_age_committed*year
  --
  IF ( (in_maximum_age_committed IS NULL) OR
       (my_earliest_date < my_not_before) )
  THEN
    out_required_age = extract(year FROM age(current_date, my_not_before));
    out_age_ok = FALSE;
    out_balance_ok = TRUE;  -- not really
    out_nonce_reuse = FALSE;    -- not really
    RETURN;
  END IF;
END IF;

out_age_ok = TRUE;
out_required_age = 0;

-- Check reserve balance is sufficient.
SELECT *
  INTO my_difference
  FROM amount_left_minus_right(out_reserve_balance
                              ,in_amount_with_fee);

out_balance_ok = my_difference.ok;
IF NOT out_balance_ok
THEN
  out_nonce_reuse = FALSE;      -- not yet determined
  RETURN;
END IF;

my_balance = my_difference.diff;

-- Calculate new expiration dates.
in_min_reserve_gc=GREATEST(in_min_reserve_gc,my_reserve.gc_date);

-- Update reserve balance.
UPDATE reserves SET
  gc_date=in_min_reserve_gc
 ,current_balance=my_balance
WHERE
  reserve_pub=in_reserve_pub;

-- Ensure the uniqueness of the blinding_seed
IF in_blinding_seed IS NOT NULL
THEN
  INSERT INTO unique_withdraw_blinding_seed
    (blinding_seed)
  VALUES
    (in_blinding_seed)
  ON CONFLICT DO NOTHING;

  IF NOT FOUND
  THEN
    out_nonce_reuse = TRUE;
    RETURN;
  END IF;
END IF;

out_nonce_reuse = FALSE;

-- Write the data into the withdraw table
INSERT INTO withdraw
  (planchets_h
  ,execution_date
  ,max_age
  ,amount_with_fee
  ,reserve_pub
  ,reserve_sig
  ,noreveal_index
  ,denom_serials
  ,selected_h
  ,blinding_seed
  ,cs_r_values
  ,cs_r_choices
  ,denom_sigs)
VALUES
  (in_planchets_h
  ,in_now
  ,in_maximum_age_committed
  ,in_amount_with_fee
  ,in_reserve_pub
  ,in_reserve_sig
  ,in_noreveal_index
  ,in_denom_serials
  ,in_selected_h
  ,in_blinding_seed
  ,in_cs_r_values
  ,in_cs_r_choices
  ,in_denom_sigs)
ON CONFLICT DO NOTHING;

IF NOT FOUND
THEN
  RAISE EXCEPTION 'Conflict on insert into withdraw despite idempotency check for reserve_pub(%) and planchets_h(%)!',
    in_reserve_pub,
    in_planchets_h;
END IF;

END $$;

COMMENT ON FUNCTION exchange_do_withdraw(
  taler_amount,
  BYTEA,
  BYTEA,
  INT8,
  INT8,
  BYTEA,
  INT2,
  INT2,
  BYTEA,
  INT8[],
  BYTEA[],
  BYTEA,
  BYTEA[],
  INT8)
  IS 'Checks whether the reserve has sufficient balance for an withdraw operation (or the request is repeated and was previously approved) and that age requirements are met. If so updates the database with the result. Includes storing the hashes of all blinded planchets, (separately) the hashes of the chosen planchets and denomination signatures, or signaling idempotency (and previous noreveal_index) or nonce reuse';
