package com.zioinfo.kintex.auth;

import com.zioinfo.kintex.auth.dto.AcceptInviteRequest;
import com.zioinfo.kintex.auth.dto.LoginRequest;
import com.zioinfo.kintex.auth.dto.LoginResponse;
import com.zioinfo.kintex.auth.dto.MeResponse;
import com.zioinfo.kintex.auth.dto.WorkspaceDto;
import com.zioinfo.kintex.auth.mapper.UserMapper;
import com.zioinfo.kintex.auth.profile.ProfilePhotoService;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.security.TwoFactorService;
import com.zioinfo.kintex.security.dto.SecureLoginResponse;
import org.springframework.stereotype.Service;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 인증 서비스 — UserMapper(인증행·행사 역할) 배선 완료. 로그인·워크스페이스·초대 수락을 처리한다.
 *
 * <p>보안(계약 §0-3): 비밀번호 대조는 위임된 secureLogin(BCrypt)이 수행하며, 해시·내부 식별자는 응답에 미노출.
 * 실패 메시지는 이메일/비밀번호를 구분하지 않는 일반화 문구만 반환한다.
 *
 * <p>레거시 /api/auth/login 도 {@link TwoFactorService#secureLogin}에 위임한다 — 위임하지 않으면
 * 이 경로가 로그인 실패 잠금·OTP 강제를 모두 우회하는 구멍이 된다. OTP 대상 계정은 토큰 없이
 * OTP_REQUIRED 로 거절되며(챌린지 토큰 미반환), 보안 로그인(/login/secure) 플로우만 허용된다.
 */
@Service
public class AuthServiceImpl implements AuthService {

    private final JwtService jwtService;
    private final UserMapper userMapper;
    private final TwoFactorService twoFactorService;

    public AuthServiceImpl(JwtService jwtService, UserMapper userMapper, TwoFactorService twoFactorService) {
        this.jwtService = jwtService;
        this.userMapper = userMapper;
        this.twoFactorService = twoFactorService;
    }

    @Override
    public LoginResponse login(LoginRequest request) {
        SecureLoginResponse secured = twoFactorService.secureLogin(request.email(), request.password());
        if (secured.login() == null) {
            throw new ApiException(ErrorCode.OTP_REQUIRED,
                    "2차 인증이 필요한 계정입니다. 보안 로그인 절차를 이용해 주세요.");
        }
        return secured.login();
    }

    @Override
    public MeResponse me(KintexPrincipal principal) {
        Map<String, Object> p = userMapper.findProfile(principal.userId());
        String email = p == null ? null : str(p.get("email"));
        String phone = p == null ? null : str(p.get("phone"));
        String deptId = p == null ? null : str(p.get("deptId"));
        boolean hasPhoto = p != null && Boolean.TRUE.equals(bool(p.get("hasPhoto")));
        String photoUrl = hasPhoto ? ProfilePhotoService.serveUrl(principal.userId()) : null;
        return new MeResponse(
                principal.userId(), principal.displayName(), principal.eventRoles(),
                principal.hallManager(), principal.roleCode(), principal.tenantId(),
                email, phone, deptId, photoUrl);
    }

    @Override
    public List<WorkspaceDto> listWorkspaces(KintexPrincipal principal) {
        return loadWorkspaces(principal.userId());
    }

    @Override
    public LoginResponse acceptInvite(KintexPrincipal principal, AcceptInviteRequest request) {
        // 장치업체(CONTRACTOR) 초대 수락 시 사업자번호 → 킨텍스 등록업체 검증(미등록 차단, 계약 §M7).
        if (request.companyRegistrationNo() != null && !request.companyRegistrationNo().isBlank()) {
            Boolean registered = userMapper.findCompanyRegistered(request.companyRegistrationNo());
            if (!Boolean.TRUE.equals(registered)) {
                throw new ApiException(ErrorCode.NOT_REGISTERED_COMPANY,
                        "킨텍스 등록업체만 참여할 수 있습니다.");
            }
        }
        // 멤버십 반영 후 갱신된 역할로 새 토큰 재발급(현재 역할 기준 재조립).
        // ★초대코드→행사/부스/역할 매핑(invite 테이블)은 db-engineer 인수 대기 — 매핑 매퍼 제공 시 역할 upsert 배선.
        return buildLoginResponse(principal.userId(), principal.displayName(), principal.hallManager(),
                principal.roleCode(), principal.tenantId());
    }

    /** 사용자 역할을 조회해 토큰 + 워크스페이스 카드를 조립한다. globalRoleCode=app_user.role_code(null 허용). */
    private LoginResponse buildLoginResponse(String userId, String displayName, boolean hallManager,
                                             String globalRoleCode, String tenantId) {
        List<Map<String, Object>> roleRows = userMapper.findEventRoles(userId);
        Map<String, EventRole> eventRoles = new LinkedHashMap<>();
        if (roleRows != null) {
            for (Map<String, Object> r : roleRows) {
                String eventId = str(r.get("eventId"));
                EventRole role = parseRole(str(r.get("roleCode")));
                if (eventId != null && role != null) {
                    eventRoles.put(eventId, role);
                }
            }
        }
        String token = jwtService.issue(userId, displayName, eventRoles, hallManager, globalRoleCode, tenantId);
        List<WorkspaceDto> workspaces = toWorkspaces(roleRows);
        return new LoginResponse(token, jwtService.ttlSeconds(),
                new LoginResponse.UserSummary(userId, displayName, hallManager, globalRoleCode), workspaces);
    }

    private List<WorkspaceDto> loadWorkspaces(String userId) {
        return toWorkspaces(userMapper.findEventRoles(userId));
    }

    private List<WorkspaceDto> toWorkspaces(List<Map<String, Object>> roleRows) {
        List<WorkspaceDto> out = new java.util.ArrayList<>();
        if (roleRows == null) {
            return out;
        }
        for (Map<String, Object> r : roleRows) {
            out.add(new WorkspaceDto(
                    str(r.get("eventId")), str(r.get("eventName")),
                    str(r.get("startDate")), str(r.get("endDate")),
                    str(r.get("hallLabel")), str(r.get("roleCode")),
                    intVal(r.get("dday"))));
        }
        return out;
    }

    private static EventRole parseRole(String code) {
        if (code == null) {
            return null;
        }
        try {
            return EventRole.valueOf(code);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    private static String str(Object o) {
        return o == null ? null : String.valueOf(o);
    }

    private static Boolean bool(Object o) {
        if (o == null) return null;
        if (o instanceof Boolean b) return b;
        return Boolean.parseBoolean(String.valueOf(o));
    }

    private static int intVal(Object o) {
        if (o instanceof Number n) return n.intValue();
        if (o == null) return 0;
        try {
            return Integer.parseInt(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0;
        }
    }
}
