-- ============================================================================
-- Audit Schema: Audit Logs Table
-- Comprehensive audit trail for Trading Platform SaaS
-- ============================================================================

-- Create audit schema if not exists
CREATE SCHEMA IF NOT EXISTS audit;

-- Grant usage
GRANT USAGE ON SCHEMA audit TO trading_user;

-- ============================================================================
-- AUDIT_LOGS TABLE
-- Immutable audit trail for all significant actions
-- ============================================================================

CREATE TABLE IF NOT EXISTS audit.audit_logs (
    -- Primary key (using BIGSERIAL for high volume)
    id BIGSERIAL PRIMARY KEY,

    -- Event identification
    event_id UUID NOT NULL DEFAULT gen_random_uuid(),
    event_type VARCHAR(100) NOT NULL,
    event_category VARCHAR(50) NOT NULL,

    -- Actor information (who performed the action)
    actor_id UUID,
    actor_email VARCHAR(255),
    actor_type VARCHAR(20) NOT NULL DEFAULT 'user'
        CHECK (actor_type IN ('user', 'system', 'api', 'webhook', 'scheduled')),
    actor_ip INET,
    actor_user_agent TEXT,

    -- Tenant context
    tenant_id UUID,

    -- Target resource
    resource_type VARCHAR(100) NOT NULL,
    resource_id VARCHAR(255),
    resource_name VARCHAR(255),

    -- Action details
    action VARCHAR(50) NOT NULL,
    action_result VARCHAR(20) NOT NULL DEFAULT 'success'
        CHECK (action_result IN ('success', 'failure', 'partial', 'pending')),

    -- Change tracking
    previous_state JSONB,
    new_state JSONB,
    changes JSONB, -- Computed diff between previous and new state

    -- Additional context
    metadata JSONB DEFAULT '{}'::jsonb,
    request_id VARCHAR(100),
    session_id UUID,

    -- Error information (for failures)
    error_code VARCHAR(50),
    error_message TEXT,

    -- Timestamps
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,

    -- Retention marker
    is_archived BOOLEAN NOT NULL DEFAULT false,
    archived_at TIMESTAMPTZ
);

-- ============================================================================
-- INDEXES
-- Optimized for common query patterns
-- ============================================================================

