-- ============================================================================
-- CRM Security Architecture Upgrade
-- Internal Company CRM — Prioritizes control, recovery, and maintainability
-- ============================================================================
-- Implements:
--   • Dual password storage (bcrypt + pgcrypto AES reversible)
--   • SUPER_ADMIN master key recovery system
--   • Row Level Security (RLS) on CRM tables
--   • Database export logging
--   • Backup logging
--   • Immutable admin action tracking
--   • SQL injection defense (parameterized queries enforced app-side)
-- ============================================================================

-- ============================================================================
-- 1. DUAL PASSWORD STORAGE
-- ============================================================================
-- password_hash (bcrypt)      — used for normal login authentication
-- password_encrypted (AES)    — reversible encryption for emergency credential recovery
-- ============================================================================

ALTER TABLE users ADD COLUMN IF NOT EXISTS password_encrypted TEXT;

-- ============================================================================
-- 2. SUPER_ADMIN MASTER KEY SYSTEM
-- ============================================================================
-- Stores the master decryption key used with pgp_sym_encrypt/pgp_sym_decrypt.
-- Only accessible by SUPER_ADMIN role via security definer functions.
-- ============================================================================

CREATE TABLE IF NOT EXISTS master_keys (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    key_name VARCHAR(100) NOT NULL UNIQUE,
    key_value TEXT NOT NULL,
    description TEXT,
    created_by UUID REFERENCES users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    is_active BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE INDEX idx_master_keys_active ON master_keys(key_name) WHERE is_active = TRUE;

-- ============================================================================
-- 3. DATABASE EXPORT LOGGING
-- ============================================================================
-- Tracks all database exports and SQL dumps performed by SUPER_ADMIN.
-- Immutable — never allow deletion or update.
-- ============================================================================

CREATE TABLE IF NOT EXISTS database_export_logs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    exported_by UUID NOT NULL REFERENCES users(id),
    export_type VARCHAR(50) NOT NULL,
    file_name VARCHAR(500) NOT NULL,
    file_size_bytes BIGINT,
    record_count INT,
    ip_address INET,
    user_agent TEXT,
    notes TEXT,
    exported_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_export_logs_user ON database_export_logs(exported_by);
CREATE INDEX idx_export_logs_time ON database_export_logs(exported_at DESC);

COMMENT ON TABLE database_export_logs IS 'Immutable audit of all database exports. Never DELETE or UPDATE rows.';
COMMENT ON COLUMN database_export_logs.export_type IS 'pg_dump, csv_export, full_backup, selective_export';

-- ============================================================================
-- 4. BACKUP LOGGING
-- ============================================================================
-- Records every automated or manual database backup.
-- Retention: minimum 30 days of backup history.
-- ============================================================================

CREATE TABLE IF NOT EXISTS backup_logs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    backup_type VARCHAR(20) NOT NULL,
    status VARCHAR(20) NOT NULL,
    file_name VARCHAR(500),
    file_size_bytes BIGINT,
    pg_dump_exit_code INT,
    error_message TEXT,
    checksum VARCHAR(64),
    verification_status VARCHAR(20),
    started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at TIMESTAMPTZ,
    duration_seconds INT,
    retention_days INT NOT NULL DEFAULT 30,
    triggered_by UUID REFERENCES users(id),
    notes TEXT
);

CREATE INDEX idx_backup_logs_time ON backup_logs(started_at DESC);
CREATE INDEX idx_backup_logs_status ON backup_logs(status);
CREATE INDEX idx_backup_logs_type ON backup_logs(backup_type);
CREATE INDEX idx_backup_logs_completed ON backup_logs(status)
    WHERE status = 'completed';

-- ============================================================================
-- 5. AUDIT TRIGGER: Password changes
-- ============================================================================

CREATE OR REPLACE FUNCTION audit_password_change()
RETURNS TRIGGER AS $$
BEGIN
    IF OLD.password_hash IS DISTINCT FROM NEW.password_hash THEN
        INSERT INTO audit_logs (
            table_name, record_id, action, old_data, new_data, changed_by, ip_address
        ) VALUES (
            'users',
            NEW.id,
            'UPDATE',
            jsonb_build_object('password_changed', true, 'password_change_required', OLD.password_change_required),
            jsonb_build_object('password_changed', true, 'password_change_required', NEW.password_change_required),
            NULLIF(current_setting('app.current_user_id', true), ''),
            NULLIF(current_setting('app.current_ip', true), '')
        );
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

DROP TRIGGER IF EXISTS trg_audit_password_change ON users;
CREATE TRIGGER trg_audit_password_change
    AFTER UPDATE OF password_hash ON users
    FOR EACH ROW
    EXECUTE FUNCTION audit_password_change();

-- ============================================================================
-- 6. AUDIT TRIGGER: User creation
-- ============================================================================

