package com.cloudhandson.vpdbackoffice.web;

import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand;
import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.service.AppException;
import com.cloudhandson.vpdbackoffice.service.GroupService;
import com.cloudhandson.vpdbackoffice.service.PermissionService;
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
import com.cloudhandson.vpdbackoffice.service.UserService;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
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.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class PermissionController {

  private static final Logger log = LoggerFactory.getLogger(PermissionController.class);
  private final PermissionService permissionService;
  private final ProtectedObjectService protectedObjectService;
  private final UserService userService;
  private final GroupService groupService;

  public PermissionController(
      PermissionService permissionService,
      ProtectedObjectService protectedObjectService,
      UserService userService,
      GroupService groupService
  ) {
    this.permissionService = permissionService;
    this.protectedObjectService = protectedObjectService;
    this.userService = userService;
    this.groupService = groupService;
  }

  @GetMapping("/permissions/object-columns")
  @ResponseBody
  public List<String> objectColumns(@RequestParam String objectRef) {
    if (objectRef == null || objectRef.isBlank()) {
      return List.of();
    }
    if (objectRef.startsWith("protected:")) {
      long objectId;
      try {
        objectId = Long.parseLong(objectRef.substring("protected:".length()));
      } catch (NumberFormatException exception) {
        return List.of();
      }
      return protectedObjectService.findColumns(objectId).stream()
          .map(column -> column.columnName())
          .toList();
    }
    if (objectRef.startsWith("db:")) {
      String value = objectRef.substring("db:".length());
      int dot = value.indexOf('.');
      if (dot < 1 || dot == value.length() - 1) {
        return List.of();
      }
      return protectedObjectService.findDatabaseColumns(value.substring(0, dot), value.substring(dot + 1));
    }
    return List.of();
  }

  @GetMapping("/permissions")
  public String permissions(Model model) {
    long started = System.nanoTime();
    var objects = protectedObjectService.findEnabledWithPermissions();
    long objectsAt = System.nanoTime();
    var roles = permissionService.findRoles();
    long rolesAt = System.nanoTime();
    var roleImpact = buildRoleImpact(roles);
    long roleImpactAt = System.nanoTime();
    var protectedColumnsByObject = protectedObjectService.findColumnsByObjectIds(
        objects.stream().map(object -> object.objectId()).toList()
    );
    var columnsByObject = objects.stream()
        .collect(Collectors.toMap(
            object -> object.objectId(),
            object -> protectedColumnsByObject.getOrDefault(object.objectId(), List.of()).stream()
                .map(column -> column.columnName())
                .toList()
        ));
    var maskableColumnsByObject = objects.stream()
        .collect(Collectors.toMap(
            object -> object.objectId(),
            object -> protectedColumnsByObject.getOrDefault(object.objectId(), List.of()).stream()
                .filter(ProtectedColumn::sensitive)
                .map(column -> column.columnName())
                .toList()
        ));
    var maskableColumnLabelsByObject = objects.stream()
        .collect(Collectors.toMap(
            object -> object.objectId(),
            object -> protectedColumnsByObject.getOrDefault(object.objectId(), List.of()).stream()
                .filter(ProtectedColumn::sensitive)
                .map(column -> column.columnName() + " [" + column.policyLabel() + "]")
                .toList()
        ));
    long columnsAt = System.nanoTime();
    var dbObjects = protectedObjectService.findDatabaseObjects();
    long dbObjectsAt = System.nanoTime();
    var permissions = permissionService.findPermissionViews();
    var permissionCountByObject = permissions.stream()
        .filter(permission -> permission.objectId() > 0)
        .collect(Collectors.groupingBy(
            permission -> permission.objectId(),
            Collectors.counting()
        ));
    var lastPermissionByPermissionId = permissions.stream()
        .collect(Collectors.toMap(
            permission -> permission.permissionId(),
            permission -> permission.objectId() > 0
                && permissionCountByObject.getOrDefault(permission.objectId(), 0L) <= 1,
            (left, right) -> left,
            LinkedHashMap::new
        ));
    long permissionsAt = System.nanoTime();
    log.info("permissions page timings: objects={}ms roles={}ms roleImpact={}ms columns={}ms dbObjects={}ms permissions={}ms total={}ms",
        elapsedMillis(started, objectsAt),
        elapsedMillis(objectsAt, rolesAt),
        elapsedMillis(rolesAt, roleImpactAt),
        elapsedMillis(roleImpactAt, columnsAt),
        elapsedMillis(columnsAt, dbObjectsAt),
        elapsedMillis(dbObjectsAt, permissionsAt),
        elapsedMillis(started, permissionsAt));
    model.addAttribute("roles", roles);
    model.addAttribute("objects", objects);
    model.addAttribute("columnsByObject", columnsByObject);
    model.addAttribute("maskableColumnsByObject", maskableColumnsByObject);
    model.addAttribute("maskableColumnLabelsByObject", maskableColumnLabelsByObject);
    model.addAttribute("directUsersByRole", roleImpact.directUsersByRole());
    model.addAttribute("groupsByRole", roleImpact.groupsByRole());
    model.addAttribute("groupUsersByRole", roleImpact.groupUsersByRole());
    model.addAttribute("dbObjects", dbObjects);
    model.addAttribute("permissions", permissions);
    model.addAttribute("lastPermissionByPermissionId", lastPermissionByPermissionId);
    return "permissions";
  }

  private RoleImpact buildRoleImpact(List<AppRole> roles) {
    Map<Long, Set<String>> directUsersByRole = emptyRoleSetMap(roles);
    userService.findUserRoles().forEach(row ->
        directUsersByRole.computeIfAbsent(row.roleId(), ignored -> new LinkedHashSet<>()).add(row.username()));

    Map<Long, Set<String>> groupsByRole = emptyRoleSetMap(roles);
    Map<Long, List<String>> usersByGroup = groupService.findGroupUsers().stream()
        .collect(Collectors.groupingBy(
            row -> row.groupId(),
            LinkedHashMap::new,
            Collectors.mapping(row -> row.username(), Collectors.toList())
        ));
    Map<Long, Set<String>> groupUsersByRole = emptyRoleSetMap(roles);
    groupService.findGroupRoles().forEach(row -> {
      groupsByRole.computeIfAbsent(row.roleId(), ignored -> new LinkedHashSet<>())
          .add(row.groupCode() + " / " + row.groupName());
      groupUsersByRole.computeIfAbsent(row.roleId(), ignored -> new LinkedHashSet<>())
          .addAll(usersByGroup.getOrDefault(row.groupId(), List.of()));
    });

    return new RoleImpact(
        toListMap(directUsersByRole),
        toListMap(groupsByRole),
        toListMap(groupUsersByRole)
    );
  }

  private Map<Long, Set<String>> emptyRoleSetMap(List<AppRole> roles) {
    Map<Long, Set<String>> map = new LinkedHashMap<>();
    roles.forEach(role -> map.put(role.roleId(), new LinkedHashSet<>()));
    return map;
  }

  private Map<Long, List<String>> toListMap(Map<Long, Set<String>> source) {
    Map<Long, List<String>> map = new LinkedHashMap<>();
    source.forEach((key, values) -> map.put(key, new ArrayList<>(values)));
    return map;
  }

  private record RoleImpact(
      Map<Long, List<String>> directUsersByRole,
      Map<Long, List<String>> groupsByRole,
      Map<Long, List<String>> groupUsersByRole
  ) {
  }

  private long elapsedMillis(long from, long to) {
    return (to - from) / 1_000_000;
  }

  @PostMapping("/permissions")
  public String save(
      @RequestParam(required = false) Long roleId,
      @RequestParam(required = false) String objectRef,
      @RequestParam(defaultValue = "ALLOW") String permissionEffect,
      @RequestParam(required = false) List<String> ruleColumn,
      @RequestParam(required = false) List<String> ruleType,
      @RequestParam(required = false) List<String> ruleValue,
      RedirectAttributes redirectAttributes
  ) {
    try {
      if (roleId == null) {
        throw new AppException("권한을 적용할 역할을 선택하세요.");
      }
      long objectId = resolveObjectId(objectRef);
      permissionService.savePermissionSet(new PermissionSetCommand(
          roleId,
          objectId,
          "SELECT",
          permissionEffect,
          buildRules(ruleColumn, ruleType, ruleValue),
          List.of()
      ));
      redirectAttributes.addFlashAttribute("message", "권한을 저장했습니다.");
    } catch (AppException | IllegalArgumentException exception) {
      redirectAttributes.addFlashAttribute("error", exception.getMessage());
    }
    return "redirect:/permissions";
  }

  private List<RuleCommand> buildRules(
      List<String> ruleColumns,
      List<String> ruleTypes,
      List<String> ruleValues
  ) {
    if (ruleTypes == null || ruleTypes.isEmpty()) {
      return List.of();
    }
    return java.util.stream.IntStream.range(0, ruleTypes.size())
        .mapToObj(index -> new RuleCommand(
            valueAt(ruleColumns, index),
            valueAt(ruleTypes, index),
            valueAt(ruleValues, index)
        ))
        .toList();
  }

  private String valueAt(List<String> values, int index) {
    return values == null || index >= values.size() ? null : values.get(index);
  }

  private long resolveObjectId(String objectRef) {
    if (objectRef == null || objectRef.isBlank()) {
      throw new IllegalArgumentException("보호 객체를 선택하세요.");
    }
    if (objectRef.startsWith("protected:")) {
      return Long.parseLong(objectRef.substring("protected:".length()));
    }
    if (objectRef.startsWith("db:")) {
      String value = objectRef.substring("db:".length());
      int dot = value.indexOf('.');
      if (dot < 1 || dot == value.length() - 1) {
        throw new IllegalArgumentException("DB 객체 형식이 올바르지 않습니다.");
      }
      return protectedObjectService.ensureProtectedObject(value.substring(0, dot), value.substring(dot + 1)).objectId();
    }
    throw new IllegalArgumentException("보호 객체 형식이 올바르지 않습니다.");
  }

  @PostMapping("/permissions/delete")
  public String delete(
      @RequestParam long permissionId,
      @RequestParam(defaultValue = "false") boolean confirmImpact,
      RedirectAttributes redirectAttributes
  ) {
    try {
      permissionService.deletePermission(permissionId, confirmImpact);
      redirectAttributes.addFlashAttribute("message", "권한을 삭제했습니다.");
    } catch (AppException exception) {
      redirectAttributes.addFlashAttribute("error", exception.getMessage());
    }
    return "redirect:/permissions";
  }
}
