-- ARR daily PRICE_UNMATCHED manual-price review.
-- Apply only after the operator has confirmed/applied migration 016 in the same
-- release window. This migration is additive and records no guest information
-- in review tables beyond the normalized pricing key and aggregate impact.

BEGIN;

DO $$
BEGIN
    IF current_database() <> 'booking_test' THEN
        RAISE EXCEPTION 'ARR daily price review migration is allowed only in booking_test';
    END IF;
    IF to_regclass('ingestion.processing_runs') IS NULL
       OR to_regclass('ingestion.processing_attempts') IS NULL
       OR to_regclass('ingestion.processing_deliveries') IS NULL
       OR to_regclass('finance.daily_versions') IS NULL
       OR to_regclass('finance.daily_records') IS NULL
       OR to_regprocedure('reporting.validate_monthly_run_publication()') IS NULL THEN
        RAISE EXCEPTION 'ARR migrations 008 through 016 must be applied before 017';
    END IF;
    IF to_regclass('ingestion.daily_review_cases') IS NOT NULL THEN
        RAISE EXCEPTION 'ARR daily price review migration is already applied';
    END IF;
END;
$$;

-- New lifecycle values. Historical constraints are replaced verbatim rather
-- than modifying migrations 008/010.
ALTER TABLE ingestion.processing_runs
    DROP CONSTRAINT IF EXISTS processing_runs_run_status_check,
    DROP CONSTRAINT IF EXISTS processing_runs_terminal_shape;

ALTER TABLE ingestion.processing_runs
    ADD CONSTRAINT processing_runs_run_status_check CHECK (run_status IN (
        'received', 'queued', 'running', 'validating', 'awaiting_review',
        'accepted', 'rejected', 'failed', 'cancelled'
    )),
    ADD CONSTRAINT processing_runs_terminal_shape CHECK (
        (
            run_status = 'accepted'
            AND failure_code IS NULL
            AND validated_at IS NOT NULL
            AND finished_at IS NOT NULL
            AND (
                pipeline_type <> 'opera_daily'
                OR (
                    business_date IS NOT NULL
                    AND delivered_processor_version IS NOT NULL
                    AND delivered_rule_set_sha256 IS NOT NULL
                    AND result_schema_version IS NOT NULL
                    AND delivery_sha256 IS NOT NULL
                    AND (
                        (
                            result_delivery_mode = 'artifact_callback'
                            AND result_artifact_id IS NOT NULL
                        )
                        OR (
                            result_delivery_mode = 'direct_mcp'
                            AND result_artifact_id IS NULL
                        )
                    )
                )
            )
        )
        OR (
            run_status IN ('rejected', 'failed')
            AND failure_code IS NOT NULL
            AND finished_at IS NOT NULL
        )
        OR (run_status = 'cancelled' AND finished_at IS NOT NULL)
        OR (
            run_status = 'awaiting_review'
            AND pipeline_type = 'opera_daily'
            AND result_delivery_mode = 'artifact_callback'
            AND result_artifact_id IS NOT NULL
            AND business_date IS NOT NULL
            AND delivered_processor_version IS NOT NULL
            AND delivered_rule_set_sha256 IS NOT NULL
            AND result_schema_version = '4.0'
            AND delivery_sha256 IS NOT NULL
            AND validated_at IS NOT NULL
            AND finished_at IS NULL
        )
        OR run_status IN ('received', 'queued', 'running', 'validating')
    );

ALTER TABLE ingestion.processing_attempts
    DROP CONSTRAINT IF EXISTS processing_attempts_attempt_status_check,
    DROP CONSTRAINT IF EXISTS processing_attempts_terminal_shape;

ALTER TABLE ingestion.processing_attempts
    ADD CONSTRAINT processing_attempts_attempt_status_check CHECK (attempt_status IN (
        'queued', 'dispatched', 'running', 'delivered', 'review_required',
        'succeeded', 'failed', 'cancelled'
    )),
    ADD CONSTRAINT processing_attempts_terminal_shape CHECK (
        (
            attempt_status = 'succeeded'
            AND failure_code IS NULL
            AND finished_at IS NOT NULL
        )
        OR (
            attempt_status = 'failed'
            AND failure_code IS NOT NULL
            AND finished_at IS NOT NULL
        )
        OR (
            attempt_status = 'review_required'
            AND failure_code IS NULL
            AND finished_at IS NOT NULL
        )
        OR (attempt_status = 'cancelled' AND finished_at IS NOT NULL)
        OR attempt_status IN ('queued', 'dispatched', 'running', 'delivered')
    );

