package incheon.sgp.common.service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public final class WeatherUtils {
    private WeatherUtils() {}

    // 필요한 파라미터(예: 기상청 LCC DFS 표준값)로 고정
    private static final LamcParameter DEFAULT_MAP = new LamcParameter(
        6371.00877, // Re
        5.0,        // grid
        30.0,       // slat1
        60.0,       // slat2
        126.0,      // olon
        38.0,       // olat
        43.0,       // xo
        136.0       // yo
    );

    private static final double PI = Math.PI;
    private static final double DEGRAD = PI / 180.0;

    private static final double re = DEFAULT_MAP.getRe() / DEFAULT_MAP.getGrid();
    private static final double olon = DEFAULT_MAP.getOlon() * DEGRAD;
    private static final double olat = DEFAULT_MAP.getOlat() * DEGRAD;

    private static final double sn;
    private static final double sf;
    private static final double ro;

    static {
        double slat1 = DEFAULT_MAP.getSlat1() * DEGRAD;
        double slat2 = DEFAULT_MAP.getSlat2() * DEGRAD;

        double snTmp = Math.tan(PI * 0.25 + slat2 * 0.5) / Math.tan(PI * 0.25 + slat1 * 0.5);
        sn = Math.log(Math.cos(slat1) / Math.cos(slat2)) / Math.log(snTmp);

        double sfTmp = Math.tan(PI * 0.25 + slat1 * 0.5);
        sf = Math.pow(sfTmp, sn) * Math.cos(slat1) / sn;

        double roTmp = Math.tan(PI * 0.25 + olat * 0.5);
        ro = re * sf / Math.pow(roTmp, sn);
    }

    // 위경도 -> 기상청 그리드 좌표
    public static GridPoint convertToGrid(double lon, double lat) {
        double ra = Math.tan(PI * 0.25 + (lat * DEGRAD * 0.5));
        ra = (re * sf) / Math.pow(ra, sn);

        double theta = (lon * DEGRAD) - olon;
        if (theta > PI) theta -= 2.0 * PI;
        if (theta < -PI) theta += 2.0 * PI;
        theta *= sn;

        double rawX = ra * Math.sin(theta) + DEFAULT_MAP.getXo();
        double rawY = ro - ra * Math.cos(theta) + DEFAULT_MAP.getYo();

        int x = (int) Math.round(rawX + 0.5);
        int y = (int) Math.round(rawY + 0.5);

        return new GridPoint(x, y);
    }

    public static class GridPoint {
        public final int x;
        public final int y;
        public GridPoint(int x, int y) { this.x = x; this.y = y; }
    }

    public static class LamcParameter {
        private final double Re, grid, slat1, slat2, olon, olat, xo, yo;

        public LamcParameter(double re, double grid, double slat1, double slat2,
                double olon, double olat, double xo, double yo) {
            this.Re = re;
            this.grid = grid;
            this.slat1 = slat1;
            this.slat2 = slat2;
            this.olon = olon;
            this.olat = olat;
            this.xo = xo;
            this.yo = yo;
        }

        public double getRe() {
            return Re;
        }

        public double getGrid() {
            return grid;
        }

        public double getSlat1() {
            return slat1;
        }

        public double getSlat2() {
            return slat2;
        }

        public double getOlon() {
            return olon;
        }

        public double getOlat() {
            return olat;
        }

        public double getXo() {
            return xo;
        }

        public double getYo() {
            return yo;
        }
    }
    
    public static class WeatherBrief {
        private final String fcstTime;      // 예보시간
        private final Double temperature;   // T1H(실황/초단기예보), 없으면 TMP(단기예보)
        private final String precipitation; // RN1(1시간 강수량), 없으면 PCP(강수량)
        private final String skyPty;         // PTY!=0이면 PTY, 0이면 SKY
        private final Integer humidity;      // REH

        public WeatherBrief(String fcstTime, Double temperature, String precipitation, String skyPty,
                Integer humidity) {
            this.fcstTime = fcstTime;
            this.temperature = temperature;
            this.precipitation = precipitation;
            this.skyPty = skyPty;
            this.humidity = humidity;
        }

        public String getFcstTime() { return fcstTime; }
        public Double getTemperature() { return temperature; }
        public String getPrecipitation() { return precipitation; }
        public String getSkyPty() { return skyPty; }
        public Integer getHumidity() { return humidity; }
    }

    public static WeatherBrief extractBrief(KmaUltraSrtResponse root, String targetDate, String targetTime) {
        List<KmaUltraSrtResponse.Item> items = safeItems(root);
        if (items.isEmpty()) return new WeatherBrief(null, null, null, null, null);

        // targetDate/targetTime이 있으면 해당 시각 우선, 없으면 "가장 이른 시각" 우선
        List<KmaUltraSrtResponse.Item> filtered = items.stream()
                .filter(i -> i.getCategory() != null)
                .collect(Collectors.toList());

        // category별로 "선택된 1개"를 만든다
        Map<String, KmaUltraSrtResponse.Item> pickByCategory = new HashMap<>();
        for (KmaUltraSrtResponse.Item it : filtered) {
            String cat = it.getCategory();
            if (!pickByCategory.containsKey(cat)) {
                pickByCategory.put(cat, it);
                continue;
            }
            KmaUltraSrtResponse.Item cur = pickByCategory.get(cat);
            if (isBetter(it, cur, targetDate, targetTime)) {
                pickByCategory.put(cat, it);
            }
        }

        FunctionValue v = new FunctionValue(pickByCategory);

        String fcstTime =
            firstNonBlank(
                    getFcstTimeOrNull(pickByCategory.get("T1H")),
                    getFcstTimeOrNull(pickByCategory.get("SKY"))
            );

        String tempRaw = firstNonBlank(v.get("T1H"), v.get("TMP"));
        Double temperature = parseDoubleOrNull(tempRaw);

        String precipitationRaw = firstNonBlank(v.get("RN1"), v.get("PCP"));
        String precipitation = normalizePrecip(precipitationRaw);

        String pty = v.get("PTY");
        String sky = v.get("SKY");
        String skyPty = (isZeroLike(pty) ? sky : pty);

        Integer humidity = parseIntOrNull(v.get("REH"));

        return new WeatherBrief(fcstTime, temperature, precipitation, skyPty, humidity);
    }

    private static List<KmaUltraSrtResponse.Item> safeItems(KmaUltraSrtResponse root) {
        if (root == null
                || root.getResponse() == null
                || root.getResponse().getBody() == null
                || root.getResponse().getBody().getItems() == null
                || root.getResponse().getBody().getItems().getItem() == null) {
            return List.of();
        }
        return root.getResponse().getBody().getItems().getItem();
    }

    private static String normalizePrecip(String v) {
        if (v == null)
            return null;

        String s = v.trim();
        if (s.isEmpty())
            return null;

        // "강수없음", "없음" 등 -> 0
        if (s.contains("없음"))
            return "0";

        // "0", "0.0", "0.00" -> 0
        if (isZeroLike(s))
            return "0";

        // "1.2mm" 같이 단위가 붙는 경우 대비 (있으면 제거)
        s = s.replace("mm", "").trim();

        return s;
    }
    
    private static String getFcstTimeOrNull(KmaUltraSrtResponse.Item it) {
        if (it == null) return null;
        String t = it.getFcstTime();
        if (t != null && !t.isBlank()) return t;

        // 실황 응답(Ncst)처럼 fcstTime이 없으면 baseTime이라도 반환
        String bt = it.getBaseTime();
        return (bt != null && !bt.isBlank()) ? bt : null;
    }

    private static boolean isBetter(
            KmaUltraSrtResponse.Item candidate,
            KmaUltraSrtResponse.Item current,
            String targetDate,
            String targetTime) {
        String cd = candidate.effectiveDate();
        String ct = candidate.effectiveTime();
        String rd = current.effectiveDate();
        String rt = current.effectiveTime();

        boolean cMatch = matches(cd, ct, targetDate, targetTime);
        boolean rMatch = matches(rd, rt, targetDate, targetTime);

        if (cMatch && !rMatch) return true;
        if (!cMatch && rMatch) return false;

        // 둘 다 match 또는 둘 다 non-match이면 "더 이른 시각"을 유지(원하면 반대로 바꿀 수 있음)
        String cKey = (cd == null ? "" : cd) + (ct == null ? "" : ct);
        String rKey = (rd == null ? "" : rd) + (rt == null ? "" : rt);
        return cKey.compareTo(rKey) < 0;
    }

    private static boolean matches(String d, String t, String targetDate, String targetTime) {
        if (targetDate == null || targetTime == null) return false;
        if (d == null || t == null) return false;
        return targetDate.equals(d) && targetTime.equals(t);
    }

    private static String firstNonBlank(String a, String b) {
        if (a != null && !a.isBlank()) return a;
        if (b != null && !b.isBlank()) return b;
        return null;
    }

    private static boolean isZeroLike(String v) {
        if (v == null) return true;
        String s = v.trim();
        return s.equals("0") || s.equals("0.0") || s.equals("0.00");
    }

    private static Double parseDoubleOrNull(String v) {
        if (v == null || v.isBlank()) return null;
        try { return Double.parseDouble(v.trim()); }
        catch (Exception e) { return null; }
    }

    private static Integer parseIntOrNull(String v) {
        if (v == null || v.isBlank()) return null;
        try { return (int) Math.round(Double.parseDouble(v.trim())); }
        catch (Exception e) { return null; }
    }

    private static class FunctionValue {
        private final Map<String, KmaUltraSrtResponse.Item> map;
        FunctionValue(Map<String, KmaUltraSrtResponse.Item> map) { this.map = map; }
        String get(String category) {
            KmaUltraSrtResponse.Item it = map.get(category);
            return it == null ? null : it.value();
        }
    }
}