--
-- 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/>

BEGIN;
SET search_path TO public;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

SET search_path TO libeufin_nexus;

-- Remove all existing functions
DO
$do$
DECLARE
  _sql text;
BEGIN
  SELECT INTO _sql
        string_agg(format('DROP %s %s CASCADE;'
                        , CASE prokind
                            WHEN 'f' THEN 'FUNCTION'
                            WHEN 'p' THEN 'PROCEDURE'
                          END
                        , oid::regprocedure)
                  , E'\n')
  FROM   pg_proc
  WHERE  pronamespace = 'libeufin_nexus'::regnamespace;

  IF _sql IS NOT NULL THEN
    EXECUTE _sql;
  END IF;
END
$do$;

CREATE FUNCTION amount_normalize(
    IN amount taler_amount
  ,OUT normalized taler_amount
)
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
  normalized.val = amount.val + amount.frac / 100000000;
  IF (normalized.val > 1::INT8<<52) THEN
    RAISE EXCEPTION 'amount value overflowed';
  END IF;
  normalized.frac = amount.frac % 100000000;

END $$;
COMMENT ON FUNCTION amount_normalize
  IS 'Returns the normalized amount by adding to the .val the value of (.frac / 100000000) and removing the modulus 100000000 from .frac.'
      'It raises an exception when the resulting .val is larger than 2^52';

CREATE FUNCTION amount_add(
   IN l taler_amount
  ,IN r taler_amount
  ,OUT sum taler_amount
)
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
  sum = (l.val + r.val, l.frac + r.frac);
  SELECT normalized.val, normalized.frac INTO sum.val, sum.frac FROM amount_normalize(sum) as normalized;
END $$;
COMMENT ON FUNCTION amount_add
  IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52';

