-- V1__init.sql — PostgreSQL schema for Cameleer Server
--
-- PostgreSQL stores RBAC, configuration, audit, runtime management,
-- outbound connections, and alerting. All observability data
-- (executions, metrics, diagrams, logs, stats) lives in ClickHouse.
--
-- This file is the consolidated baseline — the project was kept greenfield
-- and the V1..V18 evolution was collapsed before first prod deployment.
-- See commit history for the ordered migration archaeology.

-- =============================================================
-- Enums
-- =============================================================

CREATE TYPE alert_state_enum         AS ENUM ('PENDING', 'FIRING', 'RESOLVED');
CREATE TYPE severity_enum            AS ENUM ('CRITICAL', 'WARNING', 'INFO');
CREATE TYPE target_kind_enum         AS ENUM ('USER', 'GROUP', 'ROLE');
CREATE TYPE notification_status_enum AS ENUM ('PENDING', 'DELIVERED', 'FAILED');

CREATE TYPE condition_kind_enum      AS ENUM (
    'ROUTE_METRIC', 'EXCHANGE_MATCH', 'AGENT_STATE', 'AGENT_LIFECYCLE',
    'DEPLOYMENT_STATE', 'LOG_PATTERN', 'JVM_METRIC'
);

CREATE TYPE outbound_method_enum     AS ENUM ('POST', 'PUT', 'PATCH');
CREATE TYPE outbound_auth_kind_enum  AS ENUM ('NONE', 'BEARER', 'BASIC');
CREATE TYPE trust_mode_enum          AS ENUM ('SYSTEM_DEFAULT', 'TRUST_ALL', 'TRUST_PATHS');

-- =============================================================
-- RBAC — users, roles, groups, assignments
-- =============================================================