CREATE OR REPLACE FUNCTION audit_user_creation()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO audit_logs (
        table_name, record_id, action, new_data, changed_by
    ) VALUES (
        'users',
        NEW.id,
        'CREATE',
        jsonb_build_object(
            'username', NEW.username,
            'email', NEW.email,
            'first_name', NEW.first_name,
            'last_name', NEW.last_name,
            'is_active', NEW.is_active
        ),
        NEW.created_by
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

DROP TRIGGER IF EXISTS trg_audit_user_create ON users;
CREATE TRIGGER trg_audit_user_create
    AFTER INSERT ON users
    FOR EACH ROW
    EXECUTE FUNCTION audit_user_creation();

-- ============================================================================
-- 7. AUDIT TRIGGER: Database exports
-- ============================================================================

CREATE OR REPLACE FUNCTION audit_database_export()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO audit_logs (
        table_name, record_id, action, new_data, changed_by
    ) VALUES (
        'database_export_logs',
        NEW.id,
        'CREATE',
        jsonb_build_object(
            'export_type', NEW.export_type,
            'file_name', NEW.file_name,
            'file_size_bytes', NEW.file_size_bytes,
            'record_count', NEW.record_count
        ),
        NEW.exported_by
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

DROP TRIGGER IF EXISTS trg_audit_database_export ON database_export_logs;
CREATE TRIGGER trg_audit_database_export
    AFTER INSERT ON database_export_logs
    FOR EACH ROW
    EXECUTE FUNCTION audit_database_export();

-- ============================================================================
-- 8. AUDIT TRIGGER: Login/logout already exists via login_attempts trigger
--    (trg_audit_login in 001_schema.sql)
--    This enhances it to also track session-level events.
-- ============================================================================

DROP TRIGGER IF EXISTS trg_audit_login ON login_attempts;
CREATE TRIGGER trg_audit_login
    AFTER INSERT ON login_attempts
    FOR EACH ROW
    EXECUTE FUNCTION audit_login();

-- ============================================================================
-- 9. ROW LEVEL SECURITY
-- ============================================================================
-- RLS policies enforce data visibility per role:
--   SALES_USER  → only sees records assigned to them
--   ADMIN       → sees operational records for investigation
--   SUPER_ADMIN → sees everything
-- ============================================================================

-- Helper function to get current user's role hierarchy level
CREATE OR REPLACE FUNCTION current_user_hierarchy_level()
RETURNS INT AS $$
DECLARE
    uid UUID;
BEGIN
    BEGIN
        uid := NULLIF(current_setting('app.current_user_id', true), '')::UUID;
    EXCEPTION WHEN OTHERS THEN
        RETURN NULL;
    END;

    IF uid IS NULL THEN
        RETURN NULL;
    END IF;

    RETURN (
        SELECT MIN(r.hierarchy_level)
        FROM user_roles ur
        JOIN roles r ON r.id = ur.role_id
        WHERE ur.user_id = uid
    );
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;

-- Helper function to get current user's ID from session setting
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS UUID AS $$
BEGIN
    BEGIN
        RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID;
    EXCEPTION WHEN OTHERS THEN
        RETURN NULL;
    END;
END;
$$ LANGUAGE plpgsql STABLE;

-- SALES_USER level = 3, ADMIN level = 2, SUPER_ADMIN level = 1

-- ── customers ──
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS rls_customers_select ON customers;
CREATE POLICY rls_customers_select ON customers
    FOR SELECT
    USING (
        current_user_hierarchy_level() IS NULL
        OR current_user_hierarchy_level() <= 2  -- ADMIN and SUPER_ADMIN see all
        OR owner_id = current_user_id()
    );

DROP POLICY IF EXISTS rls_customers_insert ON customers;
CREATE POLICY rls_customers_insert ON customers
    FOR INSERT
    WITH CHECK (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2  -- ADMIN and SUPER_ADMIN can assign any owner
            OR owner_id = current_user_id()       -- SALES_USER can only assign to self
        )
    );

DROP POLICY IF EXISTS rls_customers_update ON customers;
CREATE POLICY rls_customers_update ON customers
    FOR UPDATE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2
            OR owner_id = current_user_id()
        )
    )
    WITH CHECK (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2
            OR (
                owner_id = current_user_id()
                AND owner_id = current_user_id()  -- SALES_USER cannot reassign ownership
            )
        )
    );

DROP POLICY IF EXISTS rls_customers_delete ON customers;
CREATE POLICY rls_customers_delete ON customers
    FOR DELETE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND current_user_hierarchy_level() <= 2
    );

-- ── leads ──
ALTER TABLE leads ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS rls_leads_select ON leads;
CREATE POLICY rls_leads_select ON leads
    FOR SELECT
    USING (
        current_user_hierarchy_level() IS NULL
        OR current_user_hierarchy_level() <= 2
        OR assigned_to = current_user_id()
    );

