CREATE SCHEMA condon;
REVOKE ALL ON SCHEMA condon FROM PUBLIC;
COMMENT ON SCHEMA condon IS 'CONDO owner entitlement backend';

CREATE TABLE condon.schema_migrations (
  version varchar(64) PRIMARY KEY,
  checksum varchar(64) NOT NULL,
  applied_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  CONSTRAINT schema_migrations_checksum_check
    CHECK (checksum ~ '^[0-9a-f]{64}$')
);

CREATE TABLE condon.room_types (
  code varchar(16) PRIMARY KEY,
  entitlement_tier smallint,
  requires_manual_multiplier boolean NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  updated_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  CONSTRAINT room_types_code_check
    CHECK (code ~ '^[A-Z0-9]+$'),
  CONSTRAINT room_types_rule_shape_check
    CHECK (
      (requires_manual_multiplier AND entitlement_tier IS NULL)
      OR
      (
        NOT requires_manual_multiplier
        AND entitlement_tier BETWEEN 1 AND 3
      )
    )
);

CREATE TABLE condon.owner_accounts (
  id uuid PRIMARY KEY,
  account_no integer,
  transfer_date date,
  owner_name text NOT NULL,
  room_no varchar(32) NOT NULL,
  purchased_room_type_code varchar(16) NOT NULL,
  unit_no varchar(32) NOT NULL,
  member_no varchar(32) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  updated_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  CONSTRAINT owner_accounts_account_no_key UNIQUE (account_no),
  CONSTRAINT owner_accounts_room_no_key UNIQUE (room_no),
  CONSTRAINT owner_accounts_account_no_check
    CHECK (account_no IS NULL OR account_no > 0),
  CONSTRAINT owner_accounts_owner_name_check
    CHECK (pg_catalog.btrim(owner_name) <> ''),
  CONSTRAINT owner_accounts_room_no_check
    CHECK (pg_catalog.btrim(room_no) <> ''),
  CONSTRAINT owner_accounts_unit_no_check
    CHECK (pg_catalog.btrim(unit_no) <> ''),
  CONSTRAINT owner_accounts_member_no_check
    CHECK (pg_catalog.btrim(member_no) <> ''),
  CONSTRAINT owner_accounts_purchased_room_type_fk
    FOREIGN KEY (purchased_room_type_code)
    REFERENCES condon.room_types(code)
    ON UPDATE RESTRICT
    ON DELETE RESTRICT
);

CREATE TABLE condon.entitlement_periods (
  id uuid PRIMARY KEY,
  owner_account_id uuid NOT NULL,
  period_year smallint NOT NULL,
  period_start date NOT NULL,
  period_end date NOT NULL,
  annual_grant integer NOT NULL DEFAULT 15,
  carry_forward integer NOT NULL DEFAULT 0,
  current_balance integer NOT NULL,
  row_version integer NOT NULL DEFAULT 1,
  created_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  updated_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  CONSTRAINT entitlement_periods_owner_year_key
    UNIQUE (owner_account_id, period_year),
  CONSTRAINT entitlement_periods_id_owner_key
    UNIQUE (id, owner_account_id),
  CONSTRAINT entitlement_periods_owner_fk
    FOREIGN KEY (owner_account_id)
    REFERENCES condon.owner_accounts(id)
    ON UPDATE RESTRICT
    ON DELETE RESTRICT,
  CONSTRAINT entitlement_periods_year_check
    CHECK (period_year BETWEEN 2000 AND 9999),
  CONSTRAINT entitlement_periods_dates_check
    CHECK (
      period_start = pg_catalog.make_date(period_year::integer, 1, 1)
      AND period_end = pg_catalog.make_date(period_year::integer, 12, 31)
    ),
  CONSTRAINT entitlement_periods_annual_grant_check
    CHECK (annual_grant >= 0),
  CONSTRAINT entitlement_periods_carry_forward_check
    CHECK (carry_forward >= 0),
  CONSTRAINT entitlement_periods_current_balance_check
    CHECK (current_balance >= 0),
  CONSTRAINT entitlement_periods_row_version_check
    CHECK (row_version >= 1)
);

