CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TYPE user_role AS ENUM (
    'admin',
    'operator',
    'viewer'
);

CREATE TYPE agent_status AS ENUM (
    'online',
    'offline',
    'busy',
    'error'
);

CREATE TYPE command_status AS ENUM (
    'pending',
    'running',
    'completed',
    'failed',
    'cancelled'
);

CREATE TYPE transfer_direction AS ENUM (
    'upload',
    'download'
);

CREATE TYPE actor_type_enum AS ENUM (
    'user',
    'agent',
    'system'
);

-- USERS

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    username TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,

    role user_role NOT NULL DEFAULT 'viewer',

    is_active BOOLEAN NOT NULL DEFAULT TRUE,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_role
ON users(role);

CREATE INDEX idx_users_active
ON users(is_active);

-- AGENTS

CREATE TABLE agents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    hostname TEXT NOT NULL UNIQUE,

    token_hash TEXT NOT NULL,

    cert_fingerprint TEXT,

    os_info JSONB,

    status agent_status NOT NULL DEFAULT 'offline',

    is_active BOOLEAN NOT NULL DEFAULT TRUE,

    last_seen TIMESTAMPTZ,

    registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_agents_status
ON agents(status);

CREATE INDEX idx_agents_last_seen
ON agents(last_seen);

CREATE INDEX idx_agents_active
ON agents(is_active);

CREATE INDEX idx_agents_os_info_gin
ON agents
USING GIN(os_info);

-- COMMAND TYPES

CREATE TABLE command_types (
    id SMALLSERIAL PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);

INSERT INTO command_types(name)
VALUES
('shell'),
('powershell'),
('upload'),
('download'),
('system'),
('custom');

-- COMMANDS

CREATE TABLE commands (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    agent_id UUID NOT NULL
        REFERENCES agents(id)
        ON DELETE CASCADE,

    issued_by UUID
        REFERENCES users(id)
        ON DELETE SET NULL,

    command_type_id SMALLINT
        REFERENCES command_types(id),

    command TEXT NOT NULL,

    status command_status NOT NULL DEFAULT 'pending',

    output TEXT,

    exit_code INTEGER,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    completed_at TIMESTAMPTZ,

    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CHECK (
        completed_at IS NULL
        OR completed_at >= created_at
    ),

    CHECK (
        exit_code IS NULL
        OR exit_code BETWEEN -255 AND 255
    )
);

CREATE INDEX idx_commands_agent
ON commands(agent_id);

CREATE INDEX idx_commands_issued_by
ON commands(issued_by);

CREATE INDEX idx_commands_status
ON commands(status);

CREATE INDEX idx_commands_created_at
ON commands(created_at DESC);

CREATE INDEX idx_commands_pending
ON commands(status)
WHERE status = 'pending';

CREATE INDEX idx_commands_running
ON commands(status)
WHERE status = 'running';

-- FILE TRANSFERS

CREATE TABLE file_transfers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    agent_id UUID
        REFERENCES agents(id)
        ON DELETE CASCADE,

    initiated_by UUID
        REFERENCES users(id)
        ON DELETE SET NULL,

    command_id UUID
        REFERENCES commands(id)
        ON DELETE SET NULL,

    direction transfer_direction NOT NULL,

    filename TEXT NOT NULL,

    sha256 CHAR(64),

    size_bytes BIGINT,

    status command_status NOT NULL DEFAULT 'pending',

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CHECK (
        size_bytes IS NULL
        OR size_bytes >= 0
    )
);

CREATE INDEX idx_file_transfers_agent
ON file_transfers(agent_id);

CREATE INDEX idx_file_transfers_status
ON file_transfers(status);

CREATE INDEX idx_file_transfers_sha256
ON file_transfers(sha256);

CREATE INDEX idx_file_transfers_created
ON file_transfers(created_at DESC);

-- AUDIT LOGS

CREATE TABLE audit_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    actor_id UUID,

    actor_type actor_type_enum NOT NULL,

    action TEXT NOT NULL,

    target TEXT,

    details JSONB,

    ip_address INET,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_audit_actor
ON audit_logs(actor_id);

CREATE INDEX idx_audit_action
ON audit_logs(action);

CREATE INDEX idx_audit_created
ON audit_logs(created_at DESC);

CREATE INDEX idx_audit_details_gin
ON audit_logs
USING GIN(details);

-- AGENT HEARTBEATS

CREATE TABLE agent_heartbeats (
    id BIGSERIAL PRIMARY KEY,

    agent_id UUID NOT NULL
        REFERENCES agents(id)
        ON DELETE CASCADE,

    received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    metadata JSONB
);

CREATE INDEX idx_heartbeats_agent
ON agent_heartbeats(agent_id);

CREATE INDEX idx_heartbeats_received
ON agent_heartbeats(received_at DESC);

-- USER SESSIONS

CREATE TABLE user_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL
        REFERENCES users(id)
        ON DELETE CASCADE,

    refresh_token_hash TEXT NOT NULL,

    user_agent TEXT,

    ip_address INET,

    expires_at TIMESTAMPTZ NOT NULL,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_sessions_user
ON user_sessions(user_id);

CREATE INDEX idx_sessions_expires
ON user_sessions(expires_at);

-- AGENT GROUPS