CREATE TABLE users (
    user_id               TEXT        PRIMARY KEY,
    provider              TEXT        NOT NULL,
    email                 TEXT,
    display_name          TEXT,
    password_hash         TEXT,
    failed_login_attempts INTEGER     NOT NULL DEFAULT 0,
    locked_until          TIMESTAMPTZ,
    token_revoked_before  TIMESTAMPTZ,
    created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE roles (
    id          UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
    name        TEXT        NOT NULL UNIQUE,
    description TEXT        NOT NULL DEFAULT '',
    scope       TEXT        NOT NULL DEFAULT 'custom',
    system      BOOLEAN     NOT NULL DEFAULT false,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO roles (id, name, description, scope, system) VALUES
    ('00000000-0000-0000-0000-000000000001', 'AGENT',    'Agent registration and data ingestion',              'system-wide', true),
    ('00000000-0000-0000-0000-000000000002', 'VIEWER',   'Read-only access to dashboards and data',            'system-wide', true),
    ('00000000-0000-0000-0000-000000000003', 'OPERATOR', 'Operational commands (start/stop/configure agents)', 'system-wide', true),
    ('00000000-0000-0000-0000-000000000004', 'ADMIN',    'Full administrative access',                         'system-wide', true);

CREATE TABLE groups (
    id              UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
    name            TEXT        NOT NULL UNIQUE,
    parent_group_id UUID        REFERENCES groups(id) ON DELETE SET NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_groups_parent ON groups (parent_group_id);

INSERT INTO groups (id, name) VALUES
    ('00000000-0000-0000-0000-000000000010', 'Admins');

CREATE TABLE group_roles (
    group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    role_id  UUID NOT NULL REFERENCES roles(id)  ON DELETE CASCADE,
    PRIMARY KEY (group_id, role_id)
);
CREATE INDEX idx_group_roles_group_id ON group_roles (group_id);

INSERT INTO group_roles (group_id, role_id) VALUES
    ('00000000-0000-0000-0000-000000000010', '00000000-0000-0000-0000-000000000004');

-- Claim-mapping rules (OIDC). Declared before user_roles/user_groups
-- because those tables carry an FK to claim_mapping_rules.id.
CREATE TABLE claim_mapping_rules (
    id          UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
    claim       TEXT        NOT NULL,
    match_type  TEXT        NOT NULL,
    match_value TEXT        NOT NULL,
    action      TEXT        NOT NULL,
    target      TEXT        NOT NULL,
    priority    INTEGER     NOT NULL DEFAULT 0,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT chk_match_type CHECK (match_type IN ('equals', 'contains', 'regex')),
    CONSTRAINT chk_action     CHECK (action     IN ('assignRole', 'addToGroup'))
);

CREATE TABLE user_roles (
    user_id    TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    role_id    UUID NOT NULL REFERENCES roles(id)      ON DELETE CASCADE,
    origin     TEXT NOT NULL DEFAULT 'direct',
    mapping_id UUID REFERENCES claim_mapping_rules(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id, origin)
);
CREATE INDEX idx_user_roles_user_id ON user_roles (user_id);
CREATE INDEX idx_user_roles_origin  ON user_roles (user_id, origin);

CREATE TABLE user_groups (
    user_id    TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    group_id   UUID NOT NULL REFERENCES groups(id)     ON DELETE CASCADE,
    origin     TEXT NOT NULL DEFAULT 'direct',
    mapping_id UUID REFERENCES claim_mapping_rules(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, group_id, origin)
);
CREATE INDEX idx_user_groups_user_id ON user_groups (user_id);
CREATE INDEX idx_user_groups_origin  ON user_groups (user_id, origin);

-- =============================================================
-- Server configuration
-- =============================================================

CREATE TABLE server_config (
    config_key TEXT        PRIMARY KEY,
    config_val JSONB       NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_by TEXT
);

-- =============================================================
-- Runtime management — environments, apps, versions, deployments
-- =============================================================

CREATE TABLE environments (
    id                       UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
    slug                     VARCHAR(100) NOT NULL UNIQUE,
    display_name             VARCHAR(255) NOT NULL,
    production               BOOLEAN      NOT NULL DEFAULT false,
    enabled                  BOOLEAN      NOT NULL DEFAULT true,
    default_container_config JSONB        NOT NULL DEFAULT '{}'::jsonb,
    jar_retention_count      INTEGER      DEFAULT 5,
    created_at               TIMESTAMPTZ  NOT NULL DEFAULT now(),
    updated_at               TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- Default environment — standalone mode always has at least one
INSERT INTO environments (slug, display_name) VALUES ('default', 'Default');

CREATE TABLE apps (
    id               UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
    environment_id   UUID         NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    slug             VARCHAR(100) NOT NULL,
    display_name     VARCHAR(255) NOT NULL,
    container_config JSONB        NOT NULL DEFAULT '{}'::jsonb,
    created_at       TIMESTAMPTZ  NOT NULL DEFAULT now(),
    updated_at       TIMESTAMPTZ  NOT NULL DEFAULT now(),
    UNIQUE (environment_id, slug)
);
CREATE INDEX idx_apps_environment_id ON apps (environment_id);

CREATE TABLE app_versions (
    id                    UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id                UUID         NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
    version               INTEGER      NOT NULL,
    jar_path              VARCHAR(500) NOT NULL,
    jar_checksum          VARCHAR(64)  NOT NULL,
    jar_filename          VARCHAR(255),
    jar_size_bytes        BIGINT,
    detected_runtime_type VARCHAR,
    detected_main_class   VARCHAR,
    uploaded_at           TIMESTAMPTZ  NOT NULL DEFAULT now(),
    UNIQUE (app_id, version)
);
CREATE INDEX idx_app_versions_app_id ON app_versions (app_id);

CREATE TABLE deployments (
    id                  UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id              UUID         NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
    app_version_id      UUID         NOT NULL REFERENCES app_versions(id),
    environment_id      UUID         NOT NULL REFERENCES environments(id),
    status              VARCHAR(20)  NOT NULL DEFAULT 'STARTING',
    target_state        VARCHAR(20)  NOT NULL DEFAULT 'RUNNING',
    deployment_strategy VARCHAR(20)  NOT NULL DEFAULT 'BLUE_GREEN',
    deploy_stage        VARCHAR(30),
    replica_states      JSONB        NOT NULL DEFAULT '[]'::jsonb,
    resolved_config     JSONB,
    container_id        VARCHAR(100),
    container_name      VARCHAR(255),
    error_message       TEXT,
    deployed_at         TIMESTAMPTZ,
    stopped_at          TIMESTAMPTZ,
    created_at          TIMESTAMPTZ  NOT NULL DEFAULT now()
);
CREATE INDEX idx_deployments_app_id ON deployments (app_id);
CREATE INDEX idx_deployments_env_id ON deployments (environment_id);

-- =============================================================
-- Application configuration (env-scoped)
-- =============================================================

CREATE TABLE application_config (
    application TEXT        NOT NULL,
    environment TEXT        NOT NULL,
    config_val  JSONB       NOT NULL,
    version     INTEGER     NOT NULL DEFAULT 1,
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_by  TEXT,
    PRIMARY KEY (application, environment)
);

CREATE TABLE app_settings (
    application_id    TEXT             NOT NULL,
    environment       TEXT             NOT NULL,
    sla_threshold_ms  INTEGER          NOT NULL DEFAULT 300,
    health_error_warn DOUBLE PRECISION NOT NULL DEFAULT 1.0,
    health_error_crit DOUBLE PRECISION NOT NULL DEFAULT 5.0,
    health_sla_warn   DOUBLE PRECISION NOT NULL DEFAULT 99.0,
    health_sla_crit   DOUBLE PRECISION NOT NULL DEFAULT 95.0,
    created_at        TIMESTAMPTZ      NOT NULL DEFAULT now(),
    updated_at        TIMESTAMPTZ      NOT NULL DEFAULT now(),
    PRIMARY KEY (application_id, environment)
);

-- =============================================================
-- Audit log
-- =============================================================

CREATE TABLE audit_log (
    id         BIGSERIAL PRIMARY KEY,
    timestamp  TIMESTAMPTZ NOT NULL DEFAULT now(),
    username   TEXT        NOT NULL,
    action     TEXT        NOT NULL,
    category   TEXT        NOT NULL,
    target     TEXT,
    detail     JSONB,
    result     TEXT        NOT NULL,
    ip_address TEXT,
    user_agent TEXT
);
CREATE INDEX idx_audit_log_timestamp ON audit_log (timestamp DESC);
CREATE INDEX idx_audit_log_username  ON audit_log (username);
CREATE INDEX idx_audit_log_category  ON audit_log (category);
CREATE INDEX idx_audit_log_action    ON audit_log (action);
CREATE INDEX idx_audit_log_target    ON audit_log (target);

-- =============================================================
-- Outbound connections (admin-managed HTTPS targets for webhooks)
-- =============================================================

CREATE TABLE outbound_connections (
    id                      UUID                     PRIMARY KEY,
    tenant_id               VARCHAR(64)              NOT NULL,
    name                    VARCHAR(100)             NOT NULL,
    description             TEXT,
    url                     TEXT                     NOT NULL,
    method                  outbound_method_enum     NOT NULL,
    default_headers         JSONB                    NOT NULL DEFAULT '{}'::jsonb,
    default_body_tmpl       TEXT,
    tls_trust_mode          trust_mode_enum          NOT NULL DEFAULT 'SYSTEM_DEFAULT',
    tls_ca_pem_paths        JSONB                    NOT NULL DEFAULT '[]'::jsonb,
    hmac_secret_ciphertext  TEXT,
    auth_kind               outbound_auth_kind_enum  NOT NULL DEFAULT 'NONE',
    auth_config             JSONB                    NOT NULL DEFAULT '{}'::jsonb,
    allowed_environment_ids UUID[]                   NOT NULL DEFAULT '{}'::uuid[],
    created_at              TIMESTAMPTZ              NOT NULL DEFAULT now(),
    created_by              TEXT                     NOT NULL REFERENCES users(user_id),
    updated_at              TIMESTAMPTZ              NOT NULL DEFAULT now(),
    updated_by              TEXT                     NOT NULL REFERENCES users(user_id),
    CONSTRAINT outbound_connections_name_unique_per_tenant UNIQUE (tenant_id, name),
    CONSTRAINT outbound_connections_url_check CHECK (url ~ '^https://')
);
CREATE INDEX outbound_connections_tenant_idx ON outbound_connections (tenant_id);

-- =============================================================
-- Alerting
-- =============================================================

CREATE TABLE alert_rules (
    id                          UUID                NOT NULL PRIMARY KEY,
    environment_id              UUID                NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    name                        VARCHAR(200)        NOT NULL,
    description                 TEXT,
    severity                    severity_enum       NOT NULL,
    enabled                     BOOLEAN             NOT NULL DEFAULT true,
    condition_kind              condition_kind_enum NOT NULL,
    condition                   JSONB               NOT NULL,
    evaluation_interval_seconds INTEGER             NOT NULL DEFAULT 60 CHECK (evaluation_interval_seconds >= 5),
    for_duration_seconds        INTEGER             NOT NULL DEFAULT 0  CHECK (for_duration_seconds >= 0),
    re_notify_minutes           INTEGER             NOT NULL DEFAULT 60 CHECK (re_notify_minutes >= 0),
    notification_title_tmpl     TEXT                NOT NULL,
    notification_message_tmpl   TEXT                NOT NULL,
    webhooks                    JSONB               NOT NULL DEFAULT '[]'::jsonb,
    next_evaluation_at          TIMESTAMPTZ         NOT NULL DEFAULT now(),
    claimed_by                  VARCHAR(64),
    claimed_until               TIMESTAMPTZ,
    eval_state                  JSONB               NOT NULL DEFAULT '{}'::jsonb,
    created_at                  TIMESTAMPTZ         NOT NULL DEFAULT now(),
    created_by                  TEXT                NOT NULL REFERENCES users(user_id),
    updated_at                  TIMESTAMPTZ         NOT NULL DEFAULT now(),
    updated_by                  TEXT                NOT NULL REFERENCES users(user_id)
);
CREATE INDEX alert_rules_env_idx       ON alert_rules (environment_id);
CREATE INDEX alert_rules_claim_due_idx ON alert_rules (next_evaluation_at) WHERE enabled = true;

CREATE TABLE alert_rule_targets (
    id          UUID             NOT NULL PRIMARY KEY,
    rule_id     UUID             NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
    target_kind target_kind_enum NOT NULL,
    target_id   VARCHAR(128)     NOT NULL,
    UNIQUE (rule_id, target_kind, target_id)
);
CREATE INDEX alert_rule_targets_lookup_idx ON alert_rule_targets (target_kind, target_id);

CREATE TABLE alert_instances (
    id                  UUID              NOT NULL PRIMARY KEY,
    rule_id             UUID              REFERENCES alert_rules(id) ON DELETE SET NULL,
    rule_snapshot       JSONB             NOT NULL,
    environment_id      UUID              NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    state               alert_state_enum  NOT NULL,
    severity            severity_enum     NOT NULL,
    fired_at            TIMESTAMPTZ       NOT NULL,
    acked_at            TIMESTAMPTZ,
    acked_by            TEXT              REFERENCES users(user_id),
    resolved_at         TIMESTAMPTZ,
    last_notified_at    TIMESTAMPTZ,
    silenced            BOOLEAN           NOT NULL DEFAULT false,
    current_value       NUMERIC,
    threshold           NUMERIC,
    context             JSONB             NOT NULL,
    title               TEXT              NOT NULL,
    message             TEXT              NOT NULL,
    target_user_ids     TEXT[]            NOT NULL DEFAULT '{}'::text[],
    target_group_ids    UUID[]            NOT NULL DEFAULT '{}'::uuid[],
    target_role_names   TEXT[]            NOT NULL DEFAULT '{}'::text[],
    read_at             TIMESTAMPTZ,
    deleted_at          TIMESTAMPTZ
);
CREATE INDEX alert_instances_inbox_idx     ON alert_instances (environment_id, state, fired_at DESC);
CREATE INDEX alert_instances_open_rule_idx ON alert_instances (rule_id, state) WHERE rule_id IS NOT NULL;
CREATE INDEX alert_instances_resolved_idx  ON alert_instances (resolved_at)     WHERE state = 'RESOLVED';
CREATE INDEX alert_instances_unread_idx    ON alert_instances (environment_id, read_at)
    WHERE read_at IS NULL AND deleted_at IS NULL;
CREATE INDEX alert_instances_deleted_idx   ON alert_instances (deleted_at)      WHERE deleted_at IS NOT NULL;
CREATE INDEX alert_instances_target_u_idx  ON alert_instances USING GIN (target_user_ids);
CREATE INDEX alert_instances_target_g_idx  ON alert_instances USING GIN (target_group_ids);
CREATE INDEX alert_instances_target_r_idx  ON alert_instances USING GIN (target_role_names);

-- Per-rule open-instance uniqueness. The discriminator prefers
-- context->>'_subjectFingerprint' (populated by the evaluator for
-- PER_EXCHANGE / PER_AGENT condition kinds), with a fall-through to
-- the legacy exchange-id path for rules predating the fingerprint.
-- Scalar kinds resolve to '' and retain strict one-open-per-rule.
-- Soft-deleted rows (deleted_at IS NOT NULL) free the slot so a
-- deleted alert can be re-raised.
CREATE UNIQUE INDEX alert_instances_open_rule_uq
  ON alert_instances (rule_id, COALESCE(
        context->>'_subjectFingerprint',
        context->'exchange'->>'id',
        ''))
  WHERE rule_id IS NOT NULL
    AND state IN ('PENDING', 'FIRING')
    AND deleted_at IS NULL;

CREATE TABLE alert_silences (
    id             UUID        NOT NULL PRIMARY KEY,
    environment_id UUID        NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    matcher        JSONB       NOT NULL,
    reason         TEXT,
    starts_at      TIMESTAMPTZ NOT NULL,
    ends_at        TIMESTAMPTZ NOT NULL,
    created_by     TEXT        NOT NULL REFERENCES users(user_id),
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    CHECK (ends_at > starts_at)
);
CREATE INDEX alert_silences_active_idx ON alert_silences (environment_id, ends_at);

CREATE TABLE alert_notifications (
    id                     UUID                     NOT NULL PRIMARY KEY,
    alert_instance_id      UUID                     NOT NULL REFERENCES alert_instances(id) ON DELETE CASCADE,
    webhook_id             UUID,
    outbound_connection_id UUID                     REFERENCES outbound_connections(id) ON DELETE SET NULL,
    status                 notification_status_enum NOT NULL DEFAULT 'PENDING',
    attempts               INTEGER                  NOT NULL DEFAULT 0,
    next_attempt_at        TIMESTAMPTZ              NOT NULL DEFAULT now(),
    claimed_by             VARCHAR(64),
    claimed_until          TIMESTAMPTZ,
    last_response_status   INTEGER,
    last_response_snippet  TEXT,
    payload                JSONB                    NOT NULL,
    delivered_at           TIMESTAMPTZ,
    created_at             TIMESTAMPTZ              NOT NULL DEFAULT now()
);
CREATE INDEX alert_notifications_instance_idx ON alert_notifications (alert_instance_id);
CREATE INDEX alert_notifications_pending_idx  ON alert_notifications (next_attempt_at) WHERE status = 'PENDING';