CREATE FUNCTION register_outgoing(
  IN in_amount taler_amount
  ,IN in_subject TEXT
  ,IN in_execution_time INT8
  ,IN in_credit_payto TEXT
  ,IN in_end_to_end_id TEXT
  ,IN in_msg_id TEXT
  ,IN in_acct_svcr_ref TEXT
  ,IN in_wtid BYTEA
  ,IN in_exchange_url TEXT
  ,OUT out_tx_id INT8
  ,OUT out_found BOOLEAN
  ,OUT out_initiated BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
init_id INT8;
local_amount taler_amount;
local_subject TEXT;
local_credit_payto TEXT;
local_wtid BYTEA;
local_exchange_base_url TEXT;
local_end_to_end_id TEXT;
BEGIN
-- Check if already registered
SELECT outgoing_transaction_id, subject, credit_payto, (amount).val, (amount).frac,
    wtid, exchange_base_url
  INTO out_tx_id, local_subject, local_credit_payto, local_amount.val, local_amount.frac, 
    local_wtid, local_exchange_base_url
  FROM outgoing_transactions LEFT JOIN talerable_outgoing_transactions USING (outgoing_transaction_id)
  WHERE end_to_end_id = in_end_to_end_id OR acct_svcr_ref = in_acct_svcr_ref;
out_found=FOUND;
IF out_found THEN
  -- Check metadata
  -- TODO take subject if missing and more detailed credit payto
  IF in_subject IS NOT NULL AND local_subject != in_subject THEN
    RAISE NOTICE 'outgoing tx %: stored subject is ''%'' got ''%''', in_end_to_end_id, local_subject, in_subject;
  END IF;
  IF in_credit_payto IS NOT NULL AND local_credit_payto != in_credit_payto THEN
    RAISE NOTICE 'outgoing tx %: stored subject credit payto is % got %', in_end_to_end_id, local_credit_payto, in_credit_payto;
  END IF;
  IF local_amount IS DISTINCT FROM in_amount THEN
    RAISE NOTICE 'outgoing tx %: stored amount is % got %', in_end_to_end_id, local_amount, in_amount;
  END IF;
  IF local_wtid IS DISTINCT FROM in_wtid THEN
    RAISE NOTICE 'outgoing tx %: stored wtid is % got %', in_end_to_end_id, local_wtid, in_wtid;
  END IF;
  IF local_exchange_base_url IS DISTINCT FROM in_exchange_url THEN
    RAISE NOTICE 'outgoing tx %: stored exchange base url is % got %', in_end_to_end_id, local_exchange_base_url, in_exchange_url;
  END IF;
END IF;

-- Check if initiated
SELECT initiated_outgoing_transaction_id, subject, credit_payto, (amount).val, (amount).frac,
    wtid, exchange_base_url
  INTO init_id, local_subject, local_credit_payto, local_amount.val, local_amount.frac, 
    local_wtid, local_exchange_base_url
  FROM initiated_outgoing_transactions LEFT JOIN transfer_operations USING (initiated_outgoing_transaction_id)
  WHERE end_to_end_id = in_end_to_end_id;
out_initiated=FOUND;
IF out_initiated AND NOT out_found THEN
  -- Check metadata
  -- TODO take subject if missing and more detailed credit payto
  IF local_subject IS DISTINCT FROM in_subject THEN
    RAISE NOTICE 'outgoing tx %: initiated subject is ''%'' got ''%''', in_end_to_end_id, local_subject, in_subject;
  END IF;
  IF local_credit_payto IS DISTINCT FROM in_credit_payto THEN
    RAISE NOTICE 'outgoing tx %: initiated subject credit payto is % got %', in_end_to_end_id, local_credit_payto, in_credit_payto;
  END IF;
  IF local_amount IS DISTINCT FROM in_amount THEN
    RAISE NOTICE 'outgoing tx %: initiated amount is % got %', in_end_to_end_id, local_amount, in_amount;
  END IF;
  IF local_wtid IS DISTINCT FROM in_wtid THEN
    RAISE NOTICE 'outgoing tx %: initiated wtid is % got %', in_end_to_end_id, local_wtid, in_wtid;
  END IF;
  IF local_exchange_base_url IS DISTINCT FROM in_exchange_url THEN
    RAISE NOTICE 'outgoing tx %: initiated exchange base url is % got %', in_end_to_end_id, local_exchange_base_url, in_exchange_url;
  END IF;
END IF;

IF NOT out_found THEN
  -- Store the transaction in the database
  INSERT INTO outgoing_transactions (
     amount
    ,subject
    ,execution_time
    ,credit_payto
    ,end_to_end_id
    ,acct_svcr_ref
  ) VALUES (
     in_amount
    ,in_subject
    ,in_execution_time
    ,in_credit_payto
    ,in_end_to_end_id
    ,in_acct_svcr_ref
  )
    RETURNING outgoing_transaction_id
      INTO out_tx_id;

  -- Register as talerable if contains wtid and exchange URL
  IF in_wtid IS NOT NULL OR in_exchange_url IS NOT NULL THEN
    SELECT end_to_end_id INTO local_end_to_end_id
      FROM talerable_outgoing_transactions
      JOIN outgoing_transactions USING (outgoing_transaction_id)
      WHERE wtid=in_wtid;
    IF FOUND THEN
      IF local_end_to_end_id != in_end_to_end_id THEN
        RAISE NOTICE 'wtid reuse: tx % and tx % have the same wtid %', in_end_to_end_id, local_end_to_end_id, in_wtid;
      END IF;
    ELSE
      INSERT INTO talerable_outgoing_transactions(outgoing_transaction_id, wtid, exchange_base_url) 
        VALUES (out_tx_id, in_wtid, in_exchange_url);
      PERFORM pg_notify('nexus_outgoing_tx', out_tx_id::text);
    END IF;
  END IF;

  IF out_initiated THEN
    -- Reconciles the related initiated transaction
    UPDATE initiated_outgoing_transactions
      SET 
        outgoing_transaction_id = out_tx_id
        ,status = 'success'
        ,status_msg = null
      WHERE initiated_outgoing_transaction_id = init_id 
        AND status != 'late_failure';
    
    -- Reconciles the related initiated batch
    UPDATE initiated_outgoing_batches
      SET status = 'success', status_msg = null
      WHERE message_id = in_msg_id AND status NOT IN ('success', 'permanent_failure', 'late_failure');
  END IF;
END IF;
END $$;
COMMENT ON FUNCTION register_outgoing
  IS 'Register an outgoing transaction and optionally reconciles the related initiated transaction with it';

CREATE FUNCTION register_incoming(
  IN in_amount taler_amount
  ,IN in_credit_fee taler_amount
  ,IN in_subject TEXT
  ,IN in_execution_time INT8
  ,IN in_debit_payto TEXT
  ,IN in_uetr UUID
  ,IN in_tx_id TEXT
  ,IN in_acct_svcr_ref TEXT
  ,IN in_type taler_incoming_type
  ,IN in_metadata BYTEA
  -- Error status
  ,OUT out_reserve_pub_reuse BOOLEAN
  -- Success return
  ,OUT out_found BOOLEAN
  ,OUT out_completed BOOLEAN
  ,OUT out_tx_id INT8
)
LANGUAGE plpgsql AS $$
DECLARE
local_ref TEXT;
local_amount taler_amount;
local_subject TEXT;
local_debit_payto TEXT;
BEGIN
IF in_credit_fee = (0, 0)::taler_amount THEN
  in_credit_fee = NULL;
END IF;

-- Check if already registered
SELECT incoming_transaction_id, subject, debit_payto, (amount).val, (amount).frac
  INTO out_tx_id, local_subject, local_debit_payto, local_amount.val, local_amount.frac
  FROM incoming_transactions
  WHERE uetr = in_uetr OR tx_id = in_tx_id OR acct_svcr_ref = in_acct_svcr_ref;
out_found=FOUND;
IF out_found THEN
  local_ref=COALESCE(in_uetr::text, in_tx_id, in_acct_svcr_ref);
  -- Check metadata
  IF in_subject != local_subject THEN
    RAISE NOTICE 'incoming tx %: stored subject is ''%'' got ''%''', local_ref, local_subject, in_subject;
  END IF;
  IF in_debit_payto != local_debit_payto THEN
    RAISE NOTICE 'incoming tx %: stored subject debit payto is % got %', local_ref, local_debit_payto, in_debit_payto;
  END IF;
  IF local_amount != in_amount THEN
    RAISE NOTICE 'incoming tx %: stored amount is % got %', local_ref, local_amount, in_amount;
  END IF;
  UPDATE incoming_transactions 
    SET subject=COALESCE(subject, in_subject),
        debit_payto=COALESCE(debit_payto, in_debit_payto),
        uetr=COALESCE(uetr, in_uetr),
        tx_id=COALESCE(tx_id, in_tx_id),
        acct_svcr_ref=COALESCE(acct_svcr_ref, in_acct_svcr_ref)
    WHERE incoming_transaction_id = out_tx_id;
  out_completed=COALESCE(in_subject, local_subject) IS NOT NULL AND COALESCE(in_debit_payto, local_debit_payto) IS NOT NULL;
  IF out_completed THEN
    PERFORM pg_notify('nexus_revenue_tx', out_tx_id::text);
  END IF;
ELSE
  out_reserve_pub_reuse=in_type = 'reserve' AND EXISTS(SELECT FROM talerable_incoming_transactions WHERE metadata = in_metadata AND type = 'reserve');
  IF out_reserve_pub_reuse THEN
    RETURN;
  END IF; 
  -- Store the transaction in the database
  INSERT INTO incoming_transactions (
    amount
    ,credit_fee
    ,subject
    ,execution_time
    ,debit_payto
    ,uetr
    ,tx_id
    ,acct_svcr_ref
  ) VALUES (
    in_amount
    ,in_credit_fee
    ,in_subject
    ,in_execution_time
    ,in_debit_payto
    ,in_uetr
    ,in_tx_id
    ,in_acct_svcr_ref
  ) RETURNING incoming_transaction_id INTO out_tx_id;
  IF in_subject IS NOT NULL AND in_debit_payto IS NOT NULL THEN
    PERFORM pg_notify('nexus_revenue_tx', out_tx_id::text);
  END IF;
END IF;

-- Register as talerable
IF in_type IS NOT NULL AND NOT EXISTS(SELECT FROM talerable_incoming_transactions WHERE incoming_transaction_id = out_tx_id) THEN
  -- We cannot use ON CONFLICT here because conversion use a trigger before insertion that isn't idempotent
  INSERT INTO talerable_incoming_transactions (
    incoming_transaction_id
    ,type
    ,metadata
  ) VALUES (
    out_tx_id
    ,in_type
    ,in_metadata
  );
  PERFORM pg_notify('nexus_incoming_tx', out_tx_id::text);
END IF;
END $$;

CREATE FUNCTION register_and_bounce_incoming(
  IN in_amount taler_amount
  ,IN in_credit_fee taler_amount
  ,IN in_subject TEXT
  ,IN in_execution_time INT8
  ,IN in_debit_payto TEXT
  ,IN in_uetr UUID
  ,IN in_tx_id TEXT
  ,IN in_acct_svcr_ref TEXT
  ,IN in_bounce_amount taler_amount
  ,IN in_now_date INT8
  ,IN in_bounce_id TEXT
  ,OUT out_found BOOLEAN
  ,OUT out_completed BOOLEAN
  ,OUT out_tx_id INT8
  ,OUT out_bounce_id TEXT
)
LANGUAGE plpgsql AS $$
DECLARE
init_id INT8;
bounce_amount taler_amount;
BEGIN
-- Register incoming transaction
SELECT reg.out_found, reg.out_completed, reg.out_tx_id
  FROM register_incoming(in_amount, in_credit_fee, in_subject, in_execution_time, in_debit_payto, in_uetr, in_tx_id, in_acct_svcr_ref, NULL, NULL) as reg
  INTO out_found, out_completed, out_tx_id;

-- Bounce incoming transaction
SELECT bounce.out_bounce_id INTO out_bounce_id FROM bounce_incoming(out_tx_id, in_bounce_amount, in_bounce_id, in_now_date) AS bounce;
END $$;

CREATE FUNCTION bounce_incoming(
  IN in_tx_id INT8
  ,IN in_bounce_amount taler_amount
  ,IN in_bounce_id TEXT
  ,IN in_now_date INT8
  ,OUT out_bounce_id TEXT
)
LANGUAGE plpgsql AS $$
DECLARE
local_bank_id TEXT;
payto_uri TEXT;
init_id INT8;
BEGIN
-- Check if already bounce
SELECT end_to_end_id INTO out_bounce_id
  FROM initiated_outgoing_transactions
  JOIN bounced_transactions USING (initiated_outgoing_transaction_id)
  WHERE incoming_transaction_id = in_tx_id;

-- Else initiate the bounce transaction
IF NOT FOUND THEN
  out_bounce_id = in_bounce_id;
  -- Get incoming transaction bank ID and creditor
  SELECT COALESCE(uetr::text, tx_id, acct_svcr_ref), debit_payto 
    INTO local_bank_id, payto_uri
    FROM incoming_transactions
    WHERE incoming_transaction_id = in_tx_id;
  -- Initiate the bounce transaction
  INSERT INTO initiated_outgoing_transactions (
    amount
    ,subject
    ,credit_payto
    ,initiation_time
    ,end_to_end_id
  ) VALUES (
    in_bounce_amount
    ,'bounce: ' || local_bank_id
    ,payto_uri
    ,in_now_date
    ,in_bounce_id
  )
  RETURNING initiated_outgoing_transaction_id INTO init_id;
  -- Register the bounce
  INSERT INTO bounced_transactions (incoming_transaction_id, initiated_outgoing_transaction_id)
    VALUES (in_tx_id, init_id);
END IF;
END$$;

CREATE FUNCTION taler_transfer(
  IN in_request_uid BYTEA,
  IN in_wtid BYTEA,
  IN in_subject TEXT,
  IN in_amount taler_amount,
  IN in_exchange_base_url TEXT,
  IN in_credit_account_payto TEXT,
  IN in_end_to_end_id TEXT,
  IN in_timestamp INT8,
  -- Error status
  OUT out_request_uid_reuse BOOLEAN,
  OUT out_wtid_reuse BOOLEAN,
  -- Success return
  OUT out_tx_row_id INT8,
  OUT out_timestamp INT8
)
LANGUAGE plpgsql AS $$
BEGIN
-- Check for idempotence and conflict
SELECT (amount != in_amount 
          OR credit_payto != in_credit_account_payto
          OR exchange_base_url != in_exchange_base_url
          OR wtid != in_wtid)
        ,transfer_operations.initiated_outgoing_transaction_id, initiation_time
  INTO out_request_uid_reuse, out_tx_row_id, out_timestamp
  FROM transfer_operations
      JOIN initiated_outgoing_transactions
        ON transfer_operations.initiated_outgoing_transaction_id=initiated_outgoing_transactions.initiated_outgoing_transaction_id 
  WHERE transfer_operations.request_uid = in_request_uid;
IF FOUND THEN
  RETURN;
END IF;
out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid);
IF out_wtid_reuse THEN
  RETURN;
