-- ============================================================
-- 72_hmm_leave_team_vpd.sql
--
-- HMM HR leave row security:
--   * HMM_HR_MANAGER -> self and directly managed employees
--   * HMM_HR_VIEWER  -> self only
--   * HMM_HR_ADMIN   -> all rows when the role is explicitly assigned
--
-- Run as ADMIN after the HMM access-management schema is installed.
-- This script is idempotent and never stores a plain bearer token.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF

ALTER SESSION DISABLE PARALLEL DML;

PROMPT === 1. Ensuring HMM roles and demo role assignments ===
MERGE INTO hmm_access_roles dst
USING (
  SELECT 1 role_id, 'HMM_HR_VIEWER' role_name,
         'HMM employee self-service leave access' description,
         'PUBLIC' max_sensitivity_level FROM dual
  UNION ALL
  SELECT 2, 'HMM_HR_MANAGER',
         'HMM manager access to self and direct reports',
         'INTERNAL' FROM dual
  UNION ALL
  SELECT 3, 'HMM_HR_ADMIN',
         'HMM HR administrator full leave access',
         'CONFIDENTIAL' FROM dual
) src
ON (dst.role_name = src.role_name)
WHEN MATCHED THEN UPDATE SET
  dst.description = src.description,
  dst.max_sensitivity_level = src.max_sensitivity_level,
  dst.active_yn = 'Y',
  dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
  role_id, role_name, description, max_sensitivity_level,
  active_yn, created_at, updated_at
) VALUES (
  src.role_id, src.role_name, src.description, src.max_sensitivity_level,
  'Y', SYSTIMESTAMP, SYSTIMESTAMP
);

MERGE INTO hmm_employee_access_roles dst
USING (
  SELECT e.employee_id, r.role_id
  FROM hmm_hr_employees e
  JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_MANAGER'
  WHERE e.employee_code = 'E1001'
    AND e.employment_status = 'ACTIVE'
) src
ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id)
WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at)
VALUES (src.employee_id, src.role_id, SYSTIMESTAMP);

MERGE INTO hmm_employee_access_roles dst
USING (
  SELECT e.employee_id, r.role_id
  FROM hmm_hr_employees e
  JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_VIEWER'
  WHERE e.manager_employee_id = (
    SELECT manager.employee_id
    FROM hmm_hr_employees manager
    WHERE manager.employee_code = 'E1001'
  )
    AND e.employment_status = 'ACTIVE'
) src
ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id)
WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at)
VALUES (src.employee_id, src.role_id, SYSTIMESTAMP);

COMMIT;

PROMPT === 2. Ensuring protected leave objects and columns ===
DECLARE
  PROCEDURE ensure_object(
    p_object_name IN VARCHAR2,
    p_description IN VARCHAR2
  ) AS
    v_object_id NUMBER;
  BEGIN
    BEGIN
      SELECT object_id
      INTO v_object_id
      FROM hmm_access_objects
      WHERE owner = 'ADMIN'
        AND object_name = p_object_name;

      UPDATE hmm_access_objects
      SET enabled_yn = 'Y',
          description = p_description,
          ords_path = 'cb-ords/cb-object-query/admin/' || LOWER(p_object_name)
      WHERE object_id = v_object_id;
    EXCEPTION
      WHEN NO_DATA_FOUND THEN
        SELECT NVL(MAX(object_id), 0) + 1
        INTO v_object_id
        FROM hmm_access_objects;

        INSERT INTO hmm_access_objects (
          object_id, owner, object_name, ords_path, enabled_yn, description
        ) VALUES (
          v_object_id,
          'ADMIN',
          p_object_name,
          'cb-ords/cb-object-query/admin/' || LOWER(p_object_name),
          'Y',
          p_description
        );
    END;

    FOR c IN (
      SELECT column_name
      FROM user_tab_columns
      WHERE table_name = p_object_name
      ORDER BY column_id
    ) LOOP
      MERGE INTO hmm_access_object_columns dst
      USING (
        SELECT v_object_id object_id, c.column_name column_name
        FROM dual
      ) src
      ON (
        dst.object_id = src.object_id
        AND dst.column_name = src.column_name
      )
      WHEN NOT MATCHED THEN INSERT (
        column_id, object_id, column_name, sensitive_yn,
        sensitivity_level, redaction_method
      ) VALUES (
        (SELECT NVL(MAX(column_id), 0) + 1 FROM hmm_access_object_columns),
        src.object_id,
        src.column_name,
        'N',
        'PUBLIC',
        'NONE'
      );
    END LOOP;
  END;