CREATE TABLE condon.usage_records (
  id uuid PRIMARY KEY,
  owner_account_id uuid NOT NULL,
  entitlement_period_id uuid NOT NULL,
  confirmation_no varchar(64) NOT NULL,
  check_in date NOT NULL,
  check_out date NOT NULL,
  night_count integer NOT NULL,
  used_room_type_code varchar(16) NOT NULL,
  applied_multiplier smallint NOT NULL,
  rule_version varchar(32) NOT NULL DEFAULT 'v1',
  use_nights integer NOT NULL,
  balance_before integer NOT NULL,
  balance_after integer NOT NULL,
  remark text NOT NULL DEFAULT '',
  idempotency_key uuid NOT NULL,
  created_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  CONSTRAINT usage_records_confirmation_no_key UNIQUE (confirmation_no),
  CONSTRAINT usage_records_idempotency_key_key UNIQUE (idempotency_key),
  CONSTRAINT usage_records_id_period_key UNIQUE (id, entitlement_period_id),
  CONSTRAINT usage_records_account_fk
    FOREIGN KEY (owner_account_id)
    REFERENCES condon.owner_accounts(id)
    ON UPDATE RESTRICT
    ON DELETE RESTRICT,
  CONSTRAINT usage_records_period_account_fk
    FOREIGN KEY (entitlement_period_id, owner_account_id)
    REFERENCES condon.entitlement_periods(id, owner_account_id)
    ON UPDATE RESTRICT
    ON DELETE RESTRICT,
  CONSTRAINT usage_records_used_room_type_fk
    FOREIGN KEY (used_room_type_code)
    REFERENCES condon.room_types(code)
    ON UPDATE RESTRICT
    ON DELETE RESTRICT,
  CONSTRAINT usage_records_confirmation_no_check
    CHECK (confirmation_no ~ '^[0-9]+$'),
  CONSTRAINT usage_records_dates_check
    CHECK (check_out > check_in),
  CONSTRAINT usage_records_same_year_check
    CHECK (
      EXTRACT(year FROM check_in)
      = EXTRACT(year FROM check_out)
    ),
  CONSTRAINT usage_records_night_count_check
    CHECK (night_count = check_out - check_in AND night_count > 0),
  CONSTRAINT usage_records_multiplier_check
    CHECK (applied_multiplier BETWEEN 1 AND 3),
  CONSTRAINT usage_records_use_nights_check
    CHECK (
      use_nights = night_count * applied_multiplier
      AND use_nights > 0
    ),
  CONSTRAINT usage_records_balance_check
    CHECK (
      balance_before >= use_nights
      AND balance_after = balance_before - use_nights
      AND balance_after >= 0
    )
);

CREATE TABLE condon.entitlement_ledger (
  id uuid PRIMARY KEY,
  entitlement_period_id uuid NOT NULL,
  usage_record_id uuid,
  entry_type varchar(32) NOT NULL,
  delta_nights integer NOT NULL,
  balance_before integer NOT NULL,
  balance_after integer NOT NULL,
  occurred_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(),
  CONSTRAINT entitlement_ledger_usage_record_key
    UNIQUE (usage_record_id),
  CONSTRAINT entitlement_ledger_period_fk
    FOREIGN KEY (entitlement_period_id)
    REFERENCES condon.entitlement_periods(id)
    ON UPDATE RESTRICT
    ON DELETE RESTRICT,
  CONSTRAINT entitlement_ledger_usage_period_fk
    FOREIGN KEY (usage_record_id, entitlement_period_id)
    REFERENCES condon.usage_records(id, entitlement_period_id)
    ON UPDATE RESTRICT
    ON DELETE RESTRICT,
  CONSTRAINT entitlement_ledger_entry_type_check
    CHECK (entry_type IN ('annual_grant', 'carry_forward', 'usage')),
  CONSTRAINT entitlement_ledger_balance_check
    CHECK (
      balance_after = balance_before + delta_nights
      AND balance_after >= 0
    ),
  CONSTRAINT entitlement_ledger_entry_shape_check
    CHECK (
      (
        entry_type = 'usage'
        AND usage_record_id IS NOT NULL
        AND delta_nights < 0
      )
      OR
      (
        entry_type IN ('annual_grant', 'carry_forward')
        AND usage_record_id IS NULL
        AND delta_nights > 0
      )
    )
);

CREATE INDEX owner_accounts_member_no_idx
  ON condon.owner_accounts (member_no);
CREATE INDEX owner_accounts_purchased_room_type_idx
  ON condon.owner_accounts (purchased_room_type_code);
CREATE INDEX usage_records_owner_created_idx
  ON condon.usage_records (owner_account_id, created_at DESC, id DESC);
CREATE INDEX usage_records_period_idx
  ON condon.usage_records (entitlement_period_id);
