--
-- This file is part of TALER
-- Copyright (C) 2024 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 challenger_do_challenge_set_address_and_pin;
CREATE FUNCTION challenger_do_challenge_set_address_and_pin (
  IN in_nonce BYTEA,
  IN in_address TEXT,
  -- Newest last_tx_time for which a (re)transmission is still due, that is
  -- 'now - retransmission_frequency'.  Computed by the caller because
  -- last_tx_time is only known here; see do_challenge_address.c.
  IN in_retransmit_cutoff INT8,
  IN in_now INT8,
  IN in_tan INT4,
  OUT out_not_found BOOLEAN,
  OUT out_last_tx_time INT8,
  OUT out_last_pin INT4,
  OUT out_state TEXT,
  OUT out_pin_transmit BOOLEAN,
  OUT out_auth_attempts_left INT4,
  -- How many PIN transmissions are left *after* this call?  Note that
  -- this is a different budget from out_auth_attempts_left, which counts
  -- the guesses the user has on the current PIN.
  OUT out_pin_transmissions_left INT4,
  OUT out_client_redirect_uri TEXT,
  OUT out_address_refused BOOLEAN,
  OUT out_solved BOOLEAN)
LANGUAGE plpgsql
AS $$
DECLARE
  my_status RECORD;
  my_do_update BOOL;
  my_address_differs BOOL;
BEGIN

my_do_update = FALSE;

SELECT address
      ,address_attempts_left
      ,pin_transmissions_left
      ,last_tx_time
      ,client_redirect_uri
      ,last_pin
      ,pending_pin
      ,auth_attempts_left
      ,client_state
  INTO my_status
  FROM validations
 WHERE nonce=in_nonce
   AND expiration_time > in_now
   FOR UPDATE;

IF NOT FOUND
THEN
  out_not_found=TRUE;
  out_last_tx_time=0;
  out_last_pin=NULL;
  out_pin_transmit=FALSE;
  out_auth_attempts_left=0;
  out_pin_transmissions_left=0;
  out_client_redirect_uri=NULL;
  out_address_refused=TRUE;
  out_solved=FALSE;
  out_state=NULL;
  RETURN;
END IF;
out_not_found=FALSE;
out_last_tx_time=my_status.last_tx_time;
out_last_pin=my_status.last_pin;
out_pin_transmit=FALSE;
out_auth_attempts_left=my_status.auth_attempts_left;
out_pin_transmissions_left=my_status.pin_transmissions_left;
out_state=my_status.client_state;
out_client_redirect_uri=my_status.client_redirect_uri;

IF ( 0 > my_status.auth_attempts_left ) -- this challenge is solved
THEN
  out_address_refused=TRUE;
  out_solved=TRUE;
  out_auth_attempts_left=0;
  RETURN;
END IF;
out_solved=FALSE;

-- Two addresses are the same address if they are the same JSON *value*.
-- Comparing the raw text instead would make a purely cosmetic difference --
-- a different field order, redundant whitespace -- count as a different
-- address, with two consequences.  Firstly, the daemon itself re-appends
-- 'read_only' to the address as the *last* field (see
-- challenger-httpd_challenge.c), so an unchanged address can come back here
-- with its fields in a different order and silently cost an honest user one
-- of their address attempts.  Secondly, the budget is deliberately
-- 'address_attempts_left' addresses times 'pin_transmissions_left' PINs
-- each; if cosmetic variants counted as distinct addresses, all of those
-- messages could be aimed at a single recipient.  Comparing as JSONB also
-- makes this agree with the C layer, which compares addresses with
-- json_equal() in addr_equal().
my_address_differs = ( (my_status.address IS NOT NULL) AND
                       (in_address::JSONB IS DISTINCT FROM
                        my_status.address::JSONB) );

IF ( (0 = my_status.address_attempts_left) AND
     my_address_differs )
THEN
  out_address_refused=TRUE;
  out_last_pin=NULL;
  RETURN;
END IF;
out_address_refused=FALSE;

IF ( my_address_differs OR
     (my_status.address IS NULL) )
THEN
  -- We are changing the address, update counters.  Refilling
  -- 'pin_transmissions_left' and clearing the retransmission cooldown is
  -- intentional: the user gets a fixed number of addresses and, for each
  -- address, a fixed number of PIN transmissions, so that a user who
  -- mistyped their address does not have to wait out the cooldown of a
  -- message that went to somebody else.
  my_status.address_attempts_left
    = GREATEST(0,my_status.address_attempts_left - 1);
  -- Store the canonical (JSONB) rendering, so that what is on file does not
  -- depend on the field order the client happened to use.
  my_status.address = in_address::JSONB::TEXT;
  my_status.pin_transmissions_left = 3;
  my_status.last_tx_time = 0;
  -- The PIN generated below is now merely 'pending' until the helper
  -- confirms the transmission, so -- unlike before this patch -- this call
  -- no longer necessarily overwrites 'last_pin'.  The PIN on file went to
  -- the *previous* address, so it must be dropped here: if the helper then
  -- fails, the user must be left without a usable PIN rather than with one
  -- that attests an address it was never sent to.
  my_status.last_pin = NULL;
  my_status.pending_pin = NULL;
  my_status.auth_attempts_left = 0;
  out_last_pin = NULL;
  out_auth_attempts_left = 0;
  my_do_update=TRUE;
END IF;

IF ( (my_status.pin_transmissions_left > 0) AND
     (my_status.last_tx_time <= in_retransmit_cutoff) )
THEN
  -- Enough time has passed since the last transmission, so we are changing
  -- the PIN, update counters.  The new PIN is only stored as 'pending_pin':
  -- it is promoted to 'last_pin' (and 'auth_attempts_left' reset) by
  -- CHALLENGERDB_do_challenge_address_confirm_pin() once the AUTH_COMMAND
  -- helper confirmed the transmission.  Until then the PIN the user may
  -- already hold from an earlier transmission stays valid.
  my_status.pin_transmissions_left = my_status.pin_transmissions_left - 1;
  my_status.pending_pin = in_tan;
  my_status.last_tx_time = in_now;
  out_pin_transmissions_left = my_status.pin_transmissions_left;
  out_auth_attempts_left = 3;
  out_pin_transmit=TRUE;
  out_last_pin = in_tan;
  out_last_tx_time = in_now;
  my_do_update=TRUE;
END IF;

IF my_do_update
THEN
  UPDATE validations SET
    address=my_status.address
   ,address_attempts_left=my_status.address_attempts_left
   ,pin_transmissions_left=my_status.pin_transmissions_left
   ,last_tx_time=my_status.last_tx_time
   ,last_pin=my_status.last_pin
   ,pending_pin=my_status.pending_pin
   ,auth_attempts_left=my_status.auth_attempts_left
  WHERE nonce=in_nonce;
END IF;

RETURN;

END $$;
