package com.zioinfo.kintex.module.m5;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.module.m5.dto.ReRoomJobRequest;
import com.zioinfo.kintex.module.m5.dto.RenderJobDto;
import com.zioinfo.kintex.module.m5.dto.RenderJobRequest;
import com.zioinfo.kintex.module.m5.dto.WorkerCallbackRequest;
import com.zioinfo.kintex.module.m5.mapper.RenderJobMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.OffsetDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * M5 서비스. <b>발행/상태/콜백은 Redis 기반 완성 구현</b>(큐잉·상태 전이·WebSocket 푸시·성공시 쿼터 차감).
 * 나노바나나 실호출은 Python 워커 담당(백엔드는 큐 발행까지). 내구 이력(갤러리)은 매퍼 대기(501).
 *
 * <p>보안: scene payload는 워커 소비용으로 큐에만 적재하며, GEMINI_API_KEY는 백엔드에서 다루지 않는다.
 * 상태 응답에는 워터마크·고지가 항상 포함된다(PLANNING §6-5).
 */
@Service
public class RenderJobServiceImpl implements RenderJobService {

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

    private static final String JOB_KEY_PREFIX = "kintex:renderjob:job:";

    private final StringRedisTemplate redis;
    private final ObjectMapper objectMapper;
    private final RenderProperties props;
    private final RenderNotificationService notifier;
    private final RenderJobMapper renderJobMapper;

    public RenderJobServiceImpl(StringRedisTemplate redis, ObjectMapper objectMapper,
                                RenderProperties props, RenderNotificationService notifier,
                                RenderJobMapper renderJobMapper) {
        this.redis = redis;
        this.objectMapper = objectMapper;
        this.props = props;
        this.notifier = notifier;
        this.renderJobMapper = renderJobMapper;
    }

    @Override
    public RenderJobDto publish(String eventId, String boothId, RenderJobRequest request) {
        Map<String, Object> extra = new LinkedHashMap<>();
        extra.put("scene", request.scene());
        extra.put("referenceImageUrl", request.referenceImageUrl());
        return enqueue(eventId, boothId, request.shotPreset(), extra);
    }

    @Override
    public RenderJobDto publishReRoom(String eventId, String boothId, ReRoomJobRequest request) {
        // ReRoomAI 방식 사진 시안 — 참조 이미지 필수. 워커는 reference_image(로컬경로)로 image-to-image.
        String shotPreset = (request.shotPreset() == null || request.shotPreset().isBlank())
                ? "R1" : request.shotPreset();
        Map<String, Object> extra = new LinkedHashMap<>();
        extra.put("scene", request.scene() == null ? Map.of() : request.scene());
        extra.put("referenceImageUrl", request.referenceImageUrl()); // 프론트 Before 표시용 공개 URL
        extra.put("reference_image", request.referenceLocalPath());   // 워커 소비용 로컬 경로
        extra.put("mode", "reroom");
        extra.put("style", request.style());
        extra.put("instruction", request.instruction());
        return enqueue(eventId, boothId, shotPreset, extra);
    }

    /**
     * RenderJob 발행 공통 경로(쿼터→큐 적재→상태 저장→내구 이력). extra 는 워커 payload 에 병합된다.
     * S1~S7·ReRoom(R1) 모두 이 경로로 발행되어 상태/콜백/푸시/쿼터를 동일하게 재사용한다.
     */
    private RenderJobDto enqueue(String eventId, String boothId, String shotPreset,
                                 Map<String, Object> extra) {
        // 쿼터 정본 = DB 성공(DONE) 건수(RenderJobMapper.countSucceededByEvent). 성공 시에만 차감(§6-5).
        long succeeded = renderJobMapper.countSucceededByEvent(eventId);
        if (succeeded >= props.getEventQuotaDefault()) {
            throw new ApiException(ErrorCode.RENDER_QUOTA_EXCEEDED,
                    "행사 이미지 생성 쿼터(" + props.getEventQuotaDefault() + ")를 소진했습니다.");
        }

        String jobId = UUID.randomUUID().toString();
        String now = OffsetDateTime.now().toString();

        // 워커 소비용 큐 payload (§6-2 scene + 메타). 워터마크/고지 계약 포함.
        Map<String, Object> jobPayload = new LinkedHashMap<>();
        jobPayload.put("jobId", jobId);
        jobPayload.put("eventId", eventId);
        jobPayload.put("boothId", boothId);
        jobPayload.put("shotPreset", shotPreset);
        if (extra != null) {
            jobPayload.putAll(extra); // scene/referenceImageUrl/reference_image/mode/style/instruction 등
        }
        jobPayload.put("meta", Map.of(
                "watermarkRequired", true,
                "watermarkText", RenderJobDto.WATERMARK_TEXT,
                "notice", RenderJobDto.NOTICE));
        jobPayload.put("queuedAt", now);

        try {
            redis.opsForList().leftPush(props.getQueueKey(), objectMapper.writeValueAsString(jobPayload));
        } catch (JsonProcessingException e) {
            throw new ApiException(ErrorCode.INTERNAL, "RenderJob 직렬화에 실패했습니다.");
        }

        RenderJobDto dto = new RenderJobDto(jobId, boothId, shotPreset,
                RenderJobStatus.QUEUED.name(), null, null, null,
                true, RenderJobDto.WATERMARK_TEXT, RenderJobDto.NOTICE, null, now);
        saveState(dto);

        // 내구 이력 기록(갤러리·쿼터 정본). Redis 는 큐/실시간, DB 는 이력의 정본.
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("jobId", jobId);
        params.put("eventId", eventId);
        params.put("boothId", boothId);
        params.put("shotPreset", shotPreset);
        params.put("status", RenderJobStatus.QUEUED.name());
        renderJobMapper.insertJob(params);

        log.info("RenderJob 발행: job={} booth={} shot={}", jobId, boothId, shotPreset);
        return dto;
    }

