package kr.itn.itnhub.code;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 코드값을 기동 후 첫 조회에 한 번 읽어 메모리에 들고 있는다. 코드는 관리 화면에서 가끔
 * 바뀔 뿐이라 매 요청 조회가 낭비이고, 단일 인스턴스 운영이라 분산 캐시는 필요 없다.
 *
 * <p>지연 로딩을 쓰는 이유는 Flyway 마이그레이션이 끝나기 전에 빈 초기화가 돌 수 있기
 * 때문이다. 첫 조회 시점에는 마이그레이션이 반드시 끝나 있다.</p>
 */
@Service
public class CodeService {

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

    private final CodeMapper mapper;
    private final ObjectMapper json = new ObjectMapper();

    /** 그룹ID -> 활성 코드(정렬순). 화면 선택지가 여기서 나간다. */
    private volatile Map<String, List<Code>> activeByGroup;
    /** 그룹ID -> 코드ID -> 코드(비활성 포함). 과거 데이터를 화면에 그릴 때 라벨이 필요하다. */
    private volatile Map<String, Map<String, Code>> allByGroup;

    public CodeService(CodeMapper mapper) {
        this.mapper = mapper;
    }

    /** 관리 화면에서 코드를 고친 뒤 부른다. */
    public synchronized void reload() {
        Map<String, List<Code>> active = new LinkedHashMap<>();
        Map<String, Map<String, Code>> all = new LinkedHashMap<>();

        for (CodeRow row : mapper.findAllCodes()) {
            Code code = toCode(row);
            all.computeIfAbsent(code.groupId(), key -> new LinkedHashMap<>())
                    .put(code.code(), code);
            if (code.active()) {
                active.computeIfAbsent(code.groupId(), key -> new ArrayList<>()).add(code);
            }
        }

        this.activeByGroup = active;
        this.allByGroup = all;
    }

    private Code toCode(CodeRow row) {
        Map<String, Object> attrs;
        try {
            attrs = json.readValue(row.attrsJson(), new TypeReference<>() {
            });
        } catch (Exception e) {
            // 잘못된 JSON 하나가 전체 코드 로딩을 막지는 않게 한다. 해당 코드만 속성이 빈다.
            log.warn("코드 attrs 파싱 실패 - {}:{} 의 속성을 비운다", row.groupId(), row.code(), e);
            attrs = Map.of();
        }
        return new Code(row.groupId(), row.code(), row.label(), row.sortOrder(), row.active(), attrs);
    }

    private void ensureLoaded() {
        if (activeByGroup == null) {
            reload();
        }
    }

    /** 활성 코드만, 그룹별 정렬순. */
    public Map<String, List<Code>> all() {
        ensureLoaded();
        return activeByGroup;
    }

    /** 활성 코드만. 없는 그룹이면 빈 목록. */
    public List<Code> group(String groupId) {
        ensureLoaded();
        return activeByGroup.getOrDefault(groupId, List.of());
    }

    /** 비활성 코드도 찾는다 - 과거 데이터의 라벨을 그려야 하기 때문이다. */
    public Optional<Code> find(String groupId, String code) {
        ensureLoaded();
        return Optional.ofNullable(allByGroup.getOrDefault(groupId, Map.of()).get(code));
    }

    /** attrs.scopes에 해당 자리가 든 활성 코드만. 공공누리 유형이 이걸 쓴다. */
    public List<Code> scoped(String groupId, String scope) {
        return group(groupId).stream().filter(code -> scopes(code).contains(scope)).toList();
    }

    @SuppressWarnings("unchecked")
    private List<String> scopes(Code code) {
        Object raw = code.attrs().get(Codes.ATTR_SCOPES);
        return raw instanceof List<?> list ? (List<String>) list : Collections.emptyList();
    }

    /** 저장 요청 값이 이 그룹의 활성 코드인지. */
    public boolean isValid(String groupId, String code) {
        return group(groupId).stream().anyMatch(candidate -> candidate.code().equals(code));
    }

    /** 코드 관리 화면이 쓰는 그룹 메타. 캐시하지 않는다 - 호출 빈도가 낮다. */
    public List<CodeGroup> groups() {
        return mapper.findAllGroups();
    }
}