BEGIN
  ensure_object(
    'HMM_LEAVE_BALANCES',
    'HMM employee leave balance protected by employee hierarchy'
  );
  ensure_object(
    'HMM_LEAVE_REQUESTS',
    'HMM employee leave requests protected by employee hierarchy'
  );
END;
/

COMMIT;

PROMPT === 3. Ensuring role permission rules ===
DECLARE
  PROCEDURE ensure_permission(
    p_role_name   IN VARCHAR2,
    p_target_name IN VARCHAR2,
    p_rule_type   IN VARCHAR2
  ) AS
    v_role_id NUMBER;
    v_perm_id NUMBER;
  BEGIN
    SELECT role_id
    INTO v_role_id
    FROM hmm_access_roles
    WHERE role_name = p_role_name
      AND active_yn = 'Y';

    BEGIN
      SELECT perm_id
      INTO v_perm_id
      FROM hmm_access_permissions
      WHERE role_id = v_role_id
        AND target_name = p_target_name
        AND action_name = 'SELECT';

      UPDATE hmm_access_permissions
      SET permission_effect = 'ALLOW'
      WHERE perm_id = v_perm_id;
    EXCEPTION
      WHEN NO_DATA_FOUND THEN
        SELECT cb_permission_seq.NEXTVAL
        INTO v_perm_id
        FROM dual;

        INSERT INTO hmm_access_permissions (
          perm_id, role_id, target_name, action_name, permission_effect
        ) VALUES (
          v_perm_id, v_role_id, p_target_name, 'SELECT', 'ALLOW'
        );
    END;

    DELETE FROM hmm_access_permission_rules
    WHERE perm_id = v_perm_id;

    INSERT INTO hmm_access_permission_rules (
      rule_id, perm_id, rule_column, rule_type, rule_value
    ) VALUES (
      cb_permission_rule_seq.NEXTVAL,
      v_perm_id,
      'EMPLOYEE_ID',
      p_rule_type,
      NULL
    );
  END;
BEGIN
  ensure_permission('HMM_HR_VIEWER',  'HMM_LEAVE_BALANCES', 'SELF');
  ensure_permission('HMM_HR_VIEWER',  'HMM_LEAVE_REQUESTS', 'SELF');
  ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_BALANCES', 'MANAGED_TEAM');
  ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_REQUESTS', 'MANAGED_TEAM');
  ensure_permission('HMM_HR_ADMIN',   'HMM_LEAVE_BALANCES', 'ALL');
  ensure_permission('HMM_HR_ADMIN',   'HMM_LEAVE_REQUESTS', 'ALL');
END;
/

COMMIT;

PROMPT === 4. Creating secure HMM access context ===
CREATE OR REPLACE PACKAGE hmm_access_ctx_pkg AUTHID DEFINER AS
  PROCEDURE clear_user;
  PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2);
END;
/

