-- =============================================================================
-- 002_fixtures.sql
--
--   ############################################################
--   ##  FIXTURE DATA.  NOT REAL PLANT HISTORY.  DO NOT QUOTE.  ##
--   ############################################################
--
-- Interim stand-in for `imh` (yau-sls-poc-imh), which is not built yet.
-- Everything in the `fixture` schema is generated. A number produced from these
-- tables is a test result about the pipeline, never a fact about the station.
--
-- WHY THIS EXISTS: Phase 4 is the only true blocker in the build. Cube, the
-- contracts, the agent and the UI can all be built and tested against fixtures;
-- swapping to imh then becomes a connection-string change plus a re-verify of
-- the Phase 5 gate.
--
-- THE COLUMN NAMES BELOW ARE ASSUMED, NOT AGREED. Section 10 of
-- BUILD-AI-CONTAINERS.md must be updated with the real imh schema as soon as
-- the owner confirms it, and this file changed to match — the point of the
-- fixtures is that the shape is right, so a wrong shape is worse than useless.
--
-- Still to agree with whoever builds imh (do this now, not at Phase 4):
--   * the read-only login name, and how the password reaches you
--   * the alarm / process value / operation table names and key columns
--   * whether timestamps are UTC or local, and DST behaviour
--   * whether an `operations` concept exists at all, or must be derived
--   * an NSG rule allowing lin001 -> imh on 1433 only
--
-- HOW IT IS SWITCHED OFF: USE_FIXTURES=true in ~/ai/api.env points Cube here.
-- Set it false and repoint CUBEJS_DB_* at imh. Every Cube model file carries a
-- fixture/real note at the top; check them all when you flip it.
--
-- Every fixture table carries an is_fixture column, defaulted TRUE, so a row
-- that reaches the UI can be traced back to here. The API surfaces it as a
-- banner. Do not remove it as tidying-up.
-- =============================================================================

DROP SCHEMA IF EXISTS fixture CASCADE;
CREATE SCHEMA fixture;
COMMENT ON SCHEMA fixture IS
  'GENERATED FIXTURE DATA standing in for imh. Not real plant history. See db/002_fixtures.sql.';

-- -----------------------------------------------------------------------------
-- fixture.alarm_history — one row per alarm state transition.
--
-- Definition used here, and it must match whatever imh turns out to do:
-- an ALARM is a transition INTO the active state. A row with state 'RTN' is the
-- return to normal for the preceding activation, not a second alarm. Counting
-- every row doubles every answer, which is the single most likely way the
-- "how many times last week" question returns a wrong number confidently.
-- -----------------------------------------------------------------------------
CREATE TABLE fixture.alarm_history (
    alarm_id      BIGSERIAL PRIMARY KEY,
    event_time    TIMESTAMPTZ NOT NULL,     -- UTC. Cube converts, once.
    tag_id        TEXT        NOT NULL,
    equipment_id  TEXT,
    alarm_type    TEXT        NOT NULL,     -- HIGH_LEVEL|HIGH_HIGH_LEVEL|SPILL|
                                            -- PUMP_TRIP|SEAL_LEAK|HIGH_VIBRATION|
                                            -- LEVEL_SIGNAL_FAULT|MAINS_FAILURE|
                                            -- SETPOINT_REJECTED|LOW_LOW_LEVEL
    priority      INT,                      -- 1 highest .. 3 lowest
    state         TEXT        NOT NULL,     -- ACTIVE | RTN | ACK
    value         DOUBLE PRECISION,         -- process value at transition
    engineering_unit TEXT,
    description   TEXT,
    is_fixture    BOOLEAN     NOT NULL DEFAULT TRUE
);
CREATE INDEX ON fixture.alarm_history (event_time);
CREATE INDEX ON fixture.alarm_history (tag_id, event_time);
CREATE INDEX ON fixture.alarm_history (equipment_id, event_time);