CREATE INDEX usage_records_used_type_check_in_idx
  ON condon.usage_records (used_room_type_code, check_in);
CREATE INDEX entitlement_ledger_period_occurred_idx
  ON condon.entitlement_ledger (entitlement_period_id, occurred_at, id);

INSERT INTO condon.room_types (
  code,
  entitlement_tier,
  requires_manual_multiplier
)
VALUES
  ('RM1', 1, false),
  ('RM2', 1, false),
  ('RM3', 1, false),
  ('RM4', 1, false),
  ('UG1', 1, false),
  ('UG2', 1, false),
  ('SU1', 2, false),
  ('SU2', 2, false),
  ('SU6', 2, false),
  ('SU3', 3, false),
  ('AC2', NULL, true);

CREATE FUNCTION condon.calculate_multiplier(
  p_purchased_room_type_code varchar,
  p_used_room_type_code varchar,
  p_manual_multiplier smallint
)
RETURNS smallint
LANGUAGE plpgsql
STABLE
SECURITY INVOKER
SET search_path = pg_catalog
AS $function$
DECLARE
  v_purchased_tier smallint;
  v_used_tier smallint;
  v_purchased_manual boolean;
  v_used_manual boolean;
BEGIN
  SELECT
    entitlement_tier,
    requires_manual_multiplier
  INTO
    v_purchased_tier,
    v_purchased_manual
  FROM condon.room_types
  WHERE code = p_purchased_room_type_code;

  IF NOT FOUND THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_PURCHASED_ROOM_TYPE_NOT_FOUND';
  END IF;

  SELECT
    entitlement_tier,
    requires_manual_multiplier
  INTO
    v_used_tier,
    v_used_manual
  FROM condon.room_types
  WHERE code = p_used_room_type_code;

  IF NOT FOUND THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_USED_ROOM_TYPE_NOT_FOUND';
  END IF;

  IF v_purchased_manual OR v_used_manual THEN
    IF p_manual_multiplier IS NULL OR p_manual_multiplier NOT BETWEEN 1 AND 3 THEN
      RAISE EXCEPTION USING
        ERRCODE = '22023',
        MESSAGE = 'CONDON_MANUAL_MULTIPLIER_REQUIRED';
    END IF;
    RETURN p_manual_multiplier;
  END IF;

  IF p_manual_multiplier IS NOT NULL THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_MANUAL_MULTIPLIER_NOT_ALLOWED';
  END IF;

  RETURN GREATEST(
    1,
    v_used_tier - v_purchased_tier + 1
  )::smallint;
END;
$function$;

CREATE FUNCTION condon.open_entitlement_period(
  p_period_id uuid,
  p_owner_account_id uuid,
  p_period_year smallint,
  p_annual_grant_ledger_id uuid,
  p_carry_forward_ledger_id uuid
)
RETURNS condon.entitlement_periods
LANGUAGE plpgsql
VOLATILE
SECURITY INVOKER
SET search_path = pg_catalog
AS $function$
DECLARE
  v_carry_forward integer := 0;
  v_period condon.entitlement_periods%ROWTYPE;
  v_now timestamptz := pg_catalog.clock_timestamp();
