package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.config.SecuritySqlScriptProperties;
import com.cloudhandson.vpdbackoffice.domain.securityscript.SecuritySqlScript;
import com.cloudhandson.vpdbackoffice.domain.securityscript.SecuritySqlScriptSummary;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

/**
 * Read-only catalogue of security deployment SQL bundled from the Git-tracked
 * sql/adb directory. Script ids are an application whitelist: request input
 * never becomes a filesystem or classpath path.
 */
@Service
public class SecuritySqlScriptService {

  private static final Pattern SCRIPT_ID = Pattern.compile("[a-z][a-z0-9-]{0,63}");
  private static final Pattern RESOURCE_PATH = Pattern.compile(
      "(?:[A-Za-z0-9][A-Za-z0-9_-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*\\.sql"
  );
  private final List<ScriptDefinition> scripts;

  public SecuritySqlScriptService(SecuritySqlScriptProperties properties, ObjectMapper objectMapper) {
    scripts = parse(properties.scripts(), objectMapper);
  }

  public List<SecuritySqlScriptSummary> list() {
    return scripts.stream()
        .map(definition -> new SecuritySqlScriptSummary(
            definition.scriptId(),
            definition.category(),
            definition.fileName(),
            definition.title(),
            definition.description()
        ))
        .toList();
  }

  public SecuritySqlScript find(String scriptId) {
    ScriptDefinition definition = scripts.stream()
        .filter(candidate -> candidate.scriptId().equals(scriptId))
        .findFirst()
        .orElseThrow(() -> new AppException("조회할 수 없는 보안 SQL 스크립트입니다."));
    return new SecuritySqlScript(
        definition.scriptId(),
        definition.category(),
        definition.fileName(),
        definition.title(),
        definition.description(),
        readSource(definition.fileName())
    );
  }

  private String readSource(String fileName) {
    ClassPathResource resource = new ClassPathResource("sql/adb/" + fileName);
    try (InputStream input = resource.getInputStream()) {
      return new String(input.readAllBytes(), StandardCharsets.UTF_8);
    } catch (IOException exception) {
      throw new AppException("배포된 보안 SQL 스크립트를 읽을 수 없습니다: " + fileName);
    }
  }

  private List<ScriptDefinition> parse(String raw, ObjectMapper objectMapper) {
    if (raw == null || raw.isBlank()) {
      return List.of();
    }
    try {
      List<ScriptDefinition> parsed = objectMapper.readValue(raw, new TypeReference<>() {});
      if (parsed.isEmpty() || parsed.stream().map(ScriptDefinition::scriptId).distinct().count() != parsed.size()) {
        throw new IllegalArgumentException();
      }
      parsed.forEach(this::validate);
      return List.copyOf(parsed);
    } catch (Exception exception) {
      throw new IllegalStateException("BACKOFFICE_SECURITY_SQL_SCRIPTS 설정을 확인하세요.", exception);
    }
  }

  private void validate(ScriptDefinition definition) {
    if (definition == null
        || definition.scriptId() == null || !SCRIPT_ID.matcher(definition.scriptId()).matches()
        || definition.fileName() == null || !RESOURCE_PATH.matcher(definition.fileName()).matches()
        || blank(definition.category()) || blank(definition.title()) || blank(definition.description())) {
      throw new IllegalArgumentException();
    }
  }

  private boolean blank(String value) {
    return value == null || value.isBlank();
  }

  public record ScriptDefinition(
      String scriptId,
      String category,
      String fileName,
      String title,
      String description
  ) {
  }
}
