package com.cloudhandson.vpdbackoffice.web;

import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
import com.cloudhandson.vpdbackoffice.service.AppException;
import com.cloudhandson.vpdbackoffice.service.VpdPolicyService;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.dao.DataAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class VpdPolicyController {

  private static final Logger log = LoggerFactory.getLogger(VpdPolicyController.class);
  private final VpdPolicyService vpdPolicyService;

  public VpdPolicyController(VpdPolicyService vpdPolicyService) {
    this.vpdPolicyService = vpdPolicyService;
  }

  @GetMapping("/vpd-policies")
  public String policies(
      @RequestParam(required = false) String schemaOwner,
      Model model
  ) {
    populatePolicyModel(schemaOwner, false, model);
    return "vpd-policies";
  }

  @GetMapping("/vpd-filter-policies")
  public String filterPolicies(
      @RequestParam(required = false) String schemaOwner,
      Model model
  ) {
    populatePolicyModel(schemaOwner, true, model);
    return "vpd-filter-policies";
  }

  /** Read-only operational view of the default dynamic permission filter. */
  @GetMapping("/vpd-filter-runtime")
  public String filterRuntime(Model model) {
    try {
      List<VpdPolicyView> policies = vpdPolicyService.findPolicies().stream()
          .filter(VpdPolicyView::permissionSystemDefault)
          .toList();
      model.addAttribute("policies", policies);
      if (!policies.isEmpty()) {
        VpdPolicyView filter = policies.get(0);
        model.addAttribute("source", vpdPolicyService.findFunctionSource(
            filter.functionOwner(), filter.packageName(), filter.functionName()));
      }
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      model.addAttribute("runtimeError", message);
      model.addAttribute("policies", List.of());
    } catch (AppException exception) {
      model.addAttribute("errorMessage", exception.getMessage());
      model.addAttribute("policies", List.of());
    }
    return "vpd-filter-runtime";
  }

  private void populatePolicyModel(String schemaOwner, boolean includeFilterEditor, Model model) {
    try {
      long started = System.nanoTime();
      String selectedSchemaOwner = schemaOwner == null ? "" : schemaOwner.trim().toUpperCase();
      List<com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView> policies = vpdPolicyService.findPolicies();
      long policiesAt = System.nanoTime();
      Map<String, String> policyDescriptions = new LinkedHashMap<>();
      if (!includeFilterEditor) {
        policyDescriptions.putAll(vpdPolicyService.findPolicyDescriptionMap());
        policies.forEach(policy -> policyDescriptions.put(
            policy.objectDisplayName() + "|" + policy.policyName(),
            policyDescriptions.getOrDefault(
                policy.objectDisplayName() + "|" + policy.policyName(),
                policy.objectDisplayName() + "에 요청마다 현재 권한체계의 행 접근 조건을 적용하는 "
                    + policy.policyName() + " policy입니다."
            )
        ));
      }
      long policyDescriptionsAt = System.nanoTime();
      model.addAttribute("policies", policies);
      model.addAttribute("policyDescriptions", policyDescriptions);
      model.addAttribute("vpdTargets", includeFilterEditor
          ? List.of()
          : vpdPolicyService.findVpdTargets(selectedSchemaOwner));
      long targetsAt = System.nanoTime();
      model.addAttribute("selectedSchemaOwner", selectedSchemaOwner);
      var formOptions = vpdPolicyService.formOptions();
      long formOptionsAt = System.nanoTime();
      Map<String, String> filterDescriptions = new LinkedHashMap<>();
      filterDescriptions.putAll(vpdPolicyService.findFilterDescriptionMap());
      policies.forEach(policy -> filterDescriptions.putIfAbsent(
          policy.functionOwner() + "|" + policy.functionName(),
          defaultFilterDescription(policy.functionName())
      ));
      if (includeFilterEditor) {
        formOptions.functions().forEach(function -> filterDescriptions.putIfAbsent(
            function.owner() + "|" + function.functionName(),
            defaultFilterDescription(function.functionName())
        ));
      }
      long filterDescriptionsAt = System.nanoTime();
      Map<String, String> filterPredicates = new LinkedHashMap<>();
      if (includeFilterEditor) {
        formOptions.functions().forEach(function -> filterPredicates.put(
            function.owner() + "|" + function.functionName(),
            vpdPolicyService.findFilterPredicate(function.owner(), function.packageName(), function.functionName())
        ));
      }
      long predicatesAt = System.nanoTime();
      model.addAttribute("formOptions", formOptions);
      model.addAttribute("filterDescriptions", filterDescriptions);
      model.addAttribute("filterPredicates", filterPredicates);
      log.info("vpd page timings: editor={} policies={}ms policyDescriptions={}ms targets={}ms formOptions={}ms filterDescriptions={}ms predicates={}ms total={}ms",
          includeFilterEditor,
          elapsedMillis(started, policiesAt),
          elapsedMillis(policiesAt, policyDescriptionsAt),
          elapsedMillis(policyDescriptionsAt, targetsAt),
          elapsedMillis(targetsAt, formOptionsAt),
          elapsedMillis(formOptionsAt, filterDescriptionsAt),
          elapsedMillis(filterDescriptionsAt, predicatesAt),
          elapsedMillis(started, predicatesAt));
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      model.addAttribute("runtimeError", message);
      model.addAttribute("policies", List.of());
      model.addAttribute("vpdTargets", List.of());
      model.addAttribute("selectedSchemaOwner", "");
      model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
      model.addAttribute("policyDescriptions", Map.of());
      model.addAttribute("filterDescriptions", Map.of());
      model.addAttribute("filterPredicates", Map.of());
    } catch (AppException exception) {
      model.addAttribute("errorMessage", exception.getMessage());
      model.addAttribute("policies", List.of());
      model.addAttribute("vpdTargets", List.of());
      model.addAttribute("selectedSchemaOwner", "");
      model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
      model.addAttribute("policyDescriptions", Map.of());
      model.addAttribute("filterDescriptions", Map.of());
      model.addAttribute("filterPredicates", Map.of());
    }
  }

  private static String defaultFilterDescription(String functionName) {
    return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
        ? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
        : "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.";
  }

  private static long elapsedMillis(long started, long finished) {
    return (finished - started) / 1_000_000;
  }

  @PostMapping("/vpd-policies")
  public String createPolicy(
      @RequestParam String objectKey,
      @RequestParam String policyName,
      @RequestParam(required = false) String functionKey,
      @RequestParam(required = false) String functionOwner,
      @RequestParam(required = false) String functionName,
      @RequestParam(defaultValue = "SELECT") List<String> statementTypes,
      @RequestParam(defaultValue = "false") boolean enabled,
      @RequestParam(defaultValue = "false") boolean updateCheck,
      @RequestParam(required = false) String filterPredicate,
      RedirectAttributes redirectAttributes
  ) {
    createPolicyInternal(objectKey, policyName, functionKey, functionOwner, functionName, statementTypes, enabled,
        updateCheck, filterPredicate, redirectAttributes);
    return "redirect:/vpd-policies";
  }

  @PostMapping("/vpd-policies/default")
  public String createDefaultPolicy(
      @RequestParam String objectKey,
      RedirectAttributes redirectAttributes
  ) {
    try {
      vpdPolicyService.createDefaultPermissionPolicy(objectKey);
      redirectAttributes.addFlashAttribute(
          "successMessage",
          "권한체계 자동 필터를 연결했습니다: " + objectKey
      );
    } catch (AppException exception) {
      redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      redirectAttributes.addFlashAttribute("errorMessage", message.message());
    }
    return "redirect:/vpd-policies";
  }

  @PostMapping("/vpd-filter-policies/filters")
  public String saveFilter(
      @RequestParam(required = false) String functionOwner,
      @RequestParam String functionName,
      @RequestParam String filterPredicate,
      @RequestParam(defaultValue = "Filter function이 반환하는 predicate로 조회 행을 제한합니다.") String description,
      RedirectAttributes redirectAttributes
  ) {
    try {
      vpdPolicyService.saveFilterFunction(functionOwner, functionName, filterPredicate);
      vpdPolicyService.saveFilterDescription(
          functionOwner == null || functionOwner.isBlank()
              ? vpdPolicyService.currentUser()
              : functionOwner,
          functionName,
          description
      );
      redirectAttributes.addFlashAttribute("successMessage", "Filter function을 저장했습니다: " + functionName);
    } catch (AppException exception) {
      redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      redirectAttributes.addFlashAttribute("errorMessage", message.message());
    }
    return "redirect:/vpd-filter-policies";
  }

  @PostMapping("/vpd-policies/description")
  public String savePolicyDescription(
      @RequestParam String objectOwner,
      @RequestParam String objectName,
      @RequestParam String policyName,
      @RequestParam String description,
      RedirectAttributes redirectAttributes
  ) {
    try {
      vpdPolicyService.savePolicyDescription(objectOwner, objectName, policyName, description);
      redirectAttributes.addFlashAttribute("successMessage", "Policy 설명을 저장했습니다.");
    } catch (AppException exception) {
      redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
    }
    return "redirect:/vpd-policies";
  }

  @PostMapping("/vpd-filter-policies/replace")
  public String replaceFilterPolicy(
      @RequestParam String oldObjectKey,
      @RequestParam String oldPolicyName,
      @RequestParam String objectKey,
      @RequestParam String policyName,
      @RequestParam(required = false) String functionKey,
      @RequestParam(required = false) String functionOwner,
      @RequestParam(required = false) String functionName,
      @RequestParam(defaultValue = "SELECT") List<String> statementTypes,
      @RequestParam(defaultValue = "false") boolean enabled,
      @RequestParam(defaultValue = "false") boolean updateCheck,
      @RequestParam(required = false) String filterPredicate,
      RedirectAttributes redirectAttributes
  ) {
    try {
      String[] objectParts = objectKey.split("\\.", 2);
      if (objectParts.length != 2) {
        throw new AppException("조회 대상 형식이 올바르지 않습니다: " + objectKey);
      }
      vpdPolicyService.replacePolicy(oldObjectKey, oldPolicyName, new VpdPolicyCreateCommand(
          objectParts[0],
          objectParts[1],
          policyName,
          functionKey,
          functionOwner,
          functionName,
          String.join(",", statementTypes),
          enabled,
          updateCheck,
          filterPredicate
      ));
      redirectAttributes.addFlashAttribute("successMessage", "Policy를 수정했습니다: " + policyName);
    } catch (AppException exception) {
      redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      redirectAttributes.addFlashAttribute("errorMessage", message.message());
    }
    return "redirect:/vpd-filter-policies";
  }

  private void createPolicyInternal(
      String objectKey,
      String policyName,
      String functionKey,
      String functionOwner,
      String functionName,
      List<String> statementTypes,
      boolean enabled,
      boolean updateCheck,
      String filterPredicate,
      RedirectAttributes redirectAttributes
  ) {
    try {
      String[] objectParts = objectKey.split("\\.", 2);
      if (objectParts.length != 2) {
        throw new AppException("조회 대상 형식이 올바르지 않습니다: " + objectKey);
      }
      vpdPolicyService.createPolicy(new VpdPolicyCreateCommand(
          objectParts[0],
          objectParts[1],
          policyName,
          functionKey,
          functionOwner,
          functionName,
          String.join(",", statementTypes),
          enabled,
          updateCheck,
          filterPredicate
      ));
      redirectAttributes.addFlashAttribute("successMessage", "VPD policy를 등록했습니다: " + policyName);
    } catch (AppException exception) {
      redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      redirectAttributes.addFlashAttribute("errorMessage", message.message());
    }
  }

  @PostMapping("/vpd-policies/bulk")
  public String bulkApplyPolicy(
      @RequestParam String schemaOwner,
      @RequestParam(defaultValue = "false") boolean includeTables,
      @RequestParam(defaultValue = "false") boolean includeViews,
      @RequestParam(required = false) String policyName,
      @RequestParam(required = false) String functionKey,
      @RequestParam(required = false) String functionOwner,
      @RequestParam(required = false) String functionName,
      @RequestParam(defaultValue = "SELECT") List<String> statementTypes,
      @RequestParam(defaultValue = "false") boolean enabled,
      @RequestParam(defaultValue = "false") boolean updateCheck,
      @RequestParam(required = false) String filterPredicate,
      RedirectAttributes redirectAttributes
  ) {
    bulkApplyPolicyInternal(schemaOwner, includeTables, includeViews, policyName, functionKey, functionOwner, functionName,
        statementTypes, enabled, updateCheck, filterPredicate, redirectAttributes);
    return "redirect:/vpd-policies";
  }

  private void bulkApplyPolicyInternal(
      String schemaOwner,
      boolean includeTables,
      boolean includeViews,
      String policyName,
      String functionKey,
      String functionOwner,
      String functionName,
      List<String> statementTypes,
      boolean enabled,
      boolean updateCheck,
      String filterPredicate,
      RedirectAttributes redirectAttributes
  ) {
    try {
      var result = vpdPolicyService.bulkApplySchema(
          schemaOwner,
          includeTables,
          includeViews,
          policyName,
          functionKey,
          functionOwner,
          functionName,
          String.join(",", statementTypes),
          enabled,
          updateCheck,
          filterPredicate
      );
      redirectAttributes.addFlashAttribute("successMessage", result.summary());
    } catch (AppException exception) {
      redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      redirectAttributes.addFlashAttribute("errorMessage", message.message());
    }
  }

  @GetMapping("/vpd-policies/function-source")
  public String functionSource(
      @RequestParam String owner,
      @RequestParam(required = false) String packageName,
      @RequestParam String functionName,
      Model model
  ) {
    try {
      model.addAttribute("source", vpdPolicyService.findFunctionSource(owner, packageName, functionName));
    } catch (AppException exception) {
      model.addAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      model.addAttribute("errorMessage", message.message());
    }
    return "fragments/vpd-function-source :: source";
  }

  @GetMapping("/vpd-policies/policy-detail")
  public String policyDetail(
      @RequestParam String objectOwner,
      @RequestParam String objectName,
      @RequestParam String policyName,
      Model model
  ) {
    try {
      model.addAttribute("detail", vpdPolicyService.findPolicyDetail(objectOwner, objectName, policyName));
    } catch (AppException exception) {
      model.addAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      model.addAttribute("errorMessage", message.message());
    }
    return "fragments/vpd-policy-detail :: detail";
  }

  @GetMapping("/vpd-policies/object-filter-detail")
  public String objectFilterDetail(
      @RequestParam String objectOwner,
      @RequestParam String objectName,
      Model model
  ) {
    try {
      model.addAttribute("detail", vpdPolicyService.findObjectFilterDetail(objectOwner, objectName));
    } catch (AppException exception) {
      model.addAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      model.addAttribute("errorMessage", message.message());
    }
    return "fragments/vpd-object-filter-detail :: detail";
  }

  @GetMapping("/vpd-policies/policy-explanation")
  public String policyExplanation(
      @RequestParam String objectOwner,
      @RequestParam String objectName,
      @RequestParam String policyName,
      Model model
  ) {
    try {
      model.addAttribute("explanation", vpdPolicyService.explainPolicy(objectOwner, objectName, policyName));
    } catch (AppException exception) {
      model.addAttribute("errorMessage", exception.getMessage());
    } catch (DataAccessException exception) {
      RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
      model.addAttribute("errorMessage", message.message());
    }
    return "fragments/vpd-policy-explanation :: explanation";
  }
}
