package kr.co.accura.wtm.domain.timesheet.service;

import kr.co.accura.wbx.spring.common.BusinessException;
import kr.co.accura.wbx.spring.common.NotFoundException;
import kr.co.accura.wtm.domain.timesheet.TimesheetEntryType;
import kr.co.accura.wtm.domain.timesheet.TimesheetStatus;
import kr.co.accura.wtm.domain.timesheet.dto.*;
import kr.co.accura.wtm.domain.timesheet.entity.Timesheet;
import kr.co.accura.wtm.domain.timesheet.entity.TimesheetEntry;
import kr.co.accura.wtm.domain.timesheet.entity.TimesheetUpload;
import kr.co.accura.wtm.domain.timesheet.repository.TimesheetEntryRepository;
import kr.co.accura.wtm.domain.timesheet.repository.TimesheetRepository;
import kr.co.accura.wtm.domain.timesheet.repository.TimesheetUploadRepository;
import kr.co.accura.wtm.domain.timesheet.rule.TimesheetRuleEngine;
import kr.co.accura.wtm.domain.timesheet.rule.ValidationResult;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;
import java.util.List;

@Service
@Transactional
@RequiredArgsConstructor
public class TimesheetService {

    private final TimesheetRepository timesheetRepository;
    private final TimesheetEntryRepository timesheetEntryRepository;
    private final TimesheetUploadRepository timesheetUploadRepository;
    private final TimesheetRuleEngine ruleEngine;

    public TimesheetDto getOrCreateWeekly(Long userId, LocalDate weekStart) {
        LocalDate weekEnd = weekStart.plusDays(6);
        Timesheet ts = timesheetRepository.findByUserIdAndWeekStartDate(userId, weekStart)
                .orElseGet(() -> {
                    Timesheet newTs = Timesheet.builder()
                            .userId(userId)
                            .weekStartDate(weekStart)
                            .weekEndDate(weekEnd)
                            .build();
                    return timesheetRepository.save(newTs);
                });
        return toDto(ts);
    }

    public TimesheetEntryDto saveEntry(Long timesheetId, TimesheetEntryRequest request) {
        Timesheet ts = findTimesheet(timesheetId);
        ensureEditable(ts);

        TimesheetEntry entry;
        if (request.id() != null) {
            entry = timesheetEntryRepository.findById(request.id())
                    .orElseThrow(() -> new NotFoundException("시수 항목을 찾을 수 없습니다."));
            updateEntry(entry, request);
        } else {
            entry = createEntry(ts, request);
            ts.addEntry(entry);
        }

        timesheetEntryRepository.save(entry);
        ts.recalculateTotal();
        timesheetRepository.save(ts);

        return toEntryDto(entry);
    }

    public TimesheetDto saveBatch(Long timesheetId, List<TimesheetEntryRequest> requests) {
        Timesheet ts = findTimesheet(timesheetId);
        ensureEditable(ts);

        // Clear existing entries and replace
        ts.getEntries().clear();
        for (TimesheetEntryRequest request : requests) {
            TimesheetEntry entry = createEntry(ts, request);
            ts.addEntry(entry);
        }

        ts.recalculateTotal();
        timesheetRepository.save(ts);
        return toDto(ts);
    }

    public TimesheetDto submit(Long timesheetId) {
        Timesheet ts = findTimesheet(timesheetId);

        // Validate before submission
        ValidationResult validation = ruleEngine.validate(ts);
        if (validation.hasErrors()) {
            String errorMsg = validation.getErrors().stream()
                    .map(e -> e.message())
                    .reduce((a, b) -> a + "; " + b)
                    .orElse("검증 오류");
            throw new BusinessException(errorMsg);
        }

        ts.submit();
        timesheetRepository.save(ts);
        return toDto(ts);
    }

    @Transactional(readOnly = true)
    public Page<TimesheetDto> getHistory(Long userId, LocalDate from, LocalDate to, Pageable pageable) {
        return timesheetRepository.findByUserIdAndDateRange(userId, from, to, pageable)
                .map(this::toDto);
    }