-- Time-based queries (most common)
CREATE INDEX IF NOT EXISTS idx_audit_logs_occurred_at ON audit.audit_logs(occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_time ON audit.audit_logs(tenant_id, occurred_at DESC);

-- Actor queries
CREATE INDEX IF NOT EXISTS idx_audit_logs_actor_id ON audit.audit_logs(actor_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_logs_actor_email ON audit.audit_logs(actor_email);

-- Resource queries
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit.audit_logs(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource_tenant ON audit.audit_logs(tenant_id, resource_type, occurred_at DESC);

-- Event type queries
CREATE INDEX IF NOT EXISTS idx_audit_logs_event_type ON audit.audit_logs(event_type);
CREATE INDEX IF NOT EXISTS idx_audit_logs_event_category ON audit.audit_logs(event_category);

-- Action queries
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit.audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_audit_logs_action_result ON audit.audit_logs(action_result) WHERE action_result = 'failure';

-- Request tracking
CREATE INDEX IF NOT EXISTS idx_audit_logs_request_id ON audit.audit_logs(request_id) WHERE request_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_audit_logs_session_id ON audit.audit_logs(session_id) WHERE session_id IS NOT NULL;

-- Archival
CREATE INDEX IF NOT EXISTS idx_audit_logs_archived ON audit.audit_logs(is_archived, occurred_at);

-- ============================================================================
-- PARTITIONING (for high-volume deployments)
-- Uncomment to enable monthly partitions
-- ============================================================================

-- CREATE TABLE audit.audit_logs_partitioned (
--     LIKE audit.audit_logs INCLUDING ALL
-- ) PARTITION BY RANGE (occurred_at);

-- ============================================================================
-- ROW LEVEL SECURITY
-- ============================================================================

ALTER TABLE audit.audit_logs ENABLE ROW LEVEL SECURITY;

-- Policy: Users can only see audit logs in their tenant
-- Note: System admins may need broader access
CREATE POLICY audit_logs_tenant_isolation ON audit.audit_logs
    FOR SELECT
    USING (
        tenant_id = current_setting('app.current_tenant_id', true)::uuid
        OR current_setting('app.is_system_admin', true)::boolean = true
    );

-- Policy: Insert is allowed for logging (no tenant check on insert)
CREATE POLICY audit_logs_insert ON audit.audit_logs
    FOR INSERT
    WITH CHECK (true);

-- ============================================================================
-- FUNCTION: Log audit event
-- ============================================================================

CREATE OR REPLACE FUNCTION audit.log_event(
    p_event_type VARCHAR(100),
    p_event_category VARCHAR(50),
    p_action VARCHAR(50),
    p_resource_type VARCHAR(100),
    p_resource_id VARCHAR(255) DEFAULT NULL,
    p_resource_name VARCHAR(255) DEFAULT NULL,
    p_actor_id UUID DEFAULT NULL,
    p_actor_email VARCHAR(255) DEFAULT NULL,
    p_actor_type VARCHAR(20) DEFAULT 'user',
    p_actor_ip INET DEFAULT NULL,
    p_tenant_id UUID DEFAULT NULL,
    p_previous_state JSONB DEFAULT NULL,
    p_new_state JSONB DEFAULT NULL,
    p_action_result VARCHAR(20) DEFAULT 'success',
    p_metadata JSONB DEFAULT '{}'::jsonb,
    p_request_id VARCHAR(100) DEFAULT NULL,
    p_session_id UUID DEFAULT NULL,
    p_error_code VARCHAR(50) DEFAULT NULL,
    p_error_message TEXT DEFAULT NULL
)
RETURNS BIGINT AS $$
DECLARE
    v_log_id BIGINT;
    v_changes JSONB;
BEGIN
    -- Compute changes if both states provided
    IF p_previous_state IS NOT NULL AND p_new_state IS NOT NULL THEN
        v_changes := audit.compute_json_diff(p_previous_state, p_new_state);
    END IF;

    INSERT INTO audit.audit_logs (
        event_type, event_category, action,
        resource_type, resource_id, resource_name,
        actor_id, actor_email, actor_type, actor_ip,
        tenant_id,
        previous_state, new_state, changes,
        action_result, metadata,
        request_id, session_id,
        error_code, error_message
    ) VALUES (
        p_event_type, p_event_category, p_action,
        p_resource_type, p_resource_id, p_resource_name,
        p_actor_id, p_actor_email, p_actor_type, p_actor_ip,
        p_tenant_id,
        p_previous_state, p_new_state, v_changes,
        p_action_result, p_metadata,
        p_request_id, p_session_id,
        p_error_code, p_error_message
    )
    RETURNING id INTO v_log_id;

    RETURN v_log_id;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- FUNCTION: Compute JSON diff
-- ============================================================================

CREATE OR REPLACE FUNCTION audit.compute_json_diff(
    p_old JSONB,
    p_new JSONB
)
RETURNS JSONB AS $$
DECLARE
    v_key TEXT;
    v_diff JSONB := '{}'::jsonb;
BEGIN
    -- Find changed and added keys
    FOR v_key IN SELECT jsonb_object_keys(p_new)
    LOOP
        IF NOT p_old ? v_key THEN
            -- New key added
            v_diff := v_diff || jsonb_build_object(v_key, jsonb_build_object(
                'action', 'added',
                'new', p_new->v_key
            ));
        ELSIF p_old->v_key != p_new->v_key THEN
            -- Key modified
            v_diff := v_diff || jsonb_build_object(v_key, jsonb_build_object(
                'action', 'modified',
                'old', p_old->v_key,
                'new', p_new->v_key
            ));
        END IF;
    END LOOP;

    -- Find deleted keys
    FOR v_key IN SELECT jsonb_object_keys(p_old)
    LOOP
        IF NOT p_new ? v_key THEN
            v_diff := v_diff || jsonb_build_object(v_key, jsonb_build_object(
                'action', 'deleted',
                'old', p_old->v_key
            ));
        END IF;
    END LOOP;

    RETURN v_diff;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- ============================================================================
-- CONVENIENCE FUNCTIONS FOR COMMON EVENTS
-- ============================================================================

-- Log authentication event
CREATE OR REPLACE FUNCTION audit.log_auth_event(
    p_action VARCHAR(50), -- 'login', 'logout', 'login_failed', 'password_changed', etc.
    p_user_id UUID,
    p_user_email VARCHAR(255),
    p_tenant_id UUID,
    p_ip INET DEFAULT NULL,
    p_success BOOLEAN DEFAULT true,
    p_metadata JSONB DEFAULT '{}'::jsonb,
    p_error_message TEXT DEFAULT NULL
)
RETURNS BIGINT AS $$
BEGIN
    RETURN audit.log_event(
        'auth.' || p_action,
        'authentication',
        p_action,
        'user',
        p_user_id::text,
        p_user_email,
        p_user_id,
        p_user_email,
        'user',
        p_ip,
        p_tenant_id,
        NULL, NULL,
        CASE WHEN p_success THEN 'success' ELSE 'failure' END,
        p_metadata,
        NULL, NULL,
        CASE WHEN NOT p_success THEN 'AUTH_FAILED' ELSE NULL END,
        p_error_message
    );
END;
$$ LANGUAGE plpgsql;

-- Log resource CRUD event
CREATE OR REPLACE FUNCTION audit.log_resource_event(
    p_action VARCHAR(50), -- 'created', 'updated', 'deleted', 'viewed'
    p_resource_type VARCHAR(100),
    p_resource_id VARCHAR(255),
    p_resource_name VARCHAR(255),
    p_actor_id UUID,
    p_actor_email VARCHAR(255),
    p_tenant_id UUID,
    p_previous_state JSONB DEFAULT NULL,
    p_new_state JSONB DEFAULT NULL,
    p_metadata JSONB DEFAULT '{}'::jsonb
)
RETURNS BIGINT AS $$
BEGIN
    RETURN audit.log_event(
        p_resource_type || '.' || p_action,
        'resource',
        p_action,
        p_resource_type,
        p_resource_id,
        p_resource_name,
        p_actor_id,
        p_actor_email,
        'user',
        NULL,
        p_tenant_id,
        p_previous_state,
        p_new_state,
        'success',
        p_metadata
    );
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- VIEW: Recent audit logs
-- ============================================================================

CREATE OR REPLACE VIEW audit.v_recent_logs AS
SELECT
    id,
    event_id,
    event_type,
    event_category,
    actor_email,
    actor_type,
    actor_ip,
    tenant_id,
    resource_type,
    resource_id,
    resource_name,
    action,
    action_result,
    changes,
    metadata,
    error_message,
    occurred_at
FROM audit.audit_logs
WHERE occurred_at > CURRENT_TIMESTAMP - INTERVAL '30 days'
  AND is_archived = false
ORDER BY occurred_at DESC;

-- ============================================================================
-- FUNCTION: Archive old logs
-- ============================================================================

CREATE OR REPLACE FUNCTION audit.archive_old_logs(
    p_older_than_days INTEGER DEFAULT 90
)
RETURNS INTEGER AS $$
DECLARE
    v_count INTEGER;
BEGIN
    UPDATE audit.audit_logs
    SET is_archived = true,
        archived_at = CURRENT_TIMESTAMP
    WHERE is_archived = false
      AND occurred_at < CURRENT_TIMESTAMP - (p_older_than_days || ' days')::interval;

    GET DIAGNOSTICS v_count = ROW_COUNT;
    RETURN v_count;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- GRANTS
-- ============================================================================

GRANT SELECT ON audit.audit_logs TO trading_user;
GRANT INSERT ON audit.audit_logs TO trading_user;
GRANT SELECT ON audit.v_recent_logs TO trading_user;
GRANT USAGE, SELECT ON SEQUENCE audit.audit_logs_id_seq TO trading_user;
GRANT EXECUTE ON FUNCTION audit.log_event TO trading_user;
GRANT EXECUTE ON FUNCTION audit.log_auth_event TO trading_user;
GRANT EXECUTE ON FUNCTION audit.log_resource_event TO trading_user;
GRANT EXECUTE ON FUNCTION audit.compute_json_diff TO trading_user;

-- ============================================================================
-- COMMENTS
-- ============================================================================

COMMENT ON TABLE audit.audit_logs IS 'Immutable audit trail for all significant actions';
COMMENT ON COLUMN audit.audit_logs.event_type IS 'Full event type e.g., auth.login, user.created';
COMMENT ON COLUMN audit.audit_logs.event_category IS 'High-level category: authentication, resource, system';
COMMENT ON COLUMN audit.audit_logs.actor_type IS 'Type of actor: user, system, api, webhook, scheduled';
COMMENT ON COLUMN audit.audit_logs.changes IS 'Computed diff between previous_state and new_state';
COMMENT ON COLUMN audit.audit_logs.is_archived IS 'Marker for log retention/archival';
COMMENT ON FUNCTION audit.log_event IS 'Main function to log any audit event';
COMMENT ON FUNCTION audit.log_auth_event IS 'Convenience function for authentication events';
COMMENT ON FUNCTION audit.log_resource_event IS 'Convenience function for resource CRUD events';