CREATE OR REPLACE PACKAGE BODY hmm_access_ctx_pkg AS
  PROCEDURE clear_user AS
  BEGIN
    DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID', NULL);
    DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_CODE', NULL);
    DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'TEAM_ID', NULL);
  END;

  PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2) AS
    v_employee_id   hmm_hr_employees.employee_id%TYPE;
    v_employee_code hmm_hr_employees.employee_code%TYPE;
    v_team_id       hmm_hr_employees.team_id%TYPE;
  BEGIN
    IF p_bearer_token IS NULL OR TRIM(p_bearer_token) IS NULL THEN
      clear_user;
      RAISE_APPLICATION_ERROR(-20101, 'Bearer token is required');
    END IF;

    SELECT e.employee_id,
           e.employee_code,
           e.team_id
    INTO v_employee_id,
         v_employee_code,
         v_team_id
    FROM hmm_access_bearer_tokens t
    JOIN hmm_hr_employees e
      ON e.employee_id = t.employee_id
    WHERE t.key_hash = STANDARD_HASH(p_bearer_token, 'SHA256')
      AND t.revoked_at IS NULL
      AND t.expires_at > CAST(SYSTIMESTAMP AS TIMESTAMP)
      AND e.employment_status = 'ACTIVE';

    DBMS_SESSION.SET_CONTEXT(
      'HMM_ACCESS_CTX', 'EMPLOYEE_ID', TO_CHAR(v_employee_id)
    );
    DBMS_SESSION.SET_CONTEXT(
      'HMM_ACCESS_CTX', 'EMPLOYEE_CODE', v_employee_code
    );
    DBMS_SESSION.SET_CONTEXT(
      'HMM_ACCESS_CTX', 'TEAM_ID', TO_CHAR(v_team_id)
    );
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      clear_user;
      RAISE_APPLICATION_ERROR(-20102, 'Invalid or expired bearer token');
    WHEN TOO_MANY_ROWS THEN
      clear_user;
      RAISE_APPLICATION_ERROR(-20103, 'Bearer token mapping is not unique');
  END;
END;
/

CREATE OR REPLACE CONTEXT hmm_access_ctx USING hmm_access_ctx_pkg;

PROMPT === 5. Creating the HMM leave VPD filter ===
CREATE OR REPLACE FUNCTION hmm_leave_vpd_filter(
  p_schema IN VARCHAR2,
  p_object IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
  v_employee_id         NUMBER;
  v_target              VARCHAR2(128);
  v_allow_predicate     VARCHAR2(32767);
  v_deny_predicate      VARCHAR2(32767);
  v_permission_clause   VARCHAR2(32767);

  FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS
  BEGIN
    RETURN '''' || REPLACE(NVL(p_value, ''), '''', '''''') || '''';
  END;

  FUNCTION safe_column(p_column_name IN VARCHAR2) RETURN VARCHAR2 IS
    v_column VARCHAR2(128);
    v_count  NUMBER;
  BEGIN
    v_column := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(TRIM(p_column_name)));

    SELECT COUNT(*)
    INTO v_count
    FROM all_tab_columns
    WHERE owner = UPPER(p_schema)
      AND table_name = UPPER(p_object)
      AND column_name = v_column;

    RETURN CASE WHEN v_count = 1 THEN v_column ELSE NULL END;
  EXCEPTION
    WHEN OTHERS THEN
      RETURN NULL;
  END;

  PROCEDURE append_and(p_clause IN VARCHAR2) IS
  BEGIN
    IF p_clause IS NULL THEN
      v_permission_clause := NULL;
    ELSIF v_permission_clause IS NULL THEN
      v_permission_clause := '(' || p_clause || ')';
    ELSE
      v_permission_clause := v_permission_clause || ' AND (' || p_clause || ')';
    END IF;
  END;

  PROCEDURE append_permission(
    p_effect IN VARCHAR2,
    p_clause IN VARCHAR2
  ) IS
  BEGIN
    IF p_clause IS NULL THEN
      RETURN;
    END IF;

    IF p_effect = 'DENY' THEN
      IF v_deny_predicate IS NULL THEN
        v_deny_predicate := '(' || p_clause || ')';
      ELSE
        v_deny_predicate := v_deny_predicate || ' OR (' || p_clause || ')';
      END IF;
    ELSIF v_allow_predicate IS NULL THEN
      v_allow_predicate := '(' || p_clause || ')';
    ELSE
      v_allow_predicate := v_allow_predicate || ' OR (' || p_clause || ')';
    END IF;
  END;