DROP POLICY IF EXISTS rls_leads_insert ON leads;
CREATE POLICY rls_leads_insert ON leads
    FOR INSERT
    WITH CHECK (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2
            OR assigned_to = current_user_id()
        )
    );

DROP POLICY IF EXISTS rls_leads_update ON leads;
CREATE POLICY rls_leads_update ON leads
    FOR UPDATE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2
            OR assigned_to = current_user_id()
        )
    );

DROP POLICY IF EXISTS rls_leads_delete ON leads;
CREATE POLICY rls_leads_delete ON leads
    FOR DELETE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND current_user_hierarchy_level() <= 2
    );

-- ── opportunities ──
ALTER TABLE opportunities ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS rls_opportunities_select ON opportunities;
CREATE POLICY rls_opportunities_select ON opportunities
    FOR SELECT
    USING (
        current_user_hierarchy_level() IS NULL
        OR current_user_hierarchy_level() <= 2
        OR owner_id = current_user_id()
    );

DROP POLICY IF EXISTS rls_opportunities_insert ON opportunities;
CREATE POLICY rls_opportunities_insert ON opportunities
    FOR INSERT
    WITH CHECK (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2
            OR owner_id = current_user_id()
        )
    );

DROP POLICY IF EXISTS rls_opportunities_update ON opportunities;
CREATE POLICY rls_opportunities_update ON opportunities
    FOR UPDATE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2
            OR owner_id = current_user_id()
        )
    );

DROP POLICY IF EXISTS rls_opportunities_delete ON opportunities;
CREATE POLICY rls_opportunities_delete ON opportunities
    FOR DELETE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND current_user_hierarchy_level() <= 2
    );

-- ── communications ──
ALTER TABLE communications ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS rls_communications_select ON communications;
CREATE POLICY rls_communications_select ON communications
    FOR SELECT
    USING (
        current_user_hierarchy_level() IS NULL
        OR current_user_hierarchy_level() <= 2
        OR created_by = current_user_id()
    );

DROP POLICY IF EXISTS rls_communications_insert ON communications;
CREATE POLICY rls_communications_insert ON communications
    FOR INSERT
    WITH CHECK (
        current_user_hierarchy_level() IS NOT NULL
    );

DROP POLICY IF EXISTS rls_communications_delete ON communications;
CREATE POLICY rls_communications_delete ON communications
    FOR DELETE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND current_user_hierarchy_level() <= 2
    );

-- ── tasks ──
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS rls_tasks_select ON tasks;
CREATE POLICY rls_tasks_select ON tasks
    FOR SELECT
    USING (
        current_user_hierarchy_level() IS NULL
        OR current_user_hierarchy_level() <= 2
        OR assigned_to = current_user_id()
        OR assigned_by = current_user_id()
    );

DROP POLICY IF EXISTS rls_tasks_update ON tasks;
CREATE POLICY rls_tasks_update ON tasks
    FOR UPDATE
    USING (
        current_user_hierarchy_level() IS NOT NULL
        AND (
            current_user_hierarchy_level() <= 2
            OR assigned_to = current_user_id()
            OR assigned_by = current_user_id()
        )
    );

-- ============================================================================
-- 10. IMMUTABLE AUDIT LOG PROTECTION
-- ============================================================================
-- Prevent deletion or update of audit_logs at the database level.
-- Even SUPER_ADMIN cannot delete historical audit records.
-- ============================================================================

CREATE OR REPLACE FUNCTION prevent_audit_mutation()
RETURNS TRIGGER AS $$
BEGIN
    RAISE EXCEPTION 'IMMUTABLE: audit_logs cannot be modified or deleted. All changes are permanently recorded.';
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS trg_prevent_audit_delete ON audit_logs;
CREATE TRIGGER trg_prevent_audit_delete
    BEFORE DELETE ON audit_logs
    FOR EACH ROW
    EXECUTE FUNCTION prevent_audit_mutation();

DROP TRIGGER IF EXISTS trg_prevent_audit_update ON audit_logs;
CREATE TRIGGER trg_prevent_audit_update
    BEFORE UPDATE ON audit_logs
    FOR EACH ROW
    EXECUTE FUNCTION prevent_audit_mutation();

-- Same protection for database_export_logs
DROP TRIGGER IF EXISTS trg_prevent_export_delete ON database_export_logs;
CREATE TRIGGER trg_prevent_export_delete
    BEFORE DELETE ON database_export_logs
    FOR EACH ROW
    EXECUTE FUNCTION prevent_audit_mutation();

