--
-- This file is part of TALER
-- Copyright (C) 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 PROCEDURE IF EXISTS merchant_do_bump_number_bucket_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_number_bucket_stat(
  in_slug TEXT,
  in_timestamp TIMESTAMP,
  in_delta INT8
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_meta INT8;
  my_range merchant.statistic_range;
  my_bucket_start INT8;
  my_curs CURSOR (arg_slug TEXT)
   FOR SELECT UNNEST(ranges)
         FROM merchant_statistic_bucket_meta
        WHERE slug=arg_slug
          AND stype='number'; -- a slug may be registered for both types
BEGIN
  SELECT bmeta_serial_id
    INTO my_meta
    FROM merchant_statistic_bucket_meta
   WHERE slug=in_slug
     AND stype='number';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  OPEN my_curs (arg_slug:=in_slug);
  LOOP
    FETCH NEXT
      FROM my_curs
      INTO my_range;
    EXIT WHEN NOT FOUND;
    SELECT *
      INTO my_bucket_start
      FROM merchant.interval_to_start (in_timestamp, my_range);

    INSERT INTO merchant_statistic_bucket_counter AS msbc
      (bmeta_serial_id
      ,bucket_start
      ,bucket_range
      ,cumulative_number
      ) VALUES (
       my_meta
      ,my_bucket_start
      ,my_range
      ,in_delta)
     ON CONFLICT (bmeta_serial_id, bucket_start, bucket_range)
     DO UPDATE SET
       cumulative_number = msbc.cumulative_number + in_delta;
  END LOOP;
  CLOSE my_curs;
END $$;


DROP PROCEDURE IF EXISTS merchant_do_bump_amount_bucket_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_bucket_stat(
  in_slug TEXT,
  in_timestamp TIMESTAMP,
  in_delta merchant.taler_amount_currency
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_meta INT8;
  my_range merchant.statistic_range;
  my_bucket_start INT8;
  my_curs CURSOR (arg_slug TEXT)
   FOR SELECT UNNEST(ranges)
         FROM merchant_statistic_bucket_meta
        WHERE slug=arg_slug
          AND stype='amount'; -- a slug may be registered for both types
BEGIN
  SELECT bmeta_serial_id
    INTO my_meta
    FROM merchant_statistic_bucket_meta
   WHERE slug=in_slug
     AND stype='amount';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  OPEN my_curs (arg_slug:=in_slug);
  LOOP
    FETCH NEXT
      FROM my_curs
      INTO my_range;
    EXIT WHEN NOT FOUND;
    SELECT *
      INTO my_bucket_start
      FROM merchant.interval_to_start (in_timestamp, my_range);

    INSERT INTO merchant_statistic_bucket_amount AS msba
      (bmeta_serial_id
      ,bucket_start
      ,bucket_range
      ,curr
      ,cumulative_value
      ,cumulative_frac
      ) VALUES (
       my_meta
      ,my_bucket_start
      ,my_range
      ,(in_delta).curr
      ,(in_delta).val
      ,(in_delta).frac)
     ON CONFLICT (bmeta_serial_id, curr, bucket_start, bucket_range)
     DO UPDATE SET
       cumulative_value = msba.cumulative_value + (in_delta).val
       + CASE
           WHEN (in_delta).frac + msba.cumulative_frac >= 100000000
           THEN 1
           ELSE 0
         END,
       cumulative_frac = msba.cumulative_frac + (in_delta).frac
       - CASE
           WHEN (in_delta).frac + msba.cumulative_frac >= 100000000
           THEN 100000000
           ELSE 0
         END;
  END LOOP;
  CLOSE my_curs;
END $$;

COMMENT ON PROCEDURE merchant_do_bump_amount_bucket_stat
  IS 'Updates an amount statistic tracked over buckets';


DROP PROCEDURE IF EXISTS merchant_do_bump_number_interval_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_number_interval_stat(
  in_slug TEXT,
  in_timestamp TIMESTAMP,
  in_delta INT8
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_now INT8;
  my_record RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_rangex INT8;
  my_precisionx INT8;
  my_start INT8;
  my_event INT8;
BEGIN
  my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  SELECT imeta_serial_id
        ,ranges AS ranges
        ,precisions AS precisions
    INTO my_record
    FROM merchant_statistic_interval_meta
   WHERE slug=in_slug
     AND stype='number';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;

  my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; -- convert to seconds
  my_precisions = my_record.precisions;
  my_ranges = my_record.ranges;
  my_rangex = NULL;
  FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0)
  LOOP
    IF my_now - my_ranges[my_x] < my_start
    THEN
      my_rangex = my_ranges[my_x];
      my_precisionx = my_precisions[my_x];
      EXIT;
    END IF;
  END LOOP;
  IF my_rangex IS NULL
  THEN
    -- event is beyond the ranges we care about
    RETURN;
  END IF;

  my_meta = my_record.imeta_serial_id;
  my_start = my_start - my_start % my_precisionx; -- round down

  INSERT INTO merchant_statistic_counter_event AS msce
    (imeta_serial_id
    ,slot
    ,delta)
   VALUES
    (my_meta
    ,my_start
    ,in_delta)
   ON CONFLICT (imeta_serial_id, slot)
   DO UPDATE SET
     delta = msce.delta + in_delta
   RETURNING nevent_serial_id
        INTO my_event;

  INSERT INTO merchant_statistic_interval_counter AS msic
    (imeta_serial_id
    ,range
    ,event_delimiter
    ,cumulative_number
   ) VALUES (
     my_meta
    ,my_rangex
    ,my_event
    ,in_delta)
   ON CONFLICT (imeta_serial_id, range)
   DO UPDATE SET
     cumulative_number = msic.cumulative_number + in_delta;
END $$;

COMMENT ON PROCEDURE merchant_do_bump_number_interval_stat
  IS 'Updates a numeric statistic tracked over an interval';


DROP PROCEDURE IF EXISTS merchant_do_bump_amount_interval_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_interval_stat(
  in_slug TEXT,
  in_timestamp TIMESTAMP,
  in_delta merchant.taler_amount_currency -- new amount in table that we should add to the tracker
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_now INT8;
  my_record RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_x INT;
  my_rangex INT8;
  my_precisionx INT8;
  my_start INT8;
  my_event INT8;
BEGIN
  my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  SELECT imeta_serial_id
        ,ranges
        ,precisions
    INTO my_record
    FROM merchant_statistic_interval_meta
   WHERE slug=in_slug
     AND stype='amount';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;

  my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; -- convert to seconds since epoch
  my_precisions = my_record.precisions;
  my_ranges = my_record.ranges;
  my_rangex = NULL;
  FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0)
  LOOP
    IF my_now - my_ranges[my_x] < my_start
    THEN
      my_rangex = my_ranges[my_x];
      my_precisionx = my_precisions[my_x];
      EXIT;
    END IF;
  END LOOP;
  IF my_rangex IS NULL
  THEN
    -- event is beyond the ranges we care about
    RETURN;
  END IF;
  my_start = my_start - my_start % my_precisionx; -- round down
  my_meta = my_record.imeta_serial_id;

  INSERT INTO merchant_statistic_amount_event AS msae
    (imeta_serial_id
    ,slot
    ,delta_curr
    ,delta_value
    ,delta_frac
    ) VALUES (
     my_meta
    ,my_start
    ,(in_delta).curr
    ,(in_delta).val
    ,(in_delta).frac
    )
    ON CONFLICT (imeta_serial_id, slot, delta_curr)
    DO UPDATE SET
      delta_value = msae.delta_value + (in_delta).val
        + CASE
          WHEN (in_delta).frac + msae.delta_frac >= 100000000
          THEN 1
          ELSE 0
        END,
      delta_frac = msae.delta_frac + (in_delta).frac
        - CASE
          WHEN (in_delta).frac + msae.delta_frac >= 100000000
          THEN 100000000
          ELSE 0
        END
    RETURNING aevent_serial_id
         INTO my_event;

  INSERT INTO merchant_statistic_interval_amount AS msia
    (imeta_serial_id
    ,range
    ,event_delimiter
    ,curr
    ,cumulative_value
    ,cumulative_frac
    ) VALUES (
     my_meta
    ,my_rangex
    ,my_event
    ,(in_delta).curr
    ,(in_delta).val
    ,(in_delta).frac)
   ON CONFLICT (imeta_serial_id, curr, range)
   DO UPDATE SET
     cumulative_value = msia.cumulative_value + (in_delta).val
     + CASE
         WHEN (in_delta).frac + msia.cumulative_frac >= 100000000
         THEN 1
         ELSE 0
       END,
     cumulative_frac = msia.cumulative_frac + (in_delta).frac
     - CASE
         WHEN (in_delta).frac + msia.cumulative_frac >= 100000000
         THEN 100000000
         ELSE 0
       END;
END $$;
COMMENT ON PROCEDURE merchant_do_bump_amount_interval_stat
  IS 'Updates an amount statistic tracked over an interval';


DROP PROCEDURE IF EXISTS merchant_do_bump_number_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_number_stat(
  in_slug TEXT,
  in_timestamp TIMESTAMP,
  in_delta INT8
)
LANGUAGE plpgsql
AS $$
BEGIN
  -- Complain loudly about statistics that nobody ever registered: both
  -- subroutines below silently do nothing for an unknown slug, so a typo
  -- in a trigger would otherwise never be noticed.
  PERFORM
    FROM (SELECT 1
            FROM merchant_statistic_bucket_meta
           WHERE slug=in_slug
             AND stype='number'
          UNION ALL
          SELECT 1
            FROM merchant_statistic_interval_meta
           WHERE slug=in_slug
             AND stype='number') AS known;
  IF NOT FOUND
  THEN
    RAISE WARNING 'Numeric statistic "%" is not registered, event dropped', in_slug;
  END IF;
  CALL merchant_do_bump_number_bucket_stat (in_slug, in_timestamp, in_delta);
  CALL merchant_do_bump_number_interval_stat (in_slug, in_timestamp, in_delta);
END $$;
COMMENT ON PROCEDURE merchant_do_bump_number_stat
  IS 'Updates a numeric statistic (bucket or interval)';


DROP PROCEDURE IF EXISTS merchant_do_bump_amount_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_stat(
  in_slug TEXT,
  in_timestamp TIMESTAMP,
  in_delta merchant.taler_amount_currency
)
LANGUAGE plpgsql
AS $$
BEGIN
  -- Complain loudly about statistics that nobody ever registered: both
  -- subroutines below silently do nothing for an unknown slug, so a typo
  -- in a trigger would otherwise never be noticed.
  PERFORM
    FROM (SELECT 1
            FROM merchant_statistic_bucket_meta
           WHERE slug=in_slug
             AND stype='amount'
          UNION ALL
          SELECT 1
            FROM merchant_statistic_interval_meta
           WHERE slug=in_slug
             AND stype='amount') AS known;
  IF NOT FOUND
  THEN
    RAISE WARNING 'Amount statistic "%" is not registered, event dropped', in_slug;
  END IF;
  CALL merchant_do_bump_amount_bucket_stat (in_slug, in_timestamp, in_delta);
  CALL merchant_do_bump_amount_interval_stat (in_slug, in_timestamp, in_delta);
END $$;
COMMENT ON PROCEDURE merchant_do_bump_amount_stat
  IS 'Updates an amount statistic (bucket or interval)';



DROP PROCEDURE IF EXISTS merchant_statistic_counter_gc;
CREATE OR REPLACE PROCEDURE merchant_statistic_counter_gc ()
LANGUAGE plpgsql
AS $$
DECLARE
  my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  my_rec RECORD;
  my_sum RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_precision INT4;
  my_i INT4;
  min_slot INT8;
  max_slot INT8;
  end_slot INT8;
  my_total INT8;
BEGIN
  -- Do combination work for all numeric statistic events
  FOR my_rec IN
    SELECT imeta_serial_id
          ,ranges
          ,precisions
          ,slug
      FROM merchant_statistic_interval_meta
  LOOP
    PERFORM FROM merchant_statistic_interval_number_get (my_rec.slug);

    my_meta = my_rec.imeta_serial_id;
    my_ranges = my_rec.ranges;
    my_precisions = my_rec.precisions;

    FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0)
    LOOP
      my_precision = my_precisions[my_i];
      IF 1 >= my_precision
      THEN
        -- Cannot coarsen in this case
        CONTINUE;
      END IF;

      IF 1 = my_i
      THEN
        min_slot = 0;
      ELSE
        min_slot = my_ranges[my_i - 1];
      END IF;
      end_slot = my_ranges[my_i];
      RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision;

      LOOP
        EXIT WHEN min_slot >= end_slot;
        max_slot = min_slot + my_precision;
        SELECT SUM(delta) AS total,
               COUNT(*)   AS matches,
               MIN(nevent_serial_id) AS rep_serial_id
          INTO my_sum
          FROM merchant_statistic_counter_event
         WHERE imeta_serial_id=my_meta
           AND slot >= my_time - max_slot
           AND slot  < my_time - min_slot;

        RAISE NOTICE 'Found % entries between [%,%)', my_sum.matches, my_time - max_slot, my_time - min_slot;
        -- we only proceed if we had more then one match (optimization)
        IF FOUND AND my_sum.matches > 1
        THEN
          my_total = my_sum.total;

          RAISE NOTICE 'combining % entries to representative % for slots [%-%)', my_sum.matches, my_sum.rep_serial_id, my_time - max_slot, my_time - min_slot;

          -- The representative absorbs the deltas of all entries we are
          -- about to remove, so any interval counter still using one of
          -- them as its watermark must be moved onto the representative.
          -- Without this, event_delimiter's ON DELETE RESTRICT aborts the
          -- entire garbage collection run.
          UPDATE merchant_statistic_interval_counter
             SET event_delimiter = my_sum.rep_serial_id
           WHERE imeta_serial_id = my_meta
             AND event_delimiter IN
               (SELECT nevent_serial_id
                  FROM merchant_statistic_counter_event
                 WHERE imeta_serial_id=my_meta
                   AND slot >= my_time - max_slot
                   AND slot  < my_time - min_slot
                   AND nevent_serial_id > my_sum.rep_serial_id);

          -- combine entries
          DELETE FROM merchant_statistic_counter_event
           WHERE imeta_serial_id=my_meta
             AND slot >= my_time - max_slot
             AND slot  < my_time - min_slot
             AND nevent_serial_id > my_sum.rep_serial_id;
           -- Now update the representative to the sum
          UPDATE merchant_statistic_counter_event SET
            delta = my_total
           WHERE imeta_serial_id = my_meta
             AND nevent_serial_id = my_sum.rep_serial_id;
        END IF;
        min_slot = min_slot + my_precision;
      END LOOP; -- min_slot to end_slot by precision loop
    END LOOP; -- my_i loop
    -- Finally, delete all events beyond the range we care about

    RAISE NOTICE 'deleting entries of % before % - % = %', my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)];
    DELETE FROM merchant_statistic_counter_event
     WHERE imeta_serial_id=my_meta
       AND slot < my_time - my_ranges[array_length(my_ranges,1)];
  END LOOP; -- my_rec loop