-- -----------------------------------------------------------------------------
-- fixture.process_value_history — sampled analogue history.
--
-- 1-minute samples. Real CI Server historisation is deadband-compressed, so the
-- real table will be irregular; anything in Cube that assumes an even sample
-- interval will be wrong against imh. Time-weight the averages, do not mean the
-- rows. This fixture is deliberately regular so that the difference shows up as
-- a behaviour change when imh is connected, rather than hiding.
-- -----------------------------------------------------------------------------
CREATE TABLE fixture.process_value_history (
    sample_time      TIMESTAMPTZ NOT NULL,  -- UTC
    tag_id           TEXT        NOT NULL,
    equipment_id     TEXT,
    value            DOUBLE PRECISION,
    engineering_unit TEXT,
    quality          TEXT DEFAULT 'GOOD',   -- GOOD | BAD | UNCERTAIN
    is_fixture       BOOLEAN NOT NULL DEFAULT TRUE,
    PRIMARY KEY (tag_id, sample_time)
);
CREATE INDEX ON fixture.process_value_history (sample_time);

-- -----------------------------------------------------------------------------
-- fixture.operation_history — pump-down operations ("fills" inverted).
--
-- WRPS is a pump station, not a tank farm: the operation of interest is a
-- PUMP-DOWN — the well fills on inflow, pumps start at the duty level, the
-- level is drawn back to the stop level. One row per pump-down.
--
-- If imh has no equivalent, derive it in Cube from a monotonic level fall while
-- pumps_running > 0, and document the heuristic in cube/model/operations.yml.
-- Keep the heuristic simple; a clever one nobody can explain is not evidence.
-- -----------------------------------------------------------------------------
CREATE TABLE fixture.operation_history (
    operation_id     BIGSERIAL PRIMARY KEY,
    equipment_id     TEXT NOT NULL,         -- STN-001
    operation_type   TEXT NOT NULL DEFAULT 'PUMP_DOWN',
    start_time       TIMESTAMPTZ NOT NULL,  -- UTC
    end_time         TIMESTAMPTZ,
    start_level_pct  DOUBLE PRECISION,
    end_level_pct    DOUBLE PRECISION,
    max_level_pct    DOUBLE PRECISION,
    avg_inflow_m3h   DOUBLE PRECISION,
    avg_discharge_m3h DOUBLE PRECISION,
    peak_pumps_running INT,
    duty_pump        TEXT,                  -- PU-301 | PU-302 | PU-303
    high_level_alarm BOOLEAN DEFAULT FALSE, -- did it reach the HLA setpoint
    spill            BOOLEAN DEFAULT FALSE, -- did it go over the weir
    is_fixture       BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE INDEX ON fixture.operation_history (start_time);
CREATE INDEX ON fixture.operation_history (equipment_id, start_time);

-- =============================================================================
-- Generated rows. 30 days ending at the load time, UTC.
--
-- The pattern is deliberately boring and explainable: a diurnal inflow with a
-- morning and evening peak, a wet-weather week in the middle, pump-downs every
-- couple of hours, and a small number of alarms clustered in the wet week. It
-- is enough to exercise every question class and no more. Do not add realism
-- here — realism in fixtures is how a fixture number ends up in a slide.
-- =============================================================================

-- --- process values: 1-minute wet well level and flows, 30 days -------------
INSERT INTO fixture.process_value_history
    (sample_time, tag_id, equipment_id, value, engineering_unit)
SELECT
    ts,
    'PS_STN_WET_WELL_LEVEL',
    'WW-101',
    -- sawtooth between the stop-all level and roughly the duty start level,
    -- riding on the diurnal inflow, pushed higher during the wet week.
    round((
        30.0
        + 25.0 * abs(((extract(epoch FROM ts)::int / 60) % 140)::numeric / 140.0 - 0.5) * 2
        + 12.0 * sin(extract(epoch FROM ts) / 13750.0)
        + CASE WHEN ts BETWEEN now() - interval '18 days'
                          AND now() - interval '11 days' THEN 22.0 ELSE 0.0 END
    )::numeric, 1)::double precision,
    '%'
FROM generate_series(now() - interval '30 days', now(), interval '1 minute') AS ts;

INSERT INTO fixture.process_value_history
    (sample_time, tag_id, equipment_id, value, engineering_unit)
SELECT
    ts,
    'PS_STN_INFLOW',
    'STN-001',
    round((
        180.0
        + 90.0 * sin((extract(epoch FROM ts) - 21600) * 2 * pi() / 86400.0)
        + 40.0 * sin((extract(epoch FROM ts)) * 4 * pi() / 86400.0)
        + CASE WHEN ts BETWEEN now() - interval '18 days'
                          AND now() - interval '11 days' THEN 420.0 ELSE 0.0 END
    )::numeric, 1)::double precision,
    'm3/h'
FROM generate_series(now() - interval '30 days', now(), interval '1 minute') AS ts;

INSERT INTO fixture.process_value_history
    (sample_time, tag_id, equipment_id, value, engineering_unit)
SELECT
    ts,
    'PS_STN_TOTAL_DISCHARGE_FLOW',
    'STN-001',
    CASE WHEN ((extract(epoch FROM ts)::int / 60) % 140) < 55
         THEN 0.0
         ELSE round((432.0 + 30.0 * sin(extract(epoch FROM ts) / 900.0))::numeric, 1)::double precision
    END,
    'm3/h'
FROM generate_series(now() - interval '30 days', now(), interval '1 minute') AS ts;

-- --- pump-down operations: roughly every 140 minutes for 30 days ------------
INSERT INTO fixture.operation_history
    (equipment_id, operation_type, start_time, end_time,
     start_level_pct, end_level_pct, max_level_pct,
     avg_inflow_m3h, avg_discharge_m3h, peak_pumps_running, duty_pump,
     high_level_alarm, spill)
SELECT
    'STN-001',
    'PUMP_DOWN',
    st,
    st + make_interval(mins => 35 + (n % 12)),
    66.7,
    16.7,
    max_lvl,
    inflow,
    CASE WHEN max_lvl > 83.3 THEN 1296.0 WHEN max_lvl > 75.0 THEN 864.0 ELSE 432.0 END,
    CASE WHEN max_lvl > 83.3 THEN 3      WHEN max_lvl > 75.0 THEN 2     ELSE 1     END,
    -- duty rotates on lowest run hours; over a long window that is round-robin
    'PU-30' || (1 + (n % 3))::text,
    max_lvl >= 86.7,
    max_lvl >= 100.0
FROM (
    SELECT
        n,
        now() - interval '30 days' + make_interval(mins => n * 140) AS st,
        CASE WHEN now() - interval '30 days' + make_interval(mins => n * 140)
                  BETWEEN now() - interval '18 days' AND now() - interval '11 days'
             THEN 84.0 + (n % 7) * 2.9          -- wet week: reaches HLA, sometimes spills
             ELSE 68.0 + (n % 5) * 1.4          -- normal: comfortably below HLA
        END AS max_lvl,
        CASE WHEN now() - interval '30 days' + make_interval(mins => n * 140)
                  BETWEEN now() - interval '18 days' AND now() - interval '11 days'
             THEN 620.0 ELSE 205.0
        END AS inflow
    FROM generate_series(0, (30 * 24 * 60) / 140) AS n
) s
WHERE st < now();

-- --- alarms: derived from the operations above, so they stay consistent -----
-- One ACTIVE row per alarm, one RTN row per activation. Counting must count
-- ACTIVE only.
INSERT INTO fixture.alarm_history
    (event_time, tag_id, equipment_id, alarm_type, priority, state, value,
     engineering_unit, description)
SELECT
    o.start_time + interval '20 minutes',
    'PS_STN_HIGH_LEVEL_ALARM',
    'WW-101',
    'HIGH_LEVEL',
    2,
    'ACTIVE',
    o.max_level_pct,
    '%',
    'Wet well level above the high level alarm setpoint'
FROM fixture.operation_history o
WHERE o.high_level_alarm;

INSERT INTO fixture.alarm_history
    (event_time, tag_id, equipment_id, alarm_type, priority, state, value,
     engineering_unit, description)
SELECT
    o.start_time + interval '32 minutes',
    'PS_STN_HIGH_LEVEL_ALARM',
    'WW-101',
    'HIGH_LEVEL',
    2,
    'RTN',
    70.0,
    '%',
    'Wet well level returned below the high level alarm setpoint'
FROM fixture.operation_history o
WHERE o.high_level_alarm;

INSERT INTO fixture.alarm_history
    (event_time, tag_id, equipment_id, alarm_type, priority, state, value,
     engineering_unit, description)
SELECT
    o.start_time + interval '25 minutes',
    'PS_STN_SPILL_ACTIVE',
    'WEIR-105',
    'SPILL',
    1,
    'ACTIVE',
    o.max_level_pct,
    '%',
    'Spill over the weir - environmental reportable event'
FROM fixture.operation_history o
WHERE o.spill;

-- A handful of pump trips, one per week, so trip questions have something to
-- find and the duty-promotion story is visible.
INSERT INTO fixture.alarm_history
    (event_time, tag_id, equipment_id, alarm_type, priority, state, value,
     engineering_unit, description)
VALUES
    (now() - interval '26 days 4 hours',  'PS_PU302_TRIPPED', 'PU-302', 'PUMP_TRIP',      1, 'ACTIVE', 1, NULL, 'PU-302 tripped - no flow 20 s after start (PIT-321 below 150 kPa)'),
    (now() - interval '26 days 1 hour',   'PS_PU302_TRIPPED', 'PU-302', 'PUMP_TRIP',      1, 'RTN',    0, NULL, 'PU-302 trip reset by operator command'),
    (now() - interval '19 days 9 hours',  'PS_PU301_TRIPPED', 'PU-301', 'PUMP_TRIP',      1, 'ACTIVE', 1, NULL, 'PU-301 tripped - motor thermal TE-312'),
    (now() - interval '19 days 2 hours',  'PS_PU301_TRIPPED', 'PU-301', 'PUMP_TRIP',      1, 'RTN',    0, NULL, 'PU-301 trip reset by operator command'),
    (now() - interval '14 days 6 hours',  'PS_STN_ALARM_BITMASK', 'PU-303', 'SEAL_LEAK',  3, 'ACTIVE', 1, NULL, 'PU-303 seal leak MSE-333 - alarm only, unit remains available'),
    (now() - interval '12 days 3 hours',  'PS_STN_ALARM_BITMASK', 'PU-301', 'HIGH_VIBRATION', 2, 'ACTIVE', 8.4, 'mm/s', 'PU-301 bearing vibration above 7.1 mm/s alarm threshold'),
    (now() - interval '12 days 1 hour',   'PS_STN_ALARM_BITMASK', 'PU-301', 'HIGH_VIBRATION', 2, 'RTN',    6.2, 'mm/s', 'PU-301 bearing vibration returned below threshold'),
    (now() - interval '6 days 11 hours',  'PS_STN_ALARM_BITMASK', 'WW-101', 'LEVEL_SIGNAL_FAULT', 1, 'ACTIVE', NULL, NULL, 'LIT-101 frozen - no change greater than 1 mm for 10 minutes with a pump running'),
    (now() - interval '6 days 10 hours',  'PS_STN_ALARM_BITMASK', 'WW-101', 'LEVEL_SIGNAL_FAULT', 1, 'RTN',    NULL, NULL, 'LIT-101 signal restored'),
    (now() - interval '3 days 5 hours',   'PS_STN_ALARM_BITMASK', 'STN-001', 'SETPOINT_REJECTED', 3, 'ACTIVE', 6500, 'mm', 'Setpoint write rejected - start duty level above the spill weir; previous value retained');

-- =============================================================================
-- Grants. These live HERE and not in 003_roles.sql on purpose: this file starts
-- with DROP SCHEMA fixture CASCADE, which destroys every grant on it. Grants
-- belong with the object they are granted on, so a fixture reload keeps them.
-- Read-only for both roles - nothing writes fixtures except this file.
-- =============================================================================

GRANT USAGE ON SCHEMA fixture TO agent_ro, cube_rw;
GRANT SELECT ON ALL TABLES IN SCHEMA fixture TO agent_ro, cube_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA fixture GRANT SELECT ON TABLES TO agent_ro, cube_rw;

-- =============================================================================
-- Sanity check after loading. Expect roughly: 30 days of 1-minute samples on
-- three tags, ~300 pump-downs, and alarms concentrated in the wet week.
--
--   SELECT count(*) FROM fixture.process_value_history;
--   SELECT count(*) FROM fixture.operation_history;
--   SELECT alarm_type, state, count(*)
--     FROM fixture.alarm_history GROUP BY 1,2 ORDER BY 1,2;
--
-- And the check that matters: everything here answers TRUE.
--
--   SELECT bool_and(is_fixture) FROM fixture.alarm_history;
-- =============================================================================
