package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
import com.cloudhandson.vpdbackoffice.domain.group.GroupRoleView;
import com.cloudhandson.vpdbackoffice.domain.group.GroupUserView;
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
import com.cloudhandson.vpdbackoffice.domain.token.IssuedToken;
import com.cloudhandson.vpdbackoffice.domain.token.TokenContextView;
import com.cloudhandson.vpdbackoffice.domain.token.TokenIssueCommand;
import com.cloudhandson.vpdbackoffice.domain.stakeholder.StakeholderTokenSubject;
import com.cloudhandson.vpdbackoffice.domain.user.AppUser;
import com.cloudhandson.vpdbackoffice.domain.user.UserRoleView;
import com.cloudhandson.vpdbackoffice.mapper.BearerTokenMapper;
import com.cloudhandson.vpdbackoffice.mapper.GroupMapper;
import com.cloudhandson.vpdbackoffice.mapper.StakeholderMapper;
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
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 org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BearerTokenService {

  private final BearerTokenMapper tokenMapper;
  private final UserMapper userMapper;
  private final GroupMapper groupMapper;
  private final StakeholderMapper stakeholderMapper;
  private final AuditService auditService;
  private final TokenGenerator tokenGenerator;
  private final TokenHasher tokenHasher;
  private final BackofficeProperties properties;
  private final Clock clock;

  public BearerTokenService(
      BearerTokenMapper tokenMapper,
      UserMapper userMapper,
      GroupMapper groupMapper,
      StakeholderMapper stakeholderMapper,
      AuditService auditService,
      TokenGenerator tokenGenerator,
      TokenHasher tokenHasher,
      BackofficeProperties properties,
      Clock clock
  ) {
    this.tokenMapper = tokenMapper;
    this.userMapper = userMapper;
    this.groupMapper = groupMapper;
    this.stakeholderMapper = stakeholderMapper;
    this.auditService = auditService;
    this.tokenGenerator = tokenGenerator;
    this.tokenHasher = tokenHasher;
    this.properties = properties;
    this.clock = clock;
  }

  public List<BearerTokenRecord> findAll() {
    return findAll(false);
  }

  public List<BearerTokenRecord> findAll(boolean includeInactive) {
    return tokenMapper.findAll(includeInactive ? 1 : 0);
  }

  public List<TokenContextView> findTokenContextOptions() {
    List<BearerTokenRecord> tokens = findAll(true);
    List<GroupUserView> groupUsers = groupMapper.findGroupUsers();
    Map<Long, List<String>> directRolesByUser = directRolesByUser();
    Map<Long, List<String>> groupsByUser = groupsByUser(groupUsers);
    Map<Long, List<String>> inheritedRolesByUser = inheritedRolesByUser(groupUsers);
    LocalDateTime now = LocalDateTime.now(clock.withZone(ZoneId.systemDefault()));

    return tokens.stream()
        .map(token -> new TokenContextView(
            token.keyId(),
            token.userId(),
            token.username(),
            token.stakeholderUserId(),
            token.stakeholderRole(),
            token.stakeholderChannel(),
            token.keyPrefix(),
            token.expiresAt(),
            token.revokedAt(),
            token.description(),
            token.active(now),
            directRolesByUser.getOrDefault(token.userId(), List.of()),
            groupsByUser.getOrDefault(token.userId(), List.of()),
            inheritedRolesByUser.getOrDefault(token.userId(), List.of())
        ))
        .toList();
  }

  public TokenContextView findTokenContextByPlainToken(String plainToken) {
    BearerTokenRecord record = findByPlainToken(plainToken);
    if (record == null) {
      return null;
    }
    return findTokenContextOptions().stream()
        .filter(context -> context.keyId() == record.keyId())
        .findFirst()
        .orElse(null);
  }

  public BearerTokenRecord findById(long keyId) {
    return tokenMapper.findById(keyId);
  }

  public BearerTokenRecord findByPlainToken(String plainToken) {
    if (plainToken == null || plainToken.isBlank()) {
      return null;
    }
    return tokenMapper.findByHash(tokenHasher.sha256(plainToken));
  }

  public List<StakeholderTokenSubject> findStakeholderTokenSubjects() {
    return stakeholderMapper.findTokenSubjects();
  }

  public boolean matches(BearerTokenRecord record, String plainToken) {
    if (record == null || plainToken == null || plainToken.isBlank()) {
      return false;
    }
    String hash = tokenHasher.sha256(plainToken);
    return hash.equalsIgnoreCase(record.keyHash());
  }

  private Map<Long, List<String>> directRolesByUser() {
    Map<Long, Set<String>> grouped = new LinkedHashMap<>();
    for (UserRoleView view : userMapper.findUserRoles()) {
      grouped.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.roleName());
    }
    return toListMap(grouped);
  }

  private Map<Long, List<String>> groupsByUser(List<GroupUserView> groupUsers) {
    Map<Long, Set<String>> grouped = new LinkedHashMap<>();
    for (GroupUserView view : groupUsers) {
      grouped.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>())
          .add(view.groupCode() + " / " + view.groupName());
    }
    return toListMap(grouped);
  }

  private Map<Long, List<String>> inheritedRolesByUser(List<GroupUserView> groupUsers) {
    Map<Long, Set<String>> rolesByGroup = new LinkedHashMap<>();
    for (GroupRoleView view : groupMapper.findGroupRoles()) {
      rolesByGroup.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.roleName());
    }

    Map<Long, Set<String>> rolesByUser = new LinkedHashMap<>();
    for (GroupUserView view : groupUsers) {
      Set<String> roles = rolesByGroup.getOrDefault(view.groupId(), Set.of());
      if (!roles.isEmpty()) {
        rolesByUser.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).addAll(roles);
      }
    }
    return toListMap(rolesByUser);
  }

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

  @Transactional
  public IssuedToken issueToken(TokenIssueCommand command) {
    AppUser user = userMapper.findById(command.userId());
    if (user == null) {
      throw new AppException("사용자를 찾을 수 없습니다.");
    }
    if (!user.active()) {
      throw new AppException("비활성 사용자에게는 토큰을 발급할 수 없습니다.");
    }

    return issueForUser(user, null, command.expiresAt(), command.description());
  }

  @Transactional
  public IssuedToken issueStakeholderToken(
      String stakeholderUserId,
      OffsetDateTime expiresAt,
      String description
  ) {
    if (stakeholderUserId == null || stakeholderUserId.isBlank()) {
      throw new AppException("토큰을 발급할 이해당사자를 선택하세요.");
    }
    StakeholderTokenSubject subject = stakeholderMapper.findTokenSubject(stakeholderUserId.trim());
    if (subject == null) {
      throw new AppException("이해당사자 원장과 연결된 활성 사용자를 찾을 수 없습니다.");
    }
    AppUser bridgeUser = userMapper.findById(subject.appUserId());
    if (bridgeUser == null || !bridgeUser.active()) {
      throw new AppException("이해당사자 권한 bridge 사용자가 비활성 상태입니다.");
    }
    return issueForUser(bridgeUser, subject.stakeholderUserId(), expiresAt, description);
  }

  private IssuedToken issueForUser(
      AppUser user,
      String stakeholderUserId,
      OffsetDateTime expiresAt,
      String description
  ) {
    validateExpiry(expiresAt);

    String plainToken = tokenGenerator.generate();
    String prefix = tokenGenerator.prefix(plainToken);
    String hash = tokenHasher.sha256(plainToken);
    long keyId = tokenMapper.nextKeyId();
    LocalDateTime localExpiresAt = expiresAt
        .atZoneSameInstant(ZoneId.systemDefault())
        .toLocalDateTime();

    tokenMapper.insertToken(new BearerTokenRecord(
        keyId,
        user.userId(),
        user.username(),
        stakeholderUserId,
        null,
        null,
        prefix,
        hash,
        localExpiresAt,
        null,
        description
    ));
    auditService.record(new AuditEvent("TOKEN_ISSUED", keyId, null, "SUCCESS", null, null,
        stakeholderUserId == null ? "issued" : "stakeholder=" + stakeholderUserId));
    return new IssuedToken(keyId, prefix, plainToken, expiresAt);
  }

  private void validateExpiry(OffsetDateTime expiresAt) {
    OffsetDateTime now = OffsetDateTime.now(clock);
    if (expiresAt == null || !expiresAt.isAfter(now)) {
      throw new AppException("만료일은 현재 시각 이후여야 합니다.");
    }
    if (expiresAt.isAfter(now.plusDays(properties.token().maxDays()))) {
      throw new AppException("토큰 만료일이 최대 허용 기간을 초과했습니다.");
    }
  }

  @Transactional
  public IssuedToken issueTemporaryToken(long userId, String purpose) {
    OffsetDateTime expiresAt = OffsetDateTime.now(clock).plus(Duration.ofMinutes(10));
    return issueToken(new TokenIssueCommand(userId, expiresAt,
        purpose == null || purpose.isBlank() ? "임시 검증 토큰" : purpose));
  }

  @Transactional
  public void revokeToken(long keyId, String reason) {
    int updated = tokenMapper.revokeToken(
        keyId,
        LocalDateTime.now(clock.withZone(ZoneId.systemDefault())),
        reason
    );
    if (updated == 0) {
      throw new AppException("회수할 활성 토큰을 찾을 수 없습니다.");
    }
    auditService.record(new AuditEvent("TOKEN_REVOKED", keyId, null, "SUCCESS", null, null, reason));
  }
}