END IF;
out_timestamp=in_timestamp;
-- Initiate bank transfer
INSERT INTO initiated_outgoing_transactions (
  amount
  ,subject
  ,credit_payto
  ,initiation_time
  ,end_to_end_id
) VALUES (
  in_amount
  ,in_subject
  ,in_credit_account_payto
  ,in_timestamp
  ,in_end_to_end_id
) RETURNING initiated_outgoing_transaction_id INTO out_tx_row_id;
-- Register outgoing transaction
INSERT INTO transfer_operations(
  initiated_outgoing_transaction_id
  ,request_uid
  ,wtid
  ,exchange_base_url
) VALUES (
  out_tx_row_id
  ,in_request_uid
  ,in_wtid
  ,in_exchange_base_url
);
out_timestamp = in_timestamp;
PERFORM pg_notify('nexus_outgoing_tx', out_tx_row_id::text);
END $$;

CREATE FUNCTION batch_outgoing_transactions(
  IN in_timestamp INT8,
  IN batch_ebics_id TEXT
)
RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
batch_id INT8;
local_sum taler_amount DEFAULT (0, 0)::taler_amount;
tx record;
BEGIN
-- Create a new batch only if some transactions are not batched
IF (EXISTS(SELECT FROM initiated_outgoing_transactions WHERE initiated_outgoing_batch_id IS NULL)) THEN
  -- Create batch
  INSERT INTO initiated_outgoing_batches (creation_date, message_id)
    VALUES (in_timestamp, batch_ebics_id)
    RETURNING initiated_outgoing_batch_id INTO batch_id;
  -- Link batched payment while computing the sum of amounts
  FOR tx IN UPDATE initiated_outgoing_transactions 
    SET initiated_outgoing_batch_id=batch_id
    WHERE initiated_outgoing_batch_id IS NULL
    RETURNING amount
  LOOP
    SELECT sum.val, sum.frac 
    INTO local_sum.val, local_sum.frac
    FROM amount_add(local_sum, tx.amount) AS sum;
  END LOOP;
  -- Update the batch with the sum of amounts
  UPDATE initiated_outgoing_batches SET sum=local_sum WHERE initiated_outgoing_batch_id=batch_id;
