-- OCI GenAI candidate selection. Vector distance only orders candidates; the
-- model may select one supplied game_key or explicitly reject all candidates.
CREATE OR REPLACE FUNCTION sg_game_match_candidate(
  p_mention IN CLOB,
  p_candidates_json IN CLOB
) RETURN CLOB AUTHID DEFINER
IS
  v_prompt CLOB;
  v_result CLOB;
BEGIN
  v_prompt := 'Decide whether the extracted game-name mention refers to exactly '
    || 'one game in the supplied candidate array. Candidate vector distance is '
    || 'retrieval evidence only and must not establish identity. Compare the '
    || 'mention with candidate gameName, aliases, gameId, and gamePrefix. '
    || 'If one candidate clearly identifies the same game, return exactly '
    || '{"status":"MATCHED","game_key":"candidate key","reason":"short reason"}. '
    || 'If none clearly identifies the same game, return exactly '
    || '{"status":"UNMATCHED","game_key":null,"reason":"short reason"}. '
    || 'Never invent a game_key and never return a key absent from the supplied '
    || 'candidate array. Mention: '
    || DBMS_LOB.SUBSTR(p_mention, 1000, 1)
    || ' Candidates: '
    || DBMS_LOB.SUBSTR(p_candidates_json, 12000, 1);
  v_result := DBMS_CLOUD_AI.GENERATE(
    prompt => v_prompt,
    profile_name => 'SGMP_POC_OCI_GPT54MINI',
    action => 'chat'
  );
  RETURN v_result;
END;
/

-- Complete game query planning inside ADB. The MCP server only invokes this
-- function and returns its structured result.
CREATE OR REPLACE FUNCTION sg_game_query_plan(
  p_question IN CLOB,
  p_top_k IN PLS_INTEGER DEFAULT 5
) RETURN CLOB AUTHID DEFINER
IS
  TYPE t_seen_game_keys IS TABLE OF BOOLEAN INDEX BY VARCHAR2(128);

  v_extract_raw CLOB;
  v_extract JSON_OBJECT_T;
  v_mentions JSON_ARRAY_T;
  v_scope_type VARCHAR2(30);
  v_target_type VARCHAR2(20);
  v_targets JSON_ARRAY_T := JSON_ARRAY_T();
  v_supported JSON_ARRAY_T := JSON_ARRAY_T();
  v_unmatched JSON_ARRAY_T := JSON_ARRAY_T();
  v_mention_results JSON_ARRAY_T := JSON_ARRAY_T();
  v_seen_game_keys t_seen_game_keys;
  v_result JSON_OBJECT_T := JSON_OBJECT_T();

  v_mention VARCHAR2(1000);
  v_candidates JSON_ARRAY_T;
  v_candidate JSON_OBJECT_T;
  v_decision_raw CLOB;
  v_decision JSON_OBJECT_T;
  v_decision_status VARCHAR2(30);
  v_selected_key VARCHAR2(128);
  v_reason VARCHAR2(4000);
  v_matched_candidate JSON_OBJECT_T;
  v_mention_result JSON_OBJECT_T;
  v_target JSON_OBJECT_T;
  v_unmatched_item JSON_OBJECT_T;

  v_cursor SYS_REFCURSOR;
  v_game_key VARCHAR2(128);
  v_game_id VARCHAR2(128);
  v_game_prefix VARCHAR2(128);
  v_game_nm VARCHAR2(512);
  v_game_alias_nm VARCHAR2(512);
  v_user_master_object_name VARCHAR2(128);
  v_cosine_distance NUMBER;