CREATE TABLE agent_groups (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    name TEXT NOT NULL UNIQUE,

    description TEXT,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE agent_group_members (
    agent_id UUID NOT NULL
        REFERENCES agents(id)
        ON DELETE CASCADE,

    group_id UUID NOT NULL
        REFERENCES agent_groups(id)
        ON DELETE CASCADE,

    PRIMARY KEY (agent_id, group_id)
);

-- UPDATE updated_at FUNCTION

CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS
$$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- updated_at TRIGGERS

CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER trg_agents_updated_at
BEFORE UPDATE ON agents
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER trg_commands_updated_at
BEFORE UPDATE ON commands
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER trg_file_transfers_updated_at
BEFORE UPDATE ON file_transfers
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

-- COMMAND COMPLETION TRIGGER

CREATE OR REPLACE FUNCTION update_agent_after_command()
RETURNS TRIGGER AS
$$
BEGIN
    IF NEW.status = 'completed'
       AND OLD.status IS DISTINCT FROM 'completed'
    THEN
        UPDATE agents
        SET
            last_seen = NOW(),
            status = 'online'
        WHERE id = NEW.agent_id;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_update_agent_after_command
AFTER UPDATE ON commands
FOR EACH ROW
EXECUTE FUNCTION update_agent_after_command();

-- AUDIT LOG COMMAND CREATION

CREATE OR REPLACE FUNCTION log_command_creation()
RETURNS TRIGGER AS
$$
BEGIN
    INSERT INTO audit_logs (
        actor_id,
        actor_type,
        action,
        target,
        details
    )
    VALUES (
        NEW.issued_by,
        'user',
        'command_created',
        NEW.id::TEXT,
        jsonb_build_object(
            'command_length', length(NEW.command),
            'status', NEW.status
        )
    );

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_log_command_creation
AFTER INSERT ON commands
FOR EACH ROW
EXECUTE FUNCTION log_command_creation();

-- AGENT STATUS CHANGE LOGGING

CREATE OR REPLACE FUNCTION log_agent_status_change()
RETURNS TRIGGER AS
$$
BEGIN
    IF OLD.status IS DISTINCT FROM NEW.status THEN

        INSERT INTO audit_logs (
            actor_id,
            actor_type,
            action,
            target,
            details
        )
        VALUES (
            NEW.id,
            'agent',
            'status_changed',
            NEW.hostname,
            jsonb_build_object(
                'old_status', OLD.status,
                'new_status', NEW.status
            )
        );

    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_log_agent_status_change
AFTER UPDATE ON agents
FOR EACH ROW
EXECUTE FUNCTION log_agent_status_change();


-- IMMUTABLE AUDIT LOGS

CREATE OR REPLACE FUNCTION prevent_audit_log_modification()
RETURNS TRIGGER AS
$$
BEGIN
    RAISE EXCEPTION 'audit_logs table is immutable';
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_prevent_audit_update
BEFORE UPDATE ON audit_logs
FOR EACH ROW
EXECUTE FUNCTION prevent_audit_log_modification();

CREATE TRIGGER trg_prevent_audit_delete
BEFORE DELETE ON audit_logs
FOR EACH ROW
EXECUTE FUNCTION prevent_audit_log_modification();

-- VIEWS

CREATE VIEW active_agents_view AS
SELECT
    a.id,
    a.hostname,
    a.status,
    a.last_seen,
    a.registered_at,

    COUNT(c.id) AS total_commands,

    COUNT(*) FILTER (
        WHERE c.status = 'completed'
    ) AS completed_commands,

    COUNT(*) FILTER (
        WHERE c.status = 'pending'
    ) AS pending_commands

FROM agents a
LEFT JOIN commands c
ON a.id = c.agent_id

WHERE a.is_active = TRUE

GROUP BY
    a.id,
    a.hostname,
    a.status,
    a.last_seen,
    a.registered_at;


-- RECENT COMMANDS VIEW

CREATE VIEW recent_commands_view AS
SELECT
    c.id,
    c.command,
    c.status,
    c.created_at,
    c.completed_at,

    a.hostname AS agent_hostname,

    u.username AS issued_by

FROM commands c

JOIN agents a
ON c.agent_id = a.id

LEFT JOIN users u
ON c.issued_by = u.id;

-- AUDIT LOGS VIEW

CREATE VIEW audit_logs_view AS
SELECT
    al.id,
    al.action,
    al.target,
    al.created_at,
    al.ip_address,

    u.username AS actor_username,

    a.hostname AS agent_hostname

FROM audit_logs al

LEFT JOIN users u
ON al.actor_id = u.id
AND al.actor_type = 'user'

LEFT JOIN agents a
ON al.actor_id = a.id
AND al.actor_type = 'agent';

-- CLEANUP

CREATE OR REPLACE PROCEDURE cleanup_old_commands(
    days_to_keep INTEGER
)
LANGUAGE plpgsql
AS
$$
DECLARE
    deleted_rows INTEGER;
BEGIN

    DELETE FROM commands
    WHERE status = 'completed'
    AND completed_at < NOW() - (days_to_keep || ' days')::INTERVAL;

    GET DIAGNOSTICS deleted_rows = ROW_COUNT;

    INSERT INTO audit_logs (
        actor_type,
        action,
        target,
        details
    )
    VALUES (
        'system',
        'cleanup_completed_commands',
        'commands',
        jsonb_build_object(
            'days_to_keep', days_to_keep,
            'deleted_rows', deleted_rows
        )
    );

END;
$$;

-- SYSTEM STATS 

CREATE OR REPLACE FUNCTION get_system_stats()
RETURNS TABLE (
    total_users BIGINT,
    total_agents BIGINT,
    online_agents BIGINT,
    pending_commands BIGINT
)
LANGUAGE plpgsql
AS
$$
BEGIN

    RETURN QUERY
    SELECT
        (SELECT COUNT(*) FROM users WHERE is_active = TRUE),
        (SELECT COUNT(*) FROM agents),
        (SELECT COUNT(*) FROM agents WHERE status = 'online'),
        (SELECT COUNT(*) FROM commands WHERE status = 'pending');

END;
$$;