    @Override
    public RenderJobDto getStatus(String eventId, String jobId) {
        RenderJobDto dto = readState(jobId);
        if (dto == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "해당 RenderJob을 찾을 수 없습니다.");
        }
        return dto;
    }

    @Override
    public List<RenderJobDto> listByBooth(String boothId, String shotPreset) {
        List<Map<String, Object>> rows = renderJobMapper.findByBooth(boothId, shotPreset);
        List<RenderJobDto> out = new java.util.ArrayList<>();
        if (rows == null) {
            return out;
        }
        for (Map<String, Object> r : rows) {
            out.add(new RenderJobDto(
                    str(r.get("jobId")), str(r.get("boothId")), str(r.get("shotPreset")),
                    str(r.get("status")), str(r.get("imageUrl")),
                    str(r.get("schemaHash")), str(r.get("modelVersion")),
                    true, RenderJobDto.WATERMARK_TEXT, RenderJobDto.NOTICE,
                    str(r.get("errorMessage")), str(r.get("createdAt"))));
        }
        return out;
    }

    @Override
    public List<RenderJobDto> listHistoryByBooth(String boothId) {
        List<Map<String, Object>> rows = renderJobMapper.findHistoryByBooth(boothId);
        List<RenderJobDto> out = new java.util.ArrayList<>();
        if (rows == null) {
            return out;
        }
        for (Map<String, Object> r : rows) {
            out.add(new RenderJobDto(
                    str(r.get("jobId")), str(r.get("boothId")), str(r.get("shotPreset")),
                    str(r.get("status")), str(r.get("imageUrl")),
                    str(r.get("schemaHash")), str(r.get("modelVersion")),
                    true, RenderJobDto.WATERMARK_TEXT, RenderJobDto.NOTICE,
                    null, str(r.get("createdAt"))));
        }
        return out;
    }

    @Override
    public RenderJobDto handleWorkerCallback(WorkerCallbackRequest callback) {
        RenderJobDto prev = readState(callback.jobId());
        if (prev == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "콜백 대상 RenderJob이 없습니다.");
        }
        boolean done = "DONE".equalsIgnoreCase(callback.status());
        String status = done ? RenderJobStatus.DONE.name() : RenderJobStatus.FAILED.name();

        RenderJobDto updated = new RenderJobDto(
                prev.jobId(), prev.boothId(), prev.shotPreset(), status,
                done ? callback.imageUrl() : null,
                callback.schemaHash(), callback.modelVersion(),
                true, RenderJobDto.WATERMARK_TEXT, RenderJobDto.NOTICE,
                done ? null : safeError(callback.errorMessage()),
                prev.createdAt());
        saveState(updated);

        // 내구 상태 갱신 — 쿼터 정본은 DB의 DONE 건수(countSucceededByEvent)에서 자동 반영(별도 카운터 불필요).
        renderJobMapper.updateStatus(updated.jobId(), status, updated.imageUrl(), updated.errorMessage());

        notifier.pushJobUpdate(updated);
        log.info("RenderJob 콜백: job={} status={}", updated.jobId(), status);
        return updated;
    }

    // --- Redis 상태 저장/조회 ---
    private void saveState(RenderJobDto dto) {
        try {
            redis.opsForValue().set(JOB_KEY_PREFIX + dto.jobId(), objectMapper.writeValueAsString(dto));
        } catch (JsonProcessingException e) {
            throw new ApiException(ErrorCode.INTERNAL, "RenderJob 상태 저장에 실패했습니다.");
        }
    }

    private RenderJobDto readState(String jobId) {
        String json = redis.opsForValue().get(JOB_KEY_PREFIX + jobId);
        if (json == null) {
            return null;
        }
        try {
            return objectMapper.readValue(json, RenderJobDto.class);
        } catch (JsonProcessingException e) {
            return null;
        }
    }

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

    /** 워커 에러 메시지는 요약만 통과(스택트레이스·민감정보 유입 방지). */
    private static String safeError(String raw) {
        if (raw == null || raw.isBlank()) {
            return "이미지 생성에 실패했습니다. 잠시 후 다시 시도하세요.";
        }
        String oneLine = raw.split("\\R", 2)[0];
        return oneLine.length() > 200 ? oneLine.substring(0, 200) : oneLine;
    }
}