ALTER TABLE ingestion.processing_deliveries
    ADD COLUMN manual_override_artifact_id bigint REFERENCES ingestion.artifacts(id),
    ADD COLUMN review_case_id bigint,
    DROP CONSTRAINT IF EXISTS processing_deliveries_delivery_status_check,
    DROP CONSTRAINT IF EXISTS processing_deliveries_result_status_check;

ALTER TABLE ingestion.processing_deliveries
    ADD CONSTRAINT processing_deliveries_delivery_status_check CHECK (delivery_status IN (
        'received', 'validating', 'committed', 'recorded_failure',
        'recorded_review', 'rejected'
    )),
    ADD CONSTRAINT processing_deliveries_result_status_check CHECK (result_status IN (
        'success', 'review_required', 'failed'
    ));

CREATE TABLE ingestion.daily_review_cases (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    case_key text NOT NULL UNIQUE CHECK (
        case_key ~ '^dailyreview-[0-9a-f]{32}$'
    ),
    processing_run_id bigint NOT NULL UNIQUE
        REFERENCES ingestion.processing_runs(id),
    initial_delivery_id bigint NOT NULL UNIQUE
        REFERENCES ingestion.processing_deliveries(id),
    business_date date NOT NULL,
    source_sha256 character(64) NOT NULL CHECK (
        source_sha256 ~ '^[0-9a-f]{64}$'
    ),
    processor_version text NOT NULL CHECK (btrim(processor_version) <> ''),
    rule_set_sha256 character(64) NOT NULL CHECK (
        rule_set_sha256 ~ '^[0-9a-f]{64}$'
    ),
    review_version text NOT NULL CHECK (review_version = '1.0'),
    case_status text NOT NULL CHECK (case_status IN (
        'open', 'processing', 'completed', 'generation_failed', 'failed', 'cancelled'
    )),
    revision integer NOT NULL DEFAULT 0 CHECK (revision >= 0),
    manual_override_json jsonb CHECK (
        manual_override_json IS NULL OR jsonb_typeof(manual_override_json) = 'object'
    ),
    manual_override_sha256 character(64) CHECK (
        manual_override_sha256 IS NULL OR manual_override_sha256 ~ '^[0-9a-f]{64}$'
    ),
    manual_override_artifact_id bigint REFERENCES ingestion.artifacts(id),
    frozen_at timestamptz,
    failure_code text,
    failure_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    completed_at timestamptz,
    cancelled_at timestamptz,
    CONSTRAINT daily_review_cases_frozen_shape CHECK (
        (frozen_at IS NULL AND manual_override_json IS NULL
            AND manual_override_sha256 IS NULL AND manual_override_artifact_id IS NULL)
        OR (frozen_at IS NOT NULL AND manual_override_json IS NOT NULL
            AND manual_override_sha256 IS NOT NULL)
    ),
    CONSTRAINT daily_review_cases_terminal_shape CHECK (
        (case_status = 'completed' AND completed_at IS NOT NULL AND frozen_at IS NOT NULL)
        OR (case_status = 'cancelled' AND cancelled_at IS NOT NULL)
        OR case_status IN ('open', 'processing', 'generation_failed', 'failed')
    )
);

CREATE INDEX daily_review_cases_open_idx
    ON ingestion.daily_review_cases (case_status, updated_at)
    WHERE case_status IN ('open', 'generation_failed', 'processing');

CREATE TABLE ingestion.daily_review_items (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    review_case_id bigint NOT NULL
        REFERENCES ingestion.daily_review_cases(id),
    company_key text NOT NULL CHECK (btrim(company_key) <> ''),
    rate_code text NOT NULL CHECK (
        rate_code = upper(btrim(rate_code))
        AND rate_code ~ '^[A-Z0-9]+$'
    ),
    effective_rate_amount numeric(18,2) NOT NULL CHECK (effective_rate_amount >= 0),
    candidate_prices jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (
        jsonb_typeof(candidate_prices) = 'array'
    ),
    affected_records integer NOT NULL CHECK (affected_records > 0),
    affected_rooms integer NOT NULL CHECK (affected_rooms > 0),
    affected_room_nights integer NOT NULL CHECK (affected_room_nights >= 0),
    real_price numeric(18,2) CHECK (real_price >= 0),
    revision integer NOT NULL DEFAULT 0 CHECK (revision >= 0),
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT daily_review_items_key_unique
        UNIQUE (review_case_id, company_key, rate_code, effective_rate_amount)
);

CREATE INDEX daily_review_items_case_idx
    ON ingestion.daily_review_items (review_case_id, id);