END $$;
COMMENT ON PROCEDURE merchant_statistic_counter_gc
  IS 'Performs garbage collection and compaction of the merchant_statistic_counter_event table';



DROP PROCEDURE IF EXISTS merchant_statistic_amount_gc;
CREATE OR REPLACE PROCEDURE merchant_statistic_amount_gc ()
LANGUAGE plpgsql
AS $$
DECLARE
  my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  my_rec RECORD;
  my_sum RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_precision INT4;
  my_currency TEXT;
  my_i INT4;
  min_slot INT8;
  max_slot INT8;
  end_slot INT8;
  my_total_val INT8;
  my_total_frac INT8;
BEGIN
  -- Do combination work for all numeric statistic events
  FOR my_rec IN
    SELECT imeta_serial_id
          ,ranges
          ,precisions
          ,slug
      FROM merchant_statistic_interval_meta
  LOOP

  PERFORM FROM merchant_statistic_interval_amount_get (my_rec.slug);

  my_meta = my_rec.imeta_serial_id;
  my_ranges = my_rec.ranges;
  my_precisions = my_rec.precisions;
  FOR my_currency IN
    SELECT DISTINCT delta_curr
      FROM merchant_statistic_amount_event
     WHERE imeta_serial_id = my_meta
  LOOP

    FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0)
    LOOP
      my_precision = my_precisions[my_i];
      IF 1 >= my_precision
      THEN
        -- Cannot coarsen in this case
        CONTINUE;
      END IF;

      IF 1 = my_i
      THEN
        min_slot = 0;
      ELSE
        min_slot = my_ranges[my_i - 1];
      END IF;
      end_slot = my_ranges[my_i];

      RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision;
      LOOP
        EXIT WHEN min_slot >= end_slot;
        max_slot = min_slot + my_precision;
        SELECT SUM(delta_value) AS total_val,
               SUM(delta_frac)  AS total_frac,
               COUNT(*)         AS matches,
               MIN(aevent_serial_id) AS rep_serial_id
          INTO my_sum
          FROM merchant_statistic_amount_event
         WHERE imeta_serial_id=my_meta
           AND delta_curr = my_currency
           AND slot >= my_time - max_slot
           AND slot  < my_time - min_slot;
        -- we only proceed if we had more then one match (optimization)
        IF FOUND AND my_sum.matches > 1
        THEN
          -- normalize new total
          my_total_frac = my_sum.total_frac % 100000000;
          my_total_val = my_sum.total_val + my_sum.total_frac / 100000000;

          -- The representative absorbs the deltas of all entries we are
          -- about to remove, so any interval amount still using one of
          -- them as its watermark must be moved onto the representative.
          -- Without this, event_delimiter's ON DELETE RESTRICT aborts the
          -- entire garbage collection run.
          UPDATE merchant_statistic_interval_amount
             SET event_delimiter = my_sum.rep_serial_id
           WHERE imeta_serial_id = my_meta
             AND event_delimiter IN
               (SELECT aevent_serial_id
                  FROM merchant_statistic_amount_event
                 WHERE imeta_serial_id=my_meta
                   AND delta_curr = my_currency
                   AND slot >= my_time - max_slot
                   AND slot  < my_time - min_slot
                   AND aevent_serial_id > my_sum.rep_serial_id);

          -- combine entries
          DELETE FROM merchant_statistic_amount_event
           WHERE imeta_serial_id=my_meta
             AND delta_curr = my_currency
             AND slot >= my_time - max_slot
             AND slot  < my_time - min_slot
             AND aevent_serial_id > my_sum.rep_serial_id;
          -- Now update the representative to the sum
          UPDATE merchant_statistic_amount_event SET
             delta_value = my_total_val
            ,delta_frac = my_total_frac
           WHERE imeta_serial_id = my_meta
             AND delta_curr = my_currency
             AND aevent_serial_id = my_sum.rep_serial_id;
        END IF;
        min_slot = min_slot + my_precision;
      END LOOP; -- min_slot to end_slot by precision loop
    END LOOP; -- my_i loop
  END LOOP; -- my_currency loop
  -- Finally, delete all events beyond the range we care about

  RAISE NOTICE 'deleting entries of % before % - % = %', my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)];
  DELETE FROM merchant_statistic_amount_event
   WHERE imeta_serial_id=my_meta
     AND slot < my_time - my_ranges[array_length(my_ranges,1)];
  END LOOP; -- my_rec loop