BEGIN
  v_extract_raw := sg_game_extract_mentions(p_question);
  v_extract := JSON_OBJECT_T.parse(v_extract_raw);
  v_mentions := v_extract.get_array('game_mentions');
  v_scope_type := NVL(v_extract.get_string('scope_hint'), 'UNKNOWN');

  IF v_scope_type = 'ALL_GAMES' THEN
    v_target_type := 'ALL';
  ELSIF v_mentions.get_size = 0 THEN
    v_target_type := 'NONE';
  ELSIF v_mentions.get_size = 1 THEN
    v_target_type := 'SINGLE';
  ELSE
    v_target_type := 'MULTI';
  END IF;

  IF v_target_type = 'ALL' THEN
    FOR game_row IN (
      SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
             user_master_object_name
        FROM sg_game_catalog
       WHERE active_yn = 'Y'
       ORDER BY priority, game_key
    ) LOOP
      v_target := JSON_OBJECT_T();
      v_target.put_null('mention');
      v_target.put('status', 'CATALOG');
      v_target.put('gameKey', game_row.game_key);
      v_target.put('gameId', game_row.game_id);
      v_target.put('gamePrefix', game_row.game_prefix);
      v_target.put('gameName', game_row.game_nm);
      v_target.put('aliases', game_row.game_alias_nm);
      v_target.put(
        'userMasterObjectName',
        game_row.user_master_object_name
      );
      v_target.put(
        'objectStatus',
        CASE WHEN game_row.user_master_object_name IS NULL
          THEN 'UNAVAILABLE' ELSE 'AVAILABLE' END
      );
      v_target.put_null('cosineDistance');
      v_target.put('reasonCode', 'ALL_GAMES_CATALOG');
      v_target.put('reason', 'Active game returned from the database catalog.');
      v_targets.append(v_target);
      IF game_row.user_master_object_name IS NOT NULL THEN
        v_supported.append(v_target);
      END IF;
    END LOOP;
  ELSIF v_target_type = 'NONE' THEN
    v_target := JSON_OBJECT_T();
    v_target.put_null('mention');
    v_target.put('status', 'NONE');
    v_target.put_null('gameKey');
    v_target.put_null('gameId');
    v_target.put_null('gamePrefix');
    v_target.put_null('gameName');
    v_target.put_null('aliases');
    v_target.put_null('userMasterObjectName');
    v_target.put('objectStatus', 'NOT_APPLICABLE');
    v_target.put_null('cosineDistance');
    v_target.put('reasonCode', 'NO_GAME_TARGET');
    v_target.put(
      'reason',
      'The question does not select a particular game.'
    );
    v_targets.append(v_target);
  ELSE
    FOR i IN 0 .. v_mentions.get_size - 1 LOOP
      v_mention := v_mentions.get_string(i);
      v_candidates := JSON_ARRAY_T();
      v_cursor := sg_game_catalog_search(
        v_mention,
        LEAST(GREATEST(NVL(p_top_k, 5), 1), 20)
      );

      LOOP
        FETCH v_cursor INTO
          v_game_key,
          v_game_id,
          v_game_prefix,
          v_game_nm,
          v_game_alias_nm,
          v_user_master_object_name,
          v_cosine_distance;
        EXIT WHEN v_cursor%NOTFOUND;

        v_candidate := JSON_OBJECT_T();
        v_candidate.put('gameKey', v_game_key);
        v_candidate.put('gameId', v_game_id);
        v_candidate.put('gamePrefix', v_game_prefix);
        v_candidate.put('gameName', v_game_nm);
        v_candidate.put('aliases', v_game_alias_nm);
        v_candidate.put(
          'userMasterObjectName',
          v_user_master_object_name
        );
        v_candidate.put(
          'objectStatus',
          CASE WHEN v_user_master_object_name IS NULL
            THEN 'UNAVAILABLE' ELSE 'AVAILABLE' END
        );
        v_candidate.put('cosineDistance', v_cosine_distance);
        v_candidates.append(v_candidate);
      END LOOP;
      CLOSE v_cursor;

      v_decision_raw := sg_game_match_candidate(
        v_mention,
        v_candidates.to_clob
      );
      v_decision := JSON_OBJECT_T.parse(v_decision_raw);
      v_decision_status := UPPER(
        NVL(v_decision.get_string('status'), 'UNMATCHED')
      );
      v_selected_key := v_decision.get_string('game_key');
      v_reason := v_decision.get_string('reason');
      v_matched_candidate := NULL;

      IF v_decision_status = 'MATCHED' AND v_selected_key IS NOT NULL THEN
        FOR j IN 0 .. v_candidates.get_size - 1 LOOP
          v_candidate := TREAT(v_candidates.get(j) AS JSON_OBJECT_T);
          IF v_candidate.get_string('gameKey') = v_selected_key THEN
            v_matched_candidate := v_candidate;
            EXIT;
          END IF;
        END LOOP;
      END IF;

      v_mention_result := JSON_OBJECT_T();
      v_mention_result.put('mention', v_mention);
      v_mention_result.put('candidateGames', v_candidates);
      v_mention_result.put('reason', v_reason);

      IF v_matched_candidate IS NOT NULL THEN
        v_mention_result.put('status', 'MATCHED');
        v_mention_result.put('reasonCode', 'LLM_CANDIDATE_MATCH');
        v_mention_result.put('matchedGameKey', v_selected_key);

        v_target := JSON_OBJECT_T.parse(v_matched_candidate.to_clob);
        v_target.put('mention', v_mention);
        v_target.put('status', 'MATCHED');
        v_target.put('reasonCode', 'LLM_CANDIDATE_MATCH');
        v_target.put('reason', v_reason);
        v_targets.append(v_target);

        IF NOT v_seen_game_keys.EXISTS(v_selected_key) THEN
          v_supported.append(v_matched_candidate);
          v_seen_game_keys(v_selected_key) := TRUE;
        END IF;
      ELSE
        v_mention_result.put('status', 'UNMATCHED');
        IF v_decision_status = 'MATCHED' THEN
          v_mention_result.put(
            'reasonCode',
            'LLM_SELECTED_UNKNOWN_CANDIDATE'
          );
        ELSE
          v_mention_result.put('reasonCode', 'LLM_NO_CANDIDATE_MATCH');
        END IF;

        v_target := JSON_OBJECT_T();
        v_target.put('mention', v_mention);
        v_target.put('status', 'UNMATCHED');
        v_target.put_null('gameKey');
        v_target.put_null('gameId');
        v_target.put_null('gamePrefix');
        v_target.put_null('gameName');
        v_target.put_null('aliases');
        v_target.put_null('userMasterObjectName');
        v_target.put('objectStatus', 'UNAVAILABLE');
        v_target.put_null('cosineDistance');
        v_target.put(
          'reasonCode',
          v_mention_result.get_string('reasonCode')
        );
        v_target.put('reason', v_reason);
        v_targets.append(v_target);

        v_unmatched_item := JSON_OBJECT_T();
        v_unmatched_item.put('mention', v_mention);
        v_unmatched_item.put(
          'reasonCode',
          v_mention_result.get_string('reasonCode')
        );
        v_unmatched_item.put('reason', v_reason);
        v_unmatched.append(v_unmatched_item);
      END IF;
      v_mention_results.append(v_mention_result);
    END LOOP;
  END IF;

  v_result.put('targetType', v_target_type);
  v_result.put('scopeType', v_target_type);
  v_result.put('extractScopeHint', v_scope_type);
  IF v_target_type = 'NONE' THEN
    v_result.put('status', 'NO_TARGET');
  ELSIF v_target_type = 'ALL' THEN
    v_result.put('status', 'SUPPORTED');
  ELSIF v_supported.get_size = 0 THEN
    v_result.put('status', 'UNMATCHED');
  ELSIF v_unmatched.get_size > 0 THEN
    v_result.put('status', 'PARTIAL');
  ELSE
    v_result.put('status', 'SUPPORTED');
  END IF;
  v_result.put('targets', v_targets);
  v_result.put('matchedGames', v_supported);
  v_result.put('mentionResults', v_mention_results);
  v_result.put('supportedGames', v_supported);
  v_result.put('unmatchedGames', v_unmatched);
  v_result.put('nextAction', 'CALL_FEWSHOT');

  CASE v_target_type
    WHEN 'NONE' THEN v_result.put('executionMode', 'UNSCOPED');
    WHEN 'SINGLE' THEN v_result.put('executionMode', 'SINGLE');
    WHEN 'MULTI' THEN v_result.put('executionMode', 'COMBINED');
    WHEN 'ALL' THEN v_result.put('executionMode', 'ALL');
  END CASE;

  RETURN v_result.to_clob;
END;
/