ALTER TABLE ingestion.daily_review_items
    ADD CONSTRAINT daily_review_items_id_case_unique UNIQUE (id, review_case_id);

CREATE TABLE ingestion.daily_review_events (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    review_case_id bigint NOT NULL
        REFERENCES ingestion.daily_review_cases(id),
    review_item_id bigint REFERENCES ingestion.daily_review_items(id),
    event_type text NOT NULL CHECK (event_type IN (
        'PRICE_REVIEW_REQUIRED', 'PRICE_REVIEW_UPDATED', 'PRICE_REVIEW_FINALIZED',
        'PRICE_REVIEW_CANCELLED', 'PRICE_REVIEW_GENERATION_FAILED', 'PRICE_REVIEW_FAILED'
    )),
    actor_username text NOT NULL CHECK (btrim(actor_username) <> ''),
    previous_real_price numeric(18,2) CHECK (previous_real_price >= 0),
    new_real_price numeric(18,2) CHECK (new_real_price >= 0),
    revision integer NOT NULL CHECK (revision >= 0),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT daily_review_events_item_case_fk
        FOREIGN KEY (review_item_id, review_case_id)
        REFERENCES ingestion.daily_review_items(id, review_case_id)
    );

ALTER TABLE ingestion.processing_deliveries
    ADD CONSTRAINT processing_deliveries_review_case_fk
        FOREIGN KEY (review_case_id) REFERENCES ingestion.daily_review_cases(id);

CREATE OR REPLACE FUNCTION ingestion.enforce_daily_review_item_mutability()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
    locked_case_status text;
    locked_case_id bigint;
BEGIN
    locked_case_id := COALESCE(NEW.review_case_id, OLD.review_case_id);
    SELECT case_status INTO locked_case_status
    FROM ingestion.daily_review_cases
    WHERE id = locked_case_id
    FOR KEY SHARE;
    IF locked_case_status IS NULL THEN
        RAISE EXCEPTION 'daily review case is unavailable';
    END IF;
    IF locked_case_status <> 'open' THEN
        RAISE EXCEPTION 'daily review items are immutable after final confirmation';
    END IF;
    IF TG_OP = 'UPDATE' AND (
        NEW.review_case_id <> OLD.review_case_id
        OR NEW.company_key <> OLD.company_key
        OR NEW.rate_code <> OLD.rate_code
        OR NEW.effective_rate_amount <> OLD.effective_rate_amount
        OR NEW.candidate_prices <> OLD.candidate_prices
        OR NEW.affected_records <> OLD.affected_records
        OR NEW.affected_rooms <> OLD.affected_rooms
        OR NEW.affected_room_nights <> OLD.affected_room_nights
    ) THEN
        RAISE EXCEPTION 'daily review pricing key and impact are immutable';
    END IF;
    IF TG_OP = 'DELETE' THEN
        RETURN OLD;
    END IF;
    RETURN NEW;
END;
$$;

CREATE TRIGGER daily_review_items_mutability_guard
BEFORE UPDATE OR DELETE ON ingestion.daily_review_items
FOR EACH ROW EXECUTE FUNCTION ingestion.enforce_daily_review_item_mutability();

CREATE OR REPLACE FUNCTION ingestion.enforce_daily_review_case_immutability()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    IF TG_OP = 'DELETE' THEN
        RAISE EXCEPTION 'daily review cases are permanent audit records';
    END IF;
    IF OLD.frozen_at IS NOT NULL AND (
        NEW.manual_override_json IS DISTINCT FROM OLD.manual_override_json
        OR NEW.manual_override_sha256 IS DISTINCT FROM OLD.manual_override_sha256
        OR (
            OLD.manual_override_artifact_id IS NOT NULL
            AND NEW.manual_override_artifact_id IS DISTINCT FROM OLD.manual_override_artifact_id
        )
        OR NEW.source_sha256 IS DISTINCT FROM OLD.source_sha256
        OR NEW.business_date IS DISTINCT FROM OLD.business_date
        OR NEW.processor_version IS DISTINCT FROM OLD.processor_version
        OR NEW.rule_set_sha256 IS DISTINCT FROM OLD.rule_set_sha256
        OR NEW.review_version IS DISTINCT FROM OLD.review_version
    ) THEN
        RAISE EXCEPTION 'frozen manual override provenance is immutable';
    END IF;
    RETURN NEW;
END;
$$;

CREATE TRIGGER daily_review_cases_immutability_guard
BEFORE UPDATE OR DELETE ON ingestion.daily_review_cases
FOR EACH ROW EXECUTE FUNCTION ingestion.enforce_daily_review_case_immutability();

