-- v12.0 integrated hardening delta. Prior migrations remain immutable.

-- Rename ambiguous columns while preserving existing data.
do $$
begin
    if exists (
        select 1 from information_schema.columns
        where table_schema = 'signal_engine'
          and table_name = 'signal_decision'
          and column_name = 'lot_id'
    ) and not exists (
        select 1 from information_schema.columns
        where table_schema = 'signal_engine'
          and table_name = 'signal_decision'
          and column_name = 'position_lot_id'
    ) then
        alter table signal_engine.signal_decision
            rename column lot_id to position_lot_id;
    end if;

    if exists (
        select 1 from information_schema.columns
        where table_schema = 'signal_engine'
          and table_name = 'signal_decision'
          and column_name = 'sell_fraction'
    ) and not exists (
        select 1 from information_schema.columns
        where table_schema = 'signal_engine'
          and table_name = 'signal_decision'
          and column_name = 'sell_ratio_of_lot'
    ) then
        alter table signal_engine.signal_decision
            rename column sell_fraction to sell_ratio_of_lot;
    end if;
end;
$$;

alter table signal_engine.signal_decision
    add column if not exists target_portfolio_weight_after numeric(12,8),
    add column if not exists dataset_id text,
    add column if not exists model_version text,
    add column if not exists config_version text,
    add column if not exists code_sha text,
    add column if not exists idempotency_key text,
    add column if not exists correlation_id text;

update signal_engine.signal_decision d
set dataset_id = coalesce(d.dataset_id, e.dataset_id),
    model_version = coalesce(d.model_version, e.model_version),
    config_version = coalesce(d.config_version, e.config_version),
    code_sha = coalesce(d.code_sha, 'LEGACY_UNKNOWN'),
    target_portfolio_weight_after = coalesce(d.target_portfolio_weight_after, 0),
    idempotency_key = coalesce(d.idempotency_key, 'legacy:' || d.decision_id::text),
    correlation_id = coalesce(d.correlation_id, 'legacy:' || d.decision_id::text)
from signal_engine.evidence_snapshot e
where e.evidence_id = d.evidence_id;

alter table signal_engine.signal_decision
    alter column target_portfolio_weight_after set not null,
    alter column dataset_id set not null,
    alter column model_version set not null,
    alter column config_version set not null,
    alter column code_sha set not null,
    alter column idempotency_key set not null,
    alter column correlation_id set not null;

alter table signal_engine.signal_decision
    drop constraint if exists ck_signal_decision_sell_ratio_of_lot,
    add constraint ck_signal_decision_sell_ratio_of_lot
        check (sell_ratio_of_lot between 0 and 1),
    drop constraint if exists ck_signal_decision_target_weight,
    add constraint ck_signal_decision_target_weight
        check (target_portfolio_weight_after between 0 and 1);

create unique index if not exists ux_signal_decision_idempotency
    on signal_engine.signal_decision (idempotency_key);

create index if not exists ix_signal_decision_lot_created
    on signal_engine.signal_decision (position_lot_id, created_at desc);

-- Rebuildable, versioned read model. It is populated only by approved upstream slices.
create table if not exists signal_engine.sell_decision_context (
    context_id uuid primary key,
    position_lot_id uuid not null,
    cycle_id uuid not null,
    evidence_id text not null references signal_engine.evidence_snapshot(evidence_id),
    dataset_id text not null,
    model_version text not null,
    config_version text not null,
    code_sha text not null,
    as_of timestamptz not null,
    published_at_cutoff timestamptz not null,
    current_portfolio_weight numeric(12,8) not null
        check (current_portfolio_weight between 0 and 1),
    strategic_core_floor_weight numeric(12,8) not null
        check (strategic_core_floor_weight between 0 and current_portfolio_weight),
    hard_impairment_approved boolean not null,
    capital_floor_breached boolean not null,
    survival_sell_ratio_of_lot numeric(12,8) not null
        check (survival_sell_ratio_of_lot between 0 and 1),
    gap_below_floor_atr numeric(18,8) not null check (gap_below_floor_atr >= 0),
    consecutive_close_breaches integer not null check (consecutive_close_breaches >= 0),
    cooldown_satisfied boolean not null,
    concentration_sell_ratio_of_lot numeric(12,8) not null
        check (concentration_sell_ratio_of_lot between 0 and 1),
    opportunity_edge_lower_bound numeric(18,8) not null,
    opportunity_sell_ratio_of_lot numeric(12,8) not null
        check (opportunity_sell_ratio_of_lot between 0 and 1),
    quality_status text not null check (quality_status in ('PASS', 'WARN', 'QUARANTINED')),
    source_watermark text not null,
    projection_version integer not null,
    content_hash text not null unique,
    created_at timestamptz not null default now(),
    unique (position_lot_id, as_of, projection_version)
);

create index if not exists ix_sell_decision_context_lookup
    on signal_engine.sell_decision_context (position_lot_id, as_of desc)
    where quality_status = 'PASS';

drop trigger if exists sell_decision_context_immutable on signal_engine.sell_decision_context;
create trigger sell_decision_context_immutable
before update or delete on signal_engine.sell_decision_context
for each row execute function signal_engine.prevent_immutable_change();

-- Audit rows are append-only. Corrections are represented as new events.
create table if not exists signal_engine.decision_audit_event (
    audit_event_id uuid primary key,
    decision_id uuid not null references signal_engine.signal_decision(decision_id),
    event_type text not null,
    actor_id text not null,
    correlation_id text not null,
    payload_json jsonb not null,
    payload_hash text not null,
    occurred_at timestamptz not null,
    unique (decision_id, event_type, payload_hash)
);

drop trigger if exists decision_audit_event_immutable on signal_engine.decision_audit_event;
create trigger decision_audit_event_immutable
before update or delete on signal_engine.decision_audit_event
for each row execute function signal_engine.prevent_immutable_change();