BEGIN
  BEGIN
    v_employee_id := TO_NUMBER(
      SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')
    );
  EXCEPTION
    WHEN OTHERS THEN
      RETURN '1 = 0';
  END;

  IF v_employee_id IS NULL OR p_schema IS NULL OR p_object IS NULL THEN
    RETURN '1 = 0';
  END IF;

  v_target := UPPER(TRIM(p_object));
  IF v_target NOT IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS') THEN
    RETURN '1 = 0';
  END IF;

  FOR permission_row IN (
    SELECT DISTINCT
           p.perm_id,
           NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') permission_effect
    FROM (
      SELECT er.role_id
      FROM hmm_employee_access_roles er
      WHERE er.employee_id = v_employee_id
      UNION
      SELECT gr.role_id
      FROM hmm_access_group_members gm
      JOIN hmm_access_groups g
        ON g.group_id = gm.group_id
       AND g.active_yn = 'Y'
      JOIN hmm_group_access_roles gr
        ON gr.group_id = gm.group_id
      WHERE gm.employee_id = v_employee_id
    ) effective_role
    JOIN hmm_access_roles role
      ON role.role_id = effective_role.role_id
     AND role.active_yn = 'Y'
    JOIN hmm_access_permissions p
      ON p.role_id = role.role_id
     AND p.target_name = v_target
     AND p.action_name = 'SELECT'
    ORDER BY p.perm_id
  ) LOOP
    v_permission_clause := NULL;

    FOR rule_row IN (
      SELECT UPPER(TRIM(rule_type)) rule_type,
             UPPER(TRIM(rule_column)) rule_column,
             rule_value
      FROM hmm_access_permission_rules
      WHERE perm_id = permission_row.perm_id
      ORDER BY rule_id
    ) LOOP
      DECLARE
        v_column VARCHAR2(128);
        v_clause VARCHAR2(32767);
      BEGIN
        v_column := safe_column(NVL(rule_row.rule_column, 'EMPLOYEE_ID'));
        v_clause := NULL;

        IF rule_row.rule_type = 'ALL' THEN
          v_clause := '1 = 1';
        ELSIF rule_row.rule_type = 'SELF' AND v_column IS NOT NULL THEN
          v_clause := v_column
            || ' = TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))';
        ELSIF rule_row.rule_type = 'MANAGED_TEAM' AND v_column IS NOT NULL THEN
          v_clause := v_column || ' IN ('
            || 'SELECT employee.employee_id '
            || 'FROM ADMIN.HMM_HR_EMPLOYEES employee '
            || 'WHERE employee.employee_id = '
            || 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID'')) '
            || 'OR employee.manager_employee_id = '
            || 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))'
            || ')';
        ELSIF rule_row.rule_type = '='
              AND v_column IS NOT NULL
              AND rule_row.rule_value IS NOT NULL THEN
          v_clause := 'TO_CHAR(' || v_column || ') = '
            || quote_literal(rule_row.rule_value);
        ELSIF rule_row.rule_type IN ('!=', '<>')
              AND v_column IS NOT NULL
              AND rule_row.rule_value IS NOT NULL THEN
          v_clause := 'TO_CHAR(' || v_column || ') <> '
            || quote_literal(rule_row.rule_value);
        END IF;

        IF v_clause IS NULL THEN
          v_permission_clause := NULL;
          EXIT;
        END IF;
        append_and(v_clause);
      END;
    END LOOP;

    append_permission(
      permission_row.permission_effect,
      v_permission_clause
    );
  END LOOP;

  IF v_allow_predicate IS NULL THEN
    RETURN '1 = 0';
  END IF;

  IF v_deny_predicate IS NULL THEN
    RETURN v_allow_predicate;
  END IF;

  RETURN '((' || v_allow_predicate || ') AND NOT ('
    || v_deny_predicate || '))';
EXCEPTION
  WHEN OTHERS THEN
    RETURN '1 = 0';
END;
/

SHOW ERRORS PACKAGE hmm_access_ctx_pkg
SHOW ERRORS PACKAGE BODY hmm_access_ctx_pkg
SHOW ERRORS FUNCTION hmm_leave_vpd_filter

