package com.cloudhandson.vpdbackoffice.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.model.BmcException;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import com.oracle.bmc.generativeaiinference.model.ChatDetails;
import com.oracle.bmc.generativeaiinference.model.ChatChoice;
import com.oracle.bmc.generativeaiinference.model.BaseChatResponse;
import com.oracle.bmc.generativeaiinference.model.GenericChatRequest;
import com.oracle.bmc.generativeaiinference.model.GenericChatResponse;
import com.oracle.bmc.generativeaiinference.model.JsonSchemaResponseFormat;
import com.oracle.bmc.generativeaiinference.model.OnDemandServingMode;
import com.oracle.bmc.generativeaiinference.model.ResponseJsonSchema;
import com.oracle.bmc.generativeaiinference.model.SystemMessage;
import com.oracle.bmc.generativeaiinference.model.TextContent;
import com.oracle.bmc.generativeaiinference.model.UserMessage;
import com.oracle.bmc.generativeaiinference.requests.ChatRequest;
import com.oracle.bmc.generativeaiinference.responses.ChatResponse;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.springframework.stereotype.Service;
import org.springframework.core.env.Environment;

/**
 * Calls OCI Generative AI through the operator-managed OCI configuration profile.
 *
 * <p>The application neither reads nor stores a private key itself.  OCI's SDK
 * reads the path/profile supplied through the service environment and signs the
 * request.  This client is intentionally limited to non-streaming text chat.
 * It does not execute SQL or hand the model a database connection.</p>
 */
@Service
public class OciGenerativeAiChatClient {

  private final Environment environment;
  private final ObjectMapper objectMapper;

  public OciGenerativeAiChatClient(Environment environment, ObjectMapper objectMapper) {
    this.environment = environment;
    this.objectMapper = objectMapper;
  }

  public boolean configured() {
    return missingConfigurationNames().isEmpty();
  }

  /** Returns setting names only; no configured value or credential is ever exposed. */
  public String configurationHint() {
    List<String> missing = missingConfigurationNames();
    return missing.isEmpty() ? "" : String.join(", ", missing);
  }

  private List<String> missingConfigurationNames() {
    OciSettings settings = settings();
    return Stream.of(
            setting(settings.enabled(), "BACKOFFICE_AI_ENABLED=true"),
            setting("oci".equalsIgnoreCase(settings.provider()), "BACKOFFICE_AI_PROVIDER=oci"),
            setting(hasText(settings.ociConfigFile()), "BACKOFFICE_AI_OCI_CONFIG_FILE"),
            setting(hasText(settings.ociProfile()), "BACKOFFICE_AI_OCI_PROFILE"),
            setting(hasText(settings.ociRegion()), "BACKOFFICE_AI_OCI_REGION"),
            setting(hasText(settings.ociCompartmentId()), "BACKOFFICE_AI_OCI_COMPARTMENT_ID"),
            setting(hasText(settings.baseUrl()), "BACKOFFICE_AI_BASE_URL"),
            setting(hasText(settings.model()), "BACKOFFICE_AI_MODEL"))
        .filter(value -> value != null)
        .toList();
  }

  public String modelName() {
    return settings().model();
  }

  /** Sends a bounded, source-only request and returns the first textual choice. */
  public String chat(String systemPrompt, String userPrompt) {
    if (!configured()) {
      throw new AppException("OCI AI 호출 설정이 없습니다.");
    }

    OciSettings ai = settings();
    try (GenerativeAiInferenceClient client = GenerativeAiInferenceClient.builder()
        .region(ai.ociRegion())
        .build(new ConfigFileAuthenticationDetailsProvider(ai.ociConfigFile(), ai.ociProfile()))) {
      client.setEndpoint(ai.baseUrl());
      ChatResponse response = client.chat(ChatRequest.builder()
          .chatDetails(ChatDetails.builder()
              .compartmentId(ai.ociCompartmentId())
              .servingMode(OnDemandServingMode.builder().modelId(ai.model()).build())
              .chatRequest(GenericChatRequest.builder()
                  .messages(List.of(
                      SystemMessage.builder().content(List.of(text(systemPrompt))).build(),
                      UserMessage.builder().content(List.of(text(userPrompt))).build()))
                  // SQL source plus block-level commentary can exceed 1,200
                  // completion tokens.  GPT-5.5 can consume reasoning tokens
                  // before producing text, so leave a bounded 4K response budget.
                  .maxCompletionTokens(4_096)
                  // GPT-5.5 rejects temperature. The verified PoC route sends
                  // no temperature and explicitly requests one non-streaming response.
                  .isStream(false)
                  // Mirror the working PoC route: force a named JSON field so
                  // GPT-5.5 returns final text rather than only reasoning output.
                  .responseFormat(explanationResponseFormat())
                  .build())
              .build())
          .build());
      return extractText(response);
    } catch (AppException exception) {
      throw exception;
    } catch (BmcException exception) {
      // Keep the operational hint useful without returning OCI's raw body,
      // request IDs, request content, or any authentication detail to a browser.
      throw new AppException("OCI Generative AI 설명 호출에 실패했습니다 (HTTP "
          + exception.getStatusCode() + ").");
    } catch (Exception exception) {
      throw new AppException("OCI Generative AI 설명 호출에 실패했습니다 ("
          + exception.getClass().getSimpleName() + ").");
    }
  }