END IF;
END $$;

CREATE FUNCTION batch_status_update(
  IN in_message_id text,
  IN in_status submission_state,
  IN in_status_msg text
)
RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
local_batch_id INT8;
BEGIN
  -- Check if there is a batch for this message id
  SELECT initiated_outgoing_batch_id INTO local_batch_id
    FROM initiated_outgoing_batches
    WHERE message_id = in_message_id;
  IF FOUND THEN
    -- Update unsettled batch status 
    UPDATE initiated_outgoing_batches 
    SET status = in_status, status_msg = in_status_msg
    WHERE initiated_outgoing_batch_id = local_batch_id 
      AND status NOT IN ('success', 'permanent_failure', 'late_failure');

    -- When a batch succeed it doesn't mean that individual transaction also succeed
    IF in_status = 'success' THEN
      in_status = 'pending';
    END IF;

    -- Update unsettled batch's transaction status
    UPDATE initiated_outgoing_transactions 
    SET status = in_status, status_msg = in_status_msg
    WHERE initiated_outgoing_batch_id = local_batch_id
      AND status NOT IN ('success', 'permanent_failure', 'late_failure');
  END IF;
END $$;

CREATE FUNCTION tx_status_update(
  IN in_end_to_end_id text,
  IN in_message_id text,
  IN in_status submission_state,
  IN in_status_msg text
)
RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
local_status submission_state;
local_tx_id INT8;
BEGIN
  -- Check current tx status
  SELECT initiated_outgoing_transaction_id, status INTO local_tx_id, local_status
    FROM initiated_outgoing_transactions
    WHERE end_to_end_id = in_end_to_end_id;
  IF FOUND THEN
    -- Update unsettled transaction status
    IF in_status = 'permanent_failure' OR local_status NOT IN ('success', 'permanent_failure', 'late_failure') THEN
      IF in_status = 'permanent_failure' AND local_status = 'success' THEN
        in_status = 'late_failure';
      END IF;
      UPDATE initiated_outgoing_transactions 
      SET status = in_status, status_msg = in_status_msg
      WHERE initiated_outgoing_transaction_id = local_tx_id;
    END IF;

    -- Update unsettled batch status
    UPDATE initiated_outgoing_batches
    SET status = 'success', status_msg = NULL
    WHERE message_id = in_message_id
      AND status NOT IN ('success', 'permanent_failure', 'late_failure');
  END IF;
END $$;