DROP TRIGGER IF EXISTS trg_prevent_export_update ON database_export_logs;
CREATE TRIGGER trg_prevent_export_update
    BEFORE UPDATE ON database_export_logs
    FOR EACH ROW
    EXECUTE FUNCTION prevent_audit_mutation();

-- ============================================================================
-- 11. ENCRYPTION FUNCTIONS
-- ============================================================================
-- Uses pgcrypto's pgp_sym_encrypt / pgp_sym_decrypt with the master key.
-- Master key is stored in master_keys table, fetched by security definer functions.
-- ============================================================================

-- Get the active master decryption key
-- Only callable by SUPER_ADMIN
CREATE OR REPLACE FUNCTION get_master_decryption_key()
RETURNS TEXT AS $$
DECLARE
    key_value TEXT;
    uid UUID;
    user_level INT;
BEGIN
    uid := current_user_id();
    IF uid IS NULL THEN
        RAISE EXCEPTION 'Not authenticated';
    END IF;

    user_level := current_user_hierarchy_level();
    IF user_level IS NULL OR user_level > 1 THEN
        RAISE EXCEPTION 'ACCESS DENIED: Only SUPER_ADMIN can access the master decryption key';
    END IF;

    SELECT mk.key_value INTO key_value
    FROM master_keys mk
    WHERE mk.is_active = TRUE
    ORDER BY mk.created_at DESC
    LIMIT 1;

    RETURN key_value;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Encrypt a plaintext password using the active master key
CREATE OR REPLACE FUNCTION encrypt_password(p_plaintext TEXT)
RETURNS TEXT AS $$
DECLARE
    master_key TEXT;
BEGIN
    SELECT key_value INTO master_key
    FROM master_keys
    WHERE is_active = TRUE
    ORDER BY created_at DESC
    LIMIT 1;

    IF master_key IS NULL THEN
        RAISE EXCEPTION 'No active master key found. Contact SUPER_ADMIN.';
    END IF;

    RETURN encode(
        pgp_sym_encrypt(p_plaintext, master_key, 'compress-algo=2, cipher-algo=aes256'),
        'escape'
    );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Decrypt a password that was encrypted with encrypt_password()
CREATE OR REPLACE FUNCTION decrypt_password(p_encrypted TEXT)
RETURNS TEXT AS $$
DECLARE
    master_key TEXT;
BEGIN
    master_key := get_master_decryption_key();
    RETURN pgp_sym_decrypt(decode(p_encrypted, 'escape'), master_key);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- ============================================================================
-- 12. RLS BYPASS FOR SUPER_ADMIN
-- ============================================================================
-- SUPER_ADMIN bypasses all RLS. This function is used by triggers/policies
-- to check if the current user should bypass restrictions.
-- ============================================================================

CREATE OR REPLACE FUNCTION is_super_admin()
RETURNS BOOLEAN AS $$
BEGIN
    RETURN COALESCE(current_user_hierarchy_level(), 999) <= 1;
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;

-- ============================================================================
-- 13. SEED DATA: Master key (for development/testing)
-- ============================================================================
-- In production, this key should be set via a secure deployment process.
-- The key is stored in the database and encrypted at rest by PostgreSQL.
-- ============================================================================

INSERT INTO master_keys (key_name, key_value, description, created_by)
SELECT
    'MASTER_DECRYPTION_KEY',
    encode(gen_random_bytes(32), 'hex'),
    'Master key for reversible password encryption. Used with pgp_sym_encrypt/decrypt.',
    '00000000-0000-0000-0000-000000000001'
WHERE NOT EXISTS (
    SELECT 1 FROM master_keys WHERE key_name = 'MASTER_DECRYPTION_KEY'
);

-- ============================================================================
-- 14. UPDATE SEED DATA: Encrypt passwords for existing test accounts
-- ============================================================================

UPDATE users SET password_encrypted = encrypt_password('SuperAdmin@2026')
WHERE id = '00000000-0000-0000-0000-000000000001' AND password_encrypted IS NULL;

UPDATE users SET password_encrypted = encrypt_password('AdminAccess@2026')
WHERE id = '00000000-0000-0000-0000-000000000002' AND password_encrypted IS NULL;

UPDATE users SET password_encrypted = encrypt_password('SalesAccess@2026')
WHERE id = '00000000-0000-0000-0000-000000000003' AND password_encrypted IS NULL;

UPDATE users SET password_encrypted = encrypt_password('DevTesting@2026')
WHERE id = '00000000-0000-0000-0000-000000000004' AND password_encrypted IS NULL;

-- ============================================================================
-- FUTURE REQUIREMENT NOTE:
-- If this system is ever exposed publicly, remove reversible password storage
-- immediately. Keep bcrypt only. Delete the password_encrypted column and
-- master_keys table. The MASTER_DECRYPTION_KEY must never be exposed externally.
-- ============================================================================