    public UploadResultDto uploadExcel(Long userId, MultipartFile file, LocalDate weekStart) {
        TimesheetUpload upload = TimesheetUpload.builder()
                .userId(userId)
                .filename(file.getOriginalFilename())
                .status("PROCESSING")
                .totalRows(0)
                .successRows(0)
                .errorRows(0)
                .build();
        upload = timesheetUploadRepository.save(upload);

        // TODO: Implement Excel parsing with Apache POI
        upload.setStatus("COMPLETED");
        timesheetUploadRepository.save(upload);

        return new UploadResultDto(
                upload.getId(),
                upload.getTotalRows() != null ? upload.getTotalRows() : 0,
                upload.getSuccessRows() != null ? upload.getSuccessRows() : 0,
                upload.getErrorRows() != null ? upload.getErrorRows() : 0,
                upload.getStatus(),
                upload.getErrorLog()
        );
    }

    public void deleteEntry(Long timesheetId, Long entryId) {
        Timesheet ts = findTimesheet(timesheetId);
        ensureEditable(ts);
        ts.getEntries().removeIf(e -> e.getId().equals(entryId));
        ts.recalculateTotal();
        timesheetRepository.save(ts);
    }

    // --- Helper methods ---

    private Timesheet findTimesheet(Long id) {
        return timesheetRepository.findById(id)
                .orElseThrow(() -> new NotFoundException("Timesheet not found: " + id));
    }

    private void ensureEditable(Timesheet ts) {
        if (ts.getStatus() != TimesheetStatus.DRAFT && ts.getStatus() != TimesheetStatus.REJECTED) {
            throw new BusinessException("수정 가능한 상태가 아닙니다 (현재: " + ts.getStatus() + ")");
        }
    }

    private TimesheetEntry createEntry(Timesheet ts, TimesheetEntryRequest req) {
        return TimesheetEntry.builder()
                .timesheet(ts)
                .entryType(TimesheetEntryType.valueOf(req.entryType()))
                .entryDate(req.entryDate())
                .hours(req.hours())
                .npCategory(req.npCategory())
                .otherProjectId(req.otherProjectId())
                .otherCategory(req.otherCategory())
                .epcProjectId(req.epcProjectId())
                .canonicalWbsId(req.canonicalWbsId())
                .tealEntryId(req.tealEntryId())
                .revisionNumber(req.revisionNumber() != null ? req.revisionNumber() : 1)
                .remark(req.remark())
                .build();
    }

    private void updateEntry(TimesheetEntry entry, TimesheetEntryRequest req) {
        entry.setEntryType(TimesheetEntryType.valueOf(req.entryType()));
        entry.setEntryDate(req.entryDate());
        entry.setHours(req.hours());
        entry.setNpCategory(req.npCategory());
        entry.setOtherProjectId(req.otherProjectId());
        entry.setOtherCategory(req.otherCategory());
        entry.setEpcProjectId(req.epcProjectId());
        entry.setCanonicalWbsId(req.canonicalWbsId());
        entry.setTealEntryId(req.tealEntryId());
        entry.setRevisionNumber(req.revisionNumber() != null ? req.revisionNumber() : 1);
        entry.setRemark(req.remark());
    }

    private TimesheetDto toDto(Timesheet ts) {
        List<TimesheetEntryDto> entryDtos = ts.getEntries().stream()
                .map(this::toEntryDto)
                .toList();
        return new TimesheetDto(
                ts.getId(), ts.getUserId(),
                ts.getWeekStartDate(), ts.getWeekEndDate(),
                ts.getStatus().name(), ts.getTotalHours(),
                ts.getSubmittedAt(), entryDtos
        );
    }

    private TimesheetEntryDto toEntryDto(TimesheetEntry e) {
        return new TimesheetEntryDto(
                e.getId(), e.getEntryType().name(), e.getEntryDate(),
                e.getHours(), e.getNpCategory(),
                e.getOtherProjectId(), e.getOtherCategory(),
                e.getEpcProjectId(), e.getCanonicalWbsId(),
                e.getTealEntryId(), e.getRevisionNumber(), e.getRemark()
        );
    }
}