  private TextContent text(String value) {
    return TextContent.builder().text(value).build();
  }

  private String extractText(ChatResponse response) {
    if (response == null || response.getChatResult() == null) {
      throw new AppException("OCI Generative AI 응답 본문이 없습니다.");
    }
    BaseChatResponse baseResponse = response.getChatResult().getChatResponse();
    if (!(baseResponse instanceof GenericChatResponse generic)) {
      throw new AppException("OCI Generative AI 응답 형식이 예상과 다릅니다 ("
          + safeType(baseResponse) + ").");
    }
    if (generic.getChoices() == null || generic.getChoices().isEmpty()) {
      throw new AppException("OCI Generative AI 응답에 선택 결과가 없습니다.");
    }
    ChatChoice choice = generic.getChoices().getFirst();
    if (choice.getMessage() == null || choice.getMessage().getContent() == null) {
      throw new AppException("OCI Generative AI 응답에 메시지 콘텐츠가 없습니다.");
    }
    List<?> contents = choice.getMessage().getContent();
    String result = contents.stream()
        .filter(TextContent.class::isInstance)
        .map(TextContent.class::cast)
        .map(TextContent::getText)
        .filter(this::hasText)
        .reduce("", String::concat);
    if (result.isBlank()) {
      throw new AppException("OCI Generative AI 응답에 텍스트가 없습니다 (콘텐츠: "
          + contents.stream().map(this::safeType).distinct().reduce((left, right) -> left + ", " + right)
              .orElse("없음") + ", 종료: " + safeFinishReason(choice.getFinishReason()) + ").");
    }
    return explanationFromJson(result);
  }

  private JsonSchemaResponseFormat explanationResponseFormat() {
    Map<String, Object> schema = Map.of(
        "type", "object",
        "properties", Map.of("explanation", Map.of("type", "string")),
        "required", List.of("explanation"),
        "additionalProperties", false
    );
    return JsonSchemaResponseFormat.builder()
        .jsonSchema(ResponseJsonSchema.builder()
            .name("security_sql_explanation")
            .description("Markdown explanation for an approved read-only security SQL script")
            .schema(schema)
            .isStrict(true)
            .build())
        .build();
  }

  private String explanationFromJson(String json) {
    try {
      JsonNode explanation = objectMapper.readTree(json).path("explanation");
      if (explanation.isTextual() && !explanation.asText().isBlank()) {
        return explanation.asText();
      }
    } catch (Exception ignored) {
      // Fall through to the safe, actionable message below. The model output
      // is untrusted and is never echoed into an exception message.
    }
    throw new AppException("OCI Generative AI 응답에 explanation JSON 필드가 없습니다.");
  }

  private boolean hasText(String value) {
    return value != null && !value.isBlank();
  }

  private String setting(boolean configured, String name) {
    return configured ? null : name;
  }

  private String safeType(Object value) {
    return value == null ? "없음" : value.getClass().getSimpleName();
  }

  private String safeFinishReason(String value) {
    return value == null || value.isBlank() ? "없음" : value.replaceAll("[^A-Za-z0-9_-]", "");
  }

  /**
   * Reads the service environment directly. This is deliberate: OCI SDK API-key
   * configuration is supplied by the operator's .env/EnvironmentFile, not by
   * database data or a browser request. Values are never rendered or logged.
   */
  private OciSettings settings() {
    return new OciSettings(
        Boolean.parseBoolean(environment.getProperty("BACKOFFICE_AI_ENABLED", "false")),
        environment.getProperty("BACKOFFICE_AI_PROVIDER", ""),
        environment.getProperty("BACKOFFICE_AI_BASE_URL", ""),
        environment.getProperty("BACKOFFICE_AI_MODEL", ""),
        environment.getProperty("BACKOFFICE_AI_OCI_CONFIG_FILE", ""),
        environment.getProperty("BACKOFFICE_AI_OCI_PROFILE", ""),
        environment.getProperty("BACKOFFICE_AI_OCI_REGION", ""),
        environment.getProperty("BACKOFFICE_AI_OCI_COMPARTMENT_ID", "")
    );
  }

  private record OciSettings(
      boolean enabled,
      String provider,
      String baseUrl,
      String model,
      String ociConfigFile,
      String ociProfile,
      String ociRegion,
      String ociCompartmentId
  ) {
  }
}
