package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.domain.securityscript.SecuritySqlScript;
import com.cloudhandson.vpdbackoffice.domain.securityscript.SecuritySqlScriptExplanation;
import java.util.stream.IntStream;
import org.springframework.stereotype.Service;

/**
 * Sends a selected, read-only SQL source to the configured LLM for explanation.
 * The source comes from SecuritySqlScriptService's fixed catalogue; neither a
 * request value nor an LLM response can modify or execute database SQL.
 */
@Service
public class SecuritySqlScriptExplanationService {

  private final SecuritySqlScriptService scriptService;
  private final OciGenerativeAiChatClient aiClient;

  public SecuritySqlScriptExplanationService(
      SecuritySqlScriptService scriptService,
      OciGenerativeAiChatClient aiClient
  ) {
    this.scriptService = scriptService;
    this.aiClient = aiClient;
  }

  public SecuritySqlScriptExplanation explain(String scriptId) {
    SecuritySqlScript script = scriptService.find(scriptId);
    String prompt = buildPrompt(script);
    if (!aiClient.configured()) {
      return new SecuritySqlScriptExplanation(
          "AI_NOT_CONFIGURED",
          aiClient.modelName(),
          "OCI AI 호출 설정이 없어 설명을 생성하지 않았습니다. 누락 또는 불일치: "
              + aiClient.configurationHint(),
          prompt,
          script
      );
    }
    try {
      return new SecuritySqlScriptExplanation(
          "SUCCESS",
          aiClient.modelName(),
          aiClient.chat(systemPrompt(), prompt),
          prompt,
          script
      );
    } catch (AppException exception) {
      return new SecuritySqlScriptExplanation(
          "AI_CALL_FAILED",
          aiClient.modelName(),
          exception.getMessage(),
          prompt,
          script
      );
    } catch (Exception exception) {
      return new SecuritySqlScriptExplanation(
          "AI_CALL_FAILED",
          aiClient.modelName(),
          "AI 설명 호출에 실패했습니다. SQL 원문은 변경되지 않았으며, 잠시 후 다시 시도하세요.",
          prompt,
          script
      );
    }
  }

  private String systemPrompt() {
    return """
        당신은 Oracle 보안 운영 SQL을 검토하는 선임 데이터베이스 보안 엔지니어다.
        제공된 source만 근거로 한국어 Markdown 설명을 작성한다. SQL을 실행·수정·제안된 명령으로 바꾸지 않는다.
        source에 없는 객체·권한·실행 결과를 추측하지 않는다. 비밀값을 요청하거나 출력하지 않는다.
        최종 응답은 반드시 explanation 키 하나에 Markdown 문자열을 담은 JSON 객체로 반환한다.
        """;
  }

  private String buildPrompt(SecuritySqlScript script) {
    return """
        다음은 Git 형상에 저장된 Oracle 보안 SQL 스크립트다. 운영자가 코드를 이해할 수 있도록 전체 설명과 부분별 주석을 작성한다.

        [스크립트 메타데이터]
        - 분류: %s
        - 파일: %s
        - 제목: %s
        - 용도: %s

        [줄 번호가 붙은 SQL 원문]
        %s

        [출력 형식]
        ## 전체 설명
        - 이 스크립트가 만드는/변경하는 DB 객체와 목적을 5줄 이내로 설명한다.
        - 실행 전제조건, 실행 사용자, 다른 스크립트와의 순서가 source에 있으면 명시한다.

        ## 실행 흐름
        - source의 실제 실행 순서를 번호 목록으로 정리한다.

        ## 블록별 주석
        - 주석 heading, PROMPT, CREATE/ALTER/MERGE/GRANT/DECLARE/BEGIN, PACKAGE, PROCEDURE, FUNCTION 단위로 블록을 나눈다.
        - 각 블록은 반드시 `### [L시작-L끝] 블록명` 제목으로 시작한다.
        - 각 블록에서 “무엇을 하는지”, “입력/참조 객체”, “보안·VPD·ASO 영향”, “실패/주의점”을 source 근거가 있는 범위에서 bullet로 적는다.

        ## 토큰 처리·사용자 적용 흐름
        - source에 Bearer/Authorization/auth_header/token/key 또는 token을 받아 context를 설정하는 코드가 있으면, 반드시 “입력 → 검증/조회 → CB_AGENT_CTX 등 context 설정 → VPD/ASO/Select AI 적용 → 실패 시 동작” 순서로 설명한다.
        - 각 단계에는 source line range와 실제 식별자(예: auth_header, set_vpd_context, SYS_CONTEXT)를 붙인다.
        - source가 토큰을 직접 다루지 않는 메타데이터/초기값 스크립트라면 “이 스크립트는 토큰을 직접 검증하거나 사용자별 접근을 판정하지 않는다”라고 명시하고, source 주석/호출 관계에서 확인되는 다음 런타임 단계를 설명한다.
        - VPD는 행 접근, ASO/DBMS_REDACT는 컬럼 표시 보호라는 점을 source 근거가 있는 범위에서 구분한다.
        - 실제 어떤 사용자가 어떤 행·원문 컬럼을 볼지는 토큰으로 설정된 DB context와 당시 권한 데이터에 따라 확정된다고 구분한다.
        - source에 없는 역할명, 사용자명, 권한 결과는 만들지 않는다.

        ## 운영 확인 포인트
        - source에서 직접 확인 가능한 DB 객체·권한·정책·ORDS endpoint를 최대 7개로 정리한다.

        ## 판단 한계
        - source만으로 확인할 수 없는 실행 결과나 권한 효과가 있으면 명시한다.

        [엄격한 규칙]
        - Markdown으로 120줄 이내에 작성한다.
        - 줄 번호와 SQL 객체명은 제공된 source와 일치해야 한다.
        - 일반론이나 추측은 쓰지 않는다.
        """.formatted(
        script.category(),
        script.fileName(),
        script.title(),
        script.description(),
        numberedSource(script.source())
    );
  }

  private String numberedSource(String source) {
    String[] lines = source.split("\\R", -1);
    return IntStream.range(0, lines.length)
        .mapToObj(index -> "%4d | %s".formatted(index + 1, lines[index]))
        .reduce((left, right) -> left + "\n" + right)
        .orElse("");
  }
}