PROMPT === 6. Attaching context-sensitive VPD policies ===
DECLARE
  PROCEDURE replace_policy(p_object_name IN VARCHAR2) AS
  BEGIN
    BEGIN
      DBMS_RLS.DROP_POLICY(
        object_schema => 'ADMIN',
        object_name => p_object_name,
        policy_name => 'HMM_LEAVE_SCOPE_POLICY'
      );
    EXCEPTION
      WHEN OTHERS THEN
        IF SQLCODE != -28102 THEN
          RAISE;
        END IF;
    END;

    DBMS_RLS.ADD_POLICY(
      object_schema => 'ADMIN',
      object_name => p_object_name,
      policy_name => 'HMM_LEAVE_SCOPE_POLICY',
      function_schema => 'ADMIN',
      policy_function => 'HMM_LEAVE_VPD_FILTER',
      statement_types => 'SELECT',
      update_check => FALSE,
      enable => TRUE,
      policy_type => DBMS_RLS.CONTEXT_SENSITIVE
    );
  END;
BEGIN
  replace_policy('HMM_LEAVE_BALANCES');
  replace_policy('HMM_LEAVE_REQUESTS');
END;
/

PROMPT === 7. Recording backoffice descriptions ===
MERGE INTO hmm_access_vpd_filter_notes dst
USING (
  SELECT 'ADMIN' function_owner,
         'HMM_LEAVE_VPD_FILTER' function_name,
         'Builds self, managed-team, and administrator leave predicates from HMM roles and access rules.'
           description
  FROM dual
) src
ON (
  dst.function_owner = src.function_owner
  AND dst.function_name = src.function_name
)
WHEN MATCHED THEN UPDATE SET
  dst.description = src.description,
  dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
  function_owner, function_name, description, updated_at
) VALUES (
  src.function_owner, src.function_name, src.description, SYSTIMESTAMP
);

MERGE INTO hmm_access_vpd_policy_notes dst
USING (
  SELECT 'ADMIN' object_owner,
         object_name,
         'HMM_LEAVE_SCOPE_POLICY' policy_name,
         'Restricts leave rows to self, direct reports, or explicit HR administrator access.'
           description
  FROM (
    SELECT 'HMM_LEAVE_BALANCES' object_name FROM dual
    UNION ALL
    SELECT 'HMM_LEAVE_REQUESTS' FROM dual
  )
) src
ON (
  dst.object_owner = src.object_owner
  AND dst.object_name = src.object_name
  AND dst.policy_name = src.policy_name
)
WHEN MATCHED THEN UPDATE SET
  dst.description = src.description,
  dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
  object_owner, object_name, policy_name, description, updated_at
) VALUES (
  src.object_owner, src.object_name, src.policy_name,
  src.description, SYSTIMESTAMP
);

COMMIT;

PROMPT === 8. Deployment inventory ===
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN (
  'HMM_ACCESS_CTX',
  'HMM_ACCESS_CTX_PKG',
  'HMM_LEAVE_VPD_FILTER'
)
ORDER BY object_name, object_type;

SELECT object_name, policy_name, function, sel, enable, policy_type
FROM all_policies
WHERE object_owner = 'ADMIN'
  AND object_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
  AND policy_name = 'HMM_LEAVE_SCOPE_POLICY'
ORDER BY object_name;

SELECT e.employee_code, r.role_name
FROM hmm_employee_access_roles er
JOIN hmm_hr_employees e ON e.employee_id = er.employee_id
JOIN hmm_access_roles r ON r.role_id = er.role_id
WHERE r.role_name IN ('HMM_HR_VIEWER', 'HMM_HR_MANAGER')
ORDER BY e.employee_code, r.role_name;

SELECT r.role_name,
       p.target_name,
       pr.rule_column,
       pr.rule_type
FROM hmm_access_permissions p
JOIN hmm_access_roles r ON r.role_id = p.role_id
JOIN hmm_access_permission_rules pr ON pr.perm_id = p.perm_id
WHERE p.target_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
ORDER BY r.role_name, p.target_name;

PROMPT === HMM leave team VPD ready ===