BEGIN
  IF p_period_id IS NULL
    OR p_owner_account_id IS NULL
    OR p_annual_grant_ledger_id IS NULL
  THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_REQUIRED_ID_MISSING';
  END IF;

  IF p_period_year NOT BETWEEN 2000 AND 9999 THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_INVALID_PERIOD_YEAR';
  END IF;

  PERFORM 1
  FROM condon.owner_accounts
  WHERE id = p_owner_account_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION USING
      ERRCODE = 'P0002',
      MESSAGE = 'CONDON_OWNER_ACCOUNT_NOT_FOUND';
  END IF;

  IF EXISTS (
    SELECT 1
    FROM condon.entitlement_periods
    WHERE owner_account_id = p_owner_account_id
      AND period_year = p_period_year
  ) THEN
    RAISE EXCEPTION USING
      ERRCODE = '23505',
      MESSAGE = 'CONDON_ENTITLEMENT_PERIOD_EXISTS';
  END IF;

  SELECT current_balance
  INTO v_carry_forward
  FROM condon.entitlement_periods
  WHERE owner_account_id = p_owner_account_id
    AND period_year = p_period_year - 1
  FOR UPDATE;

  v_carry_forward := COALESCE(v_carry_forward, 0);

  IF v_carry_forward > 0 AND p_carry_forward_ledger_id IS NULL THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_CARRY_FORWARD_LEDGER_ID_REQUIRED';
  END IF;

  IF v_carry_forward = 0 AND p_carry_forward_ledger_id IS NOT NULL THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_CARRY_FORWARD_LEDGER_ID_NOT_ALLOWED';
  END IF;

  INSERT INTO condon.entitlement_periods (
    id,
    owner_account_id,
    period_year,
    period_start,
    period_end,
    annual_grant,
    carry_forward,
    current_balance,
    row_version,
    created_at,
    updated_at
  )
  VALUES (
    p_period_id,
    p_owner_account_id,
    p_period_year,
    pg_catalog.make_date(p_period_year::integer, 1, 1),
    pg_catalog.make_date(p_period_year::integer, 12, 31),
    15,
    v_carry_forward,
    15 + v_carry_forward,
    1,
    v_now,
    v_now
  )
  RETURNING * INTO v_period;

  INSERT INTO condon.entitlement_ledger (
    id,
    entitlement_period_id,
    usage_record_id,
    entry_type,
    delta_nights,
    balance_before,
    balance_after,
    occurred_at
  )
  VALUES (
    p_annual_grant_ledger_id,
    p_period_id,
    NULL,
    'annual_grant',
    15,
    0,
    15,
    v_now
  );

  IF v_carry_forward > 0 THEN
    INSERT INTO condon.entitlement_ledger (
      id,
      entitlement_period_id,
      usage_record_id,
      entry_type,
      delta_nights,
      balance_before,
      balance_after,
      occurred_at
    )
    VALUES (
      p_carry_forward_ledger_id,
      p_period_id,
      NULL,
      'carry_forward',
      v_carry_forward,
      15,
      15 + v_carry_forward,
      v_now
    );
  END IF;

  RETURN v_period;
END;
$function$;

CREATE FUNCTION condon.create_usage_record(
  p_usage_record_id uuid,
  p_ledger_id uuid,
  p_owner_account_id uuid,
  p_confirmation_no varchar,
  p_check_in date,
  p_check_out date,
  p_used_room_type_code varchar,
  p_manual_multiplier smallint,
  p_remark text,
  p_idempotency_key uuid
)
RETURNS condon.usage_records
LANGUAGE plpgsql
VOLATILE
SECURITY INVOKER
SET search_path = pg_catalog
AS $function$
DECLARE
  v_purchased_room_type_code varchar(16);
  v_multiplier smallint;
  v_night_count integer;
  v_use_nights integer;
  v_balance_before integer;
  v_balance_after integer;
  v_period condon.entitlement_periods%ROWTYPE;
  v_existing condon.usage_records%ROWTYPE;
  v_usage condon.usage_records%ROWTYPE;
  v_now timestamptz := pg_catalog.clock_timestamp();
  v_updated_count integer;
