package kr.itn.itnhub.provision;

import kr.itn.itnhub.config.MattermostProperties;
import kr.itn.itnhub.mattermost.MattermostClient;
import kr.itn.itnhub.org.Organization;
import kr.itn.itnhub.org.OrganizationMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.Optional;

/**
 * 기관의 채널 2개(문정원/법률검토)를 보장한다. VBA M07의 이식.
 *
 * 판정 순서 (한쪽 채널당):
 *   1. DB에 Channel ID가 있으면 그대로 둔다        → CACHED
 *   2. 서버에 내부명으로 채널이 있으면 ID를 가져온다 → RECOVERED
 *   3. 표시명으로 있으면 ID를 가져온다              → RECOVERED
 *   4. 없을 때만 새로 만든다                       → CREATED
 *
 * 트랜잭션을 쓰지 않는다. 외부 API 호출이라 롤백이 불가능하고,
 * 한쪽이 실패해도 성공한 쪽의 ID는 반드시 남아야 재시도가 안전해지기 때문이다.
 * 절대 @Transactional을 붙이지 말 것.
 */
@Service
public class ChannelProvisionService {

    private static final Logger log = LoggerFactory.getLogger(ChannelProvisionService.class);

    private final MattermostClient mattermost;
    private final OrganizationMapper mapper;
    private final MattermostProperties props;

    public ChannelProvisionService(MattermostClient mattermost,
                                   OrganizationMapper mapper,
                                   MattermostProperties props) {
        this.mattermost = mattermost;
        this.mapper = mapper;
        this.props = props;
    }

    public ProvisionResult provision(Long orgId) {
        Organization org = mapper.findById(orgId);
        if (org == null) {
            throw new IllegalArgumentException("기관을 찾을 수 없습니다: " + orgId);
        }

        if (!org.hasRequiredContact()) {
            return new ProvisionResult(
                    ProvisionOutcome.FAILED,
                    ProvisionOutcome.FAILED,
                    "담당자 정보가 채워지지 않아 채널을 만들 수 없습니다.");
        }

        StringBuilder messages = new StringBuilder();

        ProvisionOutcome mj = ensure(org, ChannelKind.MJ,
                org.getChannelIdMj(), props.channelNameMj(), messages);

        ProvisionOutcome law = ensure(org, ChannelKind.LAW,
                org.getChannelIdLaw(), props.channelNameLaw(), messages);

        return new ProvisionResult(mj, law, messages.toString().trim());
    }

    private ProvisionOutcome ensure(Organization org,
                                    ChannelKind kind,
                                    String storedChannelId,
                                    String label,
                                    StringBuilder messages) {

        if (storedChannelId != null && !storedChannelId.isBlank()) {
            return ProvisionOutcome.CACHED;
        }

        String internalName = ChannelNaming.internalName(
                org.getOrgNo(), org.getOrgName(), kind);
        String displayName = ChannelNaming.displayName(
                org.getOrgNo(), org.getOrgName(), label);

        try {
            Optional<String> recovered = mattermost.findChannelIdByInternalName(internalName);
            if (recovered.isEmpty()) {
                recovered = mattermost.findChannelIdByDisplayName(displayName);
            }

            if (recovered.isPresent()) {
                applyChannelId(org.getId(), kind, recovered.get());
                return ProvisionOutcome.RECOVERED;
            }

            String created = mattermost.createPrivateChannel(internalName, displayName);
            applyChannelId(org.getId(), kind, created);
            return ProvisionOutcome.CREATED;

        } catch (RuntimeException e) {
            // MattermostException(서버 API 실패)뿐 아니라, 채널 생성/복구 직후 ID를 저장하는
            // mapper 호출이 던지는 DataAccessException 등도 여기서 잡는다. Error나 checked
            // exception은 의도적으로 넓히지 않는다 - 이 두 경우는 채널 하나만 FAILED로
            // 강등시키고 메시지에 남긴 뒤, 나머지 채널은 계속 시도되게 해야 한다.
            log.warn("채널 처리 실패 org={} channel={}", org.getOrgName(), displayName, e);
            messages.append("[").append(label).append("] ")
                    .append(e.getMessage()).append(System.lineSeparator());
            return ProvisionOutcome.FAILED;
        }
    }

    /**
     * Channel ID를 저장할 컬럼(mj/law)을 결정하는 유일한 지점.
     * switch 식(expression)으로 작성해 ChannelKind에 값이 추가되고도 분기를
     * 채우지 않으면 컴파일이 실패하도록 강제한다 - if/else였다면 새 값이
     * 조용히 else(법률검토) 분기로 흘러 들어가는 것을 막지 못했을 것이다.
     */
    private void applyChannelId(Long orgId, ChannelKind kind, String channelId) {
        // 반환값(영향받은 행 수)은 쓰지 않지만, switch를 식으로 유지해야
        // 컴파일러가 exhaustiveness를 검사한다 - 문(statement) 형태의
        // switch/if-else는 이 검사를 강제하지 않는다.
        var ignored = switch (kind) {
            case MJ -> mapper.updateChannelIdMj(orgId, channelId);
            case LAW -> mapper.updateChannelIdLaw(orgId, channelId);
        };
    }
}