END $$;
COMMENT ON PROCEDURE merchant_statistic_amount_gc
  IS 'Performs garbage collection and compaction of the merchant_statistic_amount_event table';



DROP PROCEDURE IF EXISTS merchant_statistic_bucket_gc;
CREATE OR REPLACE PROCEDURE merchant_statistic_bucket_gc ()
LANGUAGE plpgsql
AS $$
DECLARE
  my_rec RECORD;
  my_range INTERVAL;
  my_now INT8;
  my_end INT8;
BEGIN
  my_now = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)::TIMESTAMP); -- seconds since epoch
  FOR my_rec IN
    SELECT bmeta_serial_id
          ,stype
          ,ranges[array_length(ranges,1)] AS range
          ,ages[array_length(ages,1)] AS age
      FROM merchant_statistic_bucket_meta
  LOOP
    IF my_rec.range = 'quarter'
    THEN
      -- '1 quarter' is not a valid PostgreSQL interval
      my_range = INTERVAL '3 months';
    ELSE
      my_range = ('1 ' || my_rec.range::TEXT)::INTERVAL;
    END IF;
    -- age is given in multiples of the range; EPOCH (and not SECONDS,
    -- which is merely the seconds *field* of the interval and thus zero
    -- for all of our ranges) converts that range into seconds
    my_end = my_now - my_rec.age * EXTRACT(EPOCH FROM my_range);
    IF my_rec.stype = 'amount'
    THEN
      DELETE
        FROM merchant_statistic_bucket_amount
       WHERE bmeta_serial_id = my_rec.bmeta_serial_id
         AND bucket_start < my_end;
    ELSE
      DELETE
        FROM merchant_statistic_bucket_counter
       WHERE bmeta_serial_id = my_rec.bmeta_serial_id
         AND bucket_start < my_end;
    END IF;
  END LOOP;
END $$;
COMMENT ON PROCEDURE merchant_statistic_bucket_gc
  IS 'Performs garbage collection of the merchant_statistic_bucket_counter and merchant_statistic_bucket_amount tables';



-- The date_trunc may not be necessary if we assume it is already truncated
DROP FUNCTION IF EXISTS merchant_statistics_bucket_end;
CREATE FUNCTION merchant_statistics_bucket_end (
  IN in_bucket_start INT8,
  IN in_range merchant.statistic_range,
  OUT out_bucket_end INT8
)
LANGUAGE plpgsql
AS $$
BEGIN
    IF in_range='quarter'
    THEN
      out_bucket_end = EXTRACT(EPOCH FROM CAST(date_trunc('quarter', to_timestamp(in_bucket_start)::date)  + interval '3 months' AS date));
    ELSE
      out_bucket_end = EXTRACT(EPOCH FROM CAST(to_timestamp(in_bucket_start)::date  + ('1 ' || in_range)::interval AS date));
    END IF;
END $$;
COMMENT ON FUNCTION merchant_statistics_bucket_end
IS 'computes the end time of the bucket for an event at the current time given the desired bucket range';