CREATE OR REPLACE FUNCTION ingestion.enforce_daily_review_event_immutability()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    RAISE EXCEPTION 'daily review events are append-only audit records';
END;
$$;

CREATE TRIGGER daily_review_events_immutability_guard
BEFORE UPDATE OR DELETE ON ingestion.daily_review_events
FOR EACH ROW EXECUTE FUNCTION ingestion.enforce_daily_review_event_immutability();

ALTER TABLE finance.daily_versions
    ADD COLUMN review_case_id bigint REFERENCES ingestion.daily_review_cases(id),
    ADD COLUMN manual_override_sha256 character(64) CHECK (
        manual_override_sha256 IS NULL OR manual_override_sha256 ~ '^[0-9a-f]{64}$'
    ),
    ADD COLUMN manually_priced_rows integer NOT NULL DEFAULT 0 CHECK (manually_priced_rows >= 0),
    ADD CONSTRAINT daily_versions_review_lineage_shape CHECK (
        (review_case_id IS NULL AND manual_override_sha256 IS NULL AND manually_priced_rows = 0)
        OR (review_case_id IS NOT NULL AND manual_override_sha256 IS NOT NULL AND manually_priced_rows > 0)
    );

CREATE UNIQUE INDEX daily_versions_review_case_unique
    ON finance.daily_versions (review_case_id)
    WHERE review_case_id IS NOT NULL;

DROP INDEX IF EXISTS finance.daily_versions_source_rule_unique;
CREATE UNIQUE INDEX daily_versions_source_rule_override_unique
    ON finance.daily_versions (
        source_artifact_id,
        business_date,
        processor_version,
        rule_set_sha256,
        COALESCE(manual_override_sha256, '')
    )
    WHERE business_date IS NOT NULL;

ALTER TABLE finance.daily_records
    DROP CONSTRAINT IF EXISTS daily_records_retained_values;

ALTER TABLE finance.daily_records
    ADD CONSTRAINT daily_records_retained_values CHECK (
        outcome <> 'retained'
        OR (
            block_code IS NOT NULL
            AND adults IS NOT NULL AND adults >= 0
            AND children IS NOT NULL AND children >= 0
            AND company_name IS NOT NULL AND btrim(company_name) <> ''
            AND company_key IS NOT NULL AND btrim(company_key) <> ''
            AND confirmation_no IS NOT NULL AND btrim(confirmation_no) <> ''
            AND disp_room_no IS NOT NULL AND btrim(disp_room_no) <> ''
            AND effective_rate_amount IS NOT NULL AND effective_rate_amount >= 0
            AND full_name IS NOT NULL
            AND no_of_rooms IS NOT NULL AND no_of_rooms > 0
            AND rate_code IS NOT NULL
            AND normalized_rate_code IS NOT NULL
            AND arrival IS NOT NULL
            AND departure IS NOT NULL
            AND nights IS NOT NULL AND nights >= 0
            AND real_price IS NOT NULL AND real_price >= 0
            AND total_price IS NOT NULL AND total_price >= 0
            AND channel_key IS NOT NULL AND btrim(channel_key) <> ''
            AND pricing_method IN (
                'price_reference_exact', 'zero_price_exception', 'manual_review'
            )
        )
    );

COMMENT ON TABLE ingestion.daily_review_cases IS
  'One immutable-audit manual price review case per XML processing run. No guest names, comments, traces or raw rows are stored here.';
COMMENT ON TABLE ingestion.daily_review_items IS
  'One staff-editable non-negative price per normalized missing fixed-price key; mutable only while its case is open.';
COMMENT ON TABLE ingestion.daily_review_events IS
  'Append-only actor, revision and before/after-price audit history. Intentionally has no notes field.';
COMMENT ON COLUMN finance.daily_versions.manual_override_sha256 IS
  'SHA-256 of the canonical frozen review manifest used for independently validated final replay.';

-- The application needs no DELETE privilege over review facts. Deployments that
-- use a differently named login role keep their existing grant model unchanged.
DO $$
BEGIN
    IF to_regrole('arr_app') IS NOT NULL THEN
        EXECUTE 'GRANT SELECT, INSERT, UPDATE ON ingestion.daily_review_cases, ingestion.daily_review_items TO arr_app';
        EXECUTE 'GRANT SELECT, INSERT ON ingestion.daily_review_events TO arr_app';
        EXECUTE 'GRANT USAGE, SELECT ON SEQUENCE ingestion.daily_review_cases_id_seq, ingestion.daily_review_items_id_seq, ingestion.daily_review_events_id_seq TO arr_app';
    END IF;
END;
$$;

COMMIT;