BEGIN
  IF p_usage_record_id IS NULL
    OR p_ledger_id IS NULL
    OR p_owner_account_id IS NULL
    OR p_idempotency_key IS NULL
  THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_REQUIRED_ID_MISSING';
  END IF;

  IF p_confirmation_no IS NULL
    OR p_confirmation_no !~ '^[0-9]+$'
  THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_INVALID_CONFIRMATION_NO';
  END IF;

  IF p_check_in IS NULL
    OR p_check_out IS NULL
    OR p_check_out <= p_check_in
  THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_INVALID_STAY_DATES';
  END IF;

  IF EXTRACT(year FROM p_check_in)
    <> EXTRACT(year FROM p_check_out)
  THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_CROSS_YEAR_STAY_NOT_SUPPORTED';
  END IF;

  SELECT purchased_room_type_code
  INTO v_purchased_room_type_code
  FROM condon.owner_accounts
  WHERE id = p_owner_account_id
  FOR SHARE;

  IF NOT FOUND THEN
    RAISE EXCEPTION USING
      ERRCODE = 'P0002',
      MESSAGE = 'CONDON_OWNER_ACCOUNT_NOT_FOUND';
  END IF;

  v_multiplier := condon.calculate_multiplier(
    v_purchased_room_type_code,
    p_used_room_type_code,
    p_manual_multiplier
  );
  v_night_count := p_check_out - p_check_in;
  v_use_nights := v_night_count * v_multiplier;

  PERFORM pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtextextended(p_idempotency_key::text, 0)
  );

  SELECT *
  INTO v_existing
  FROM condon.usage_records
  WHERE idempotency_key = p_idempotency_key;

  IF FOUND THEN
    IF v_existing.owner_account_id IS DISTINCT FROM p_owner_account_id
      OR v_existing.confirmation_no IS DISTINCT FROM p_confirmation_no
      OR v_existing.check_in IS DISTINCT FROM p_check_in
      OR v_existing.check_out IS DISTINCT FROM p_check_out
      OR v_existing.used_room_type_code IS DISTINCT FROM p_used_room_type_code
      OR v_existing.applied_multiplier IS DISTINCT FROM v_multiplier
      OR v_existing.remark IS DISTINCT FROM COALESCE(p_remark, '')
    THEN
      RAISE EXCEPTION USING
        ERRCODE = '23505',
        MESSAGE = 'CONDON_IDEMPOTENCY_KEY_REUSED';
    END IF;
    RETURN v_existing;
  END IF;

  PERFORM pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtextextended('confirmation:' || p_confirmation_no, 0)
  );

  IF EXISTS (
    SELECT 1
    FROM condon.usage_records
    WHERE confirmation_no = p_confirmation_no
  ) THEN
    RAISE EXCEPTION USING
      ERRCODE = '23505',
      MESSAGE = 'CONDON_CONFIRMATION_NO_EXISTS';
  END IF;

  SELECT *
  INTO v_period
  FROM condon.entitlement_periods
  WHERE owner_account_id = p_owner_account_id
    AND period_year = EXTRACT(year FROM p_check_in)::smallint
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION USING
      ERRCODE = 'P0002',
      MESSAGE = 'CONDON_ENTITLEMENT_PERIOD_NOT_FOUND';
  END IF;

  IF p_check_in < v_period.period_start
    OR p_check_out > v_period.period_end
  THEN
    RAISE EXCEPTION USING
      ERRCODE = '22023',
      MESSAGE = 'CONDON_STAY_OUTSIDE_ENTITLEMENT_PERIOD';
  END IF;

  v_balance_before := v_period.current_balance;
  v_balance_after := v_balance_before - v_use_nights;

  IF v_balance_after < 0 THEN
    RAISE EXCEPTION USING
      ERRCODE = 'P0001',
      MESSAGE = 'CONDON_INSUFFICIENT_BALANCE';
  END IF;

  INSERT INTO condon.usage_records (
    id,
    owner_account_id,
    entitlement_period_id,
    confirmation_no,
    check_in,
    check_out,
    night_count,
    used_room_type_code,
    applied_multiplier,
    rule_version,
    use_nights,
    balance_before,
    balance_after,
    remark,
    idempotency_key,
    created_at
  )
  VALUES (
    p_usage_record_id,
    p_owner_account_id,
    v_period.id,
    p_confirmation_no,
    p_check_in,
    p_check_out,
    v_night_count,
    p_used_room_type_code,
    v_multiplier,
    'v1',
    v_use_nights,
    v_balance_before,
    v_balance_after,
    COALESCE(p_remark, ''),
    p_idempotency_key,
    v_now
  )
  RETURNING * INTO v_usage;

  INSERT INTO condon.entitlement_ledger (
    id,
    entitlement_period_id,
    usage_record_id,
    entry_type,
    delta_nights,
    balance_before,
    balance_after,
    occurred_at
  )
  VALUES (
    p_ledger_id,
    v_period.id,
    p_usage_record_id,
    'usage',
    -v_use_nights,
    v_balance_before,
    v_balance_after,
    v_now
  );

  UPDATE condon.entitlement_periods
  SET
    current_balance = v_balance_after,
    row_version = row_version + 1,
    updated_at = v_now
  WHERE id = v_period.id
    AND row_version = v_period.row_version;

  GET DIAGNOSTICS v_updated_count = ROW_COUNT;
  IF v_updated_count <> 1 THEN
    RAISE EXCEPTION USING
      ERRCODE = '40001',
      MESSAGE = 'CONDON_BALANCE_CONCURRENTLY_CHANGED';
  END IF;

  RETURN v_usage;
END;
$function$;

REVOKE ALL ON FUNCTION condon.calculate_multiplier(
  varchar,
  varchar,
  smallint
) FROM PUBLIC;
REVOKE ALL ON FUNCTION condon.open_entitlement_period(
  uuid,
  uuid,
  smallint,
  uuid,
  uuid
) FROM PUBLIC;
REVOKE ALL ON FUNCTION condon.create_usage_record(
  uuid,
  uuid,
  uuid,
  varchar,
  date,
  date,
  varchar,
  smallint,
  text,
  uuid
) FROM PUBLIC;
