-- =============================================================================
-- Migration 002: Audit Log Table
-- GDPR Art. 5(2) Accountability + Art. 30 Records of Processing Activities
--
-- Design decisions:
--   - UUID primary key (consistent with users table)
--   - user_id is nullable: pre-auth events (LOGIN failures) have no user yet
--   - old_value / new_value are JSONB: flexible, queryable, indexable
--   - ip_address stored as TEXT (supports IPv4 + IPv6); anonymised after 90 days
--   - Table is immutable: a PostgreSQL RULE blocks UPDATE and DELETE at the
--     SQL level, which is simpler than triggers and cannot be bypassed by the
--     application role
--   - Partial index on ip_address covers only recent rows (90-day window) to
--     support efficient anonymisation queries without a full-table scan
-- =============================================================================

-- -------------------------------------------------------
-- 1. Enums — define before the table
-- -------------------------------------------------------

DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_action') THEN
    CREATE TYPE audit_action AS ENUM (
      'CREATE',
      'UPDATE',
      'DELETE',
      'LOGIN',
      'LOGOUT',
      'EXPORT',
      'PERMISSION_DENIED',
      'PASSWORD_CHANGE',
      'ROLE_CHANGE'
    );
  END IF;
END
$$;

DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_resource_type') THEN
    CREATE TYPE audit_resource_type AS ENUM (
      'MEMBER',
      'INCIDENT',
      'VEHICLE',
      'EQUIPMENT',
      'QUALIFICATION',
      'USER',
      'SYSTEM'
    );
  END IF;
END
$$;

-- -------------------------------------------------------
-- 2. Core audit_log table
-- -------------------------------------------------------

CREATE TABLE IF NOT EXISTS audit_log (
  id              UUID        PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id         UUID        REFERENCES users(id) ON DELETE SET NULL,
  user_email      VARCHAR(255),              -- denormalised snapshot; users can be deleted
  action          audit_action    NOT NULL,
  resource_type   audit_resource_type NOT NULL,
  resource_id     VARCHAR(255),              -- may be UUID, numeric ID, or 'system'
  old_value       JSONB,                     -- state before the operation
  new_value       JSONB,                     -- state after the operation
  ip_address      TEXT,                      -- anonymised to '[anonymized]' after 90 days
  user_agent      TEXT,
  metadata        JSONB DEFAULT '{}',        -- any extra context (e.g. export format, reason)
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Prevent modification of audit records at the database level.
-- Using RULE rather than a trigger because rules are evaluated before
-- the operation and cannot be disabled without superuser access.
CREATE OR REPLACE RULE audit_log_no_update AS
  ON UPDATE TO audit_log DO INSTEAD NOTHING;

CREATE OR REPLACE RULE audit_log_no_delete AS
  ON DELETE TO audit_log DO INSTEAD NOTHING;

-- -------------------------------------------------------
-- 3. Indexes
-- -------------------------------------------------------

-- Lookup by actor (admin "what did this user do?")
CREATE INDEX IF NOT EXISTS idx_audit_log_user_id
  ON audit_log (user_id)
  WHERE user_id IS NOT NULL;

-- Lookup by subject resource (admin "what happened to member X?")
CREATE INDEX IF NOT EXISTS idx_audit_log_resource
  ON audit_log (resource_type, resource_id);

-- Time-range queries and retention scans (most common filter)
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at
  ON audit_log (created_at DESC);

-- Action-type filter
CREATE INDEX IF NOT EXISTS idx_audit_log_action
  ON audit_log (action);

-- Partial index: only rows where IP address is not yet anonymised.
-- The anonymisation job does: WHERE created_at < NOW() - INTERVAL '90 days'
-- AND ip_address != '[anonymized]'
-- This index makes that query O(matched rows) instead of O(table size).
CREATE INDEX IF NOT EXISTS idx_audit_log_ip_retention
  ON audit_log (created_at)
  WHERE ip_address IS NOT NULL
    AND ip_address != '[anonymized]';

-- -------------------------------------------------------
-- 4. OPTIONAL — Range partitioning by month (recommended
--    for high-volume deployments; skip in small setups)
--
-- To use partitioning, replace the CREATE TABLE above with
-- the following DDL *before* the first row is inserted.
-- Partitioning cannot be added to an existing unpartitioned
-- table without a full rewrite (pg_partman can automate this).
--
-- CREATE TABLE audit_log (
--   id            UUID        NOT NULL DEFAULT uuid_generate_v4(),
--   user_id       UUID        REFERENCES users(id) ON DELETE SET NULL,
--   user_email    VARCHAR(255),
--   action        audit_action    NOT NULL,
--   resource_type audit_resource_type NOT NULL,
--   resource_id   VARCHAR(255),
--   old_value     JSONB,
--   new_value     JSONB,
--   ip_address    TEXT,
--   user_agent    TEXT,
--   metadata      JSONB DEFAULT '{}',
--   created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
-- ) PARTITION BY RANGE (created_at);
--
-- -- Create monthly partitions with pg_partman or manually:
-- CREATE TABLE audit_log_2026_01 PARTITION OF audit_log
--   FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- CREATE TABLE audit_log_2026_02 PARTITION OF audit_log
--   FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- -- ... and so on.  pg_partman automates partition creation.
--
-- With partitioning, old partitions can be DROPped for
-- efficient bulk retention deletion without a full-table scan.
-- -------------------------------------------------------
