using System;
using System.Text.RegularExpressions;

namespace Hc900Crawler.Core.Application.Services;

/// <summary>
/// 한글 시간 범위 추출기
/// 자연어 입력에서 시간 범위를 파싱하여 TimeRange 객체로 반환합니다.
/// </summary>
public class KoreanTimeRangeExtractor(KstClock kst)
{
    // "오늘" 기준은 KST 날짜
    private DateTime KstToday => kst.KstNow.Date;

    /// <summary>
    /// 자연어 입력에서 시간 범위 추출
    /// 우선순위 순서대로 패턴을 시도합니다.
    /// </summary>
    public TimeRange Extract(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            return new TimeRange("1 hour", null, null);

        return TryAbsoluteRange(input)
            ?? TryTodayTimeRange(input)
            ?? TryOneDirection(input)
            ?? TryRelativeRange(input)
            ?? TryKoreanDatePattern(input)  // "4월 3일" 같은 단독 한글 날짜 패턴
            ?? TryNamedDay(input)
            ?? new TimeRange("1 hour", null, null);  // 기본값
    }

    /// <summary>
    /// 입력이 한글 날짜 패턴(X월 X일 또는 YYYY년 X월 X일)을 포함하는지 확인
    /// </summary>
    internal static bool HasKoreanDatePattern(string input)
        => Regex.IsMatch(input, @"\d{1,2}\s*월\s*\d{1,2}\s*일")
            || Regex.IsMatch(input, @"\d{4}\s*년\s*\d{1,2}\s*월\s*\d{1,2}\s*일");

    /// <summary>
    /// 입력이 시간 패턴(오후/오전 N시)을 포함하는지 확인
    /// </summary>
    internal static bool HasTimePattern(string input)
        => Regex.IsMatch(input, @"(오전|오후)?\s*\d{1,2}\s*시\s*\d{2}?\s*분?|(\d{1,2}):(\d{2})");

    // ── ① 절대 범위: 부터 ~ 까지 ─────────────────────────────────
    private TimeRange? TryAbsoluteRange(string input)
    {
        var m = Regex.Match(input, @"(.+?)\s*부터\s*(.+?)\s*까지");
        if (!m.Success) return null;

        var from = ParseKstDateTime(m.Groups[1].Value.Trim());
        var to   = ParseKstDateTime(m.Groups[2].Value.Trim());
        if (from == null || to == null) return null;

        // 날짜만 지정된 "까지" → 해당일 끝 (다음날 00:00)
        if (!HasTimeComponent(m.Groups[2].Value))
            to = to.Value.AddDays(1);

        return new TimeRange(null, from, to);
    }

    // ── ② 당일 시간 범위: 오전/오후 N시부터 M시까지 ──────────────
    private TimeRange? TryTodayTimeRange(string input)
    {
        var m = Regex.Match(input,
            @"(오전|오후)?\s*(\d{1,2})\s*시\s*(\d{2})?\s*분?\s*부터" +
            @"\s*(오전|오후)?\s*(\d{1,2})\s*시\s*(\d{2})?\s*분?\s*까지");
        if (!m.Success) return null;

        var from = BuildKstTime(KstToday,
            m.Groups[1].Value, int.Parse(m.Groups[2].Value),
            m.Groups[3].Success ? int.Parse(m.Groups[3].Value) : 0);
        var to = BuildKstTime(KstToday,
            m.Groups[4].Value, int.Parse(m.Groups[5].Value),
            m.Groups[6].Success ? int.Parse(m.Groups[6].Value) : 0);

        return new TimeRange(null, from, to);
    }

    // ── ③ 단방향: 이후/부터 or 이전 ─────────────────────────────
    private TimeRange? TryOneDirection(string input)
    {
        var afterM = Regex.Match(input, @"(.+?)\s*(이후|부터)(?!\s*.+까지)");
        if (afterM.Success)
        {
            var dt = ParseKstDateTime(afterM.Groups[1].Value.Trim());
            if (dt != null) return new TimeRange(null, dt, null);
        }

        var beforeM = Regex.Match(input, @"(.+?)\s*이전");
        if (beforeM.Success)
        {
            var dt = ParseKstDateTime(beforeM.Groups[1].Value.Trim());
            if (dt != null) return new TimeRange(null, null, dt);
        }

        return null;
    }

    // ── ④ 상대 범위: 최근/지난 N시간|분|일|개월|한달 ──────────────
    private static TimeRange? TryRelativeRange(string input)
    {
        var patterns = new (string Pat, Func<string, string> ToInterval)[]
        {
            (@"(?:최근|지난)\s*(\d+)\s*시간",  FormatHourInterval),
            (@"(?:최근|지난)\s*(\d+)\s*분",    FormatMinuteInterval),
            (@"(?:최근|지난)\s*(\d+)\s*일",    n => $"{n} days"),
            (@"(?:최근|지난)\s*(\d+)\s*주",    n => $"{int.Parse(n) * 7} days"),
            (@"(?:최근|지난)\s*(\d+)\s*개월",  n => $"{int.Parse(n) * 30} days"),
            (@"(?:최근|지난)\s*한달",           _ => "30 days"),
        };

        foreach (var (pat, fn) in patterns)
        {
            var m = Regex.Match(input, pat);
            if (m.Success)
            {
                var value = m.Groups[1].Success ? m.Groups[1].Value : "1";
                return new TimeRange(fn(value), null, null);
            }
        }
        return null;
    }

    private static string FormatHourInterval(string n)
    {
        int num = int.Parse(n);
        return num == 1 ? "1 hour" : $"{n} hours";
    }

    private static string FormatMinuteInterval(string n)
    {
        int num = int.Parse(n);
        return num == 1 ? "1 minute" : $"{n} minutes";
    }

    // ── ④.5 단독 한글 날짜: X월 X일 또는 YYYY년 X월 X일 (단방향에서만 매칭되지 않은 경우) ──
    private TimeRange? TryKoreanDatePattern(string input)
    {
        // "부터"나 "이후"가 있으면 TryOneDirection에서 처리했어야 함
        if (Regex.IsMatch(input, @"\d{1,2}\s*월\s*\d{1,2}\s*일")
            || Regex.IsMatch(input, @"\d{4}\s*년\s*\d{1,2}\s*월\s*\d{1,2}\s*일"))
        {
            var dt = ParseKstDateTime(input);
            if (dt.HasValue)
                return new TimeRange(null, dt.Value, dt.Value.AddDays(1));
        }
        return null;
    }

    // ── ⑤ 지정 날짜: 오늘/어제/이번 주 ─────────────────────────
    // 한글 날짜 패턴(4월 3일)이나 시간 패턴(오후 2시)이 있으면 매칭하지 않음
    private TimeRange? TryNamedDay(string input)
    {
        // 한글 날짜 패턴이 있으면 건너뛰 (TryAbsoluteRange/TryOneDirection에서 처리)
        if (HasKoreanDatePattern(input))
            return null;
        
        // 시간 패턴이 있으면 건너뛰 (TryTodayTimeRange/TryOneDirection에서 처리)
        if (HasTimePattern(input))
            return null;
        
        if (input.Contains("오늘"))
            return new TimeRange(null, KstToday, KstToday.AddDays(1));
        if (input.Contains("어제"))
            return new TimeRange(null, KstToday.AddDays(-1), KstToday);
        if (input.Contains("이번 주") || input.Contains("이번주"))
            return new TimeRange(null,
                KstToday.AddDays(-(int)KstToday.DayOfWeek + 1), // 월요일
                null);
        return null;
    }

    // ── 내부 유틸 ─────────────────────────────────────────────────

    /// <summary>
    /// 한글 날짜/시간 문자열 → KST DateTime
    /// </summary>
    internal DateTime? ParseKstDateTime(string s)
    {
        s = s.Trim();

        // ISO 날짜 먼저 확인: "2025-01-03", "2025-01-03 14:00"
        // 날짜가 먼저 오면 ISO를 먼저 파싱해야 시간 정보가 보존됨
        var isoMatch = Regex.Match(s, @"(\d{4})-(\d{2})-(\d{2})[T\s]+(\d{1,2}):(\d{2})");
        if (isoMatch.Success)
        {
            int year  = int.Parse(isoMatch.Groups[1].Value);
            int month = int.Parse(isoMatch.Groups[2].Value);
            int day   = int.Parse(isoMatch.Groups[3].Value);
            int hour  = int.Parse(isoMatch.Groups[4].Value);
            int min   = int.Parse(isoMatch.Groups[5].Value);
            
            // 오전/오후 처리 (isoAmpm으로 변수명 변경)
            var isoAmpm = "";
            if (s.Contains("오전")) isoAmpm = "AM";
            if (s.Contains("오후")) isoAmpm = "PM";
            
            var dt = new DateTime(year, month, day, hour, min, 0);
            return ApplyAmPm(dt, isoAmpm, hour);
        }

        // ISO 날짜 (시간 없음): "2025-01-03"
        var isoDateOnly = Regex.Match(s, @"(\d{4})-(\d{2})-(\d{2})");
        if (isoDateOnly.Success)
        {
            int year  = int.Parse(isoDateOnly.Groups[1].Value);
            int month = int.Parse(isoDateOnly.Groups[2].Value);
            int day   = int.Parse(isoDateOnly.Groups[3].Value);
            return new DateTime(year, month, day);
        }

        // 상대 키워드 → KST 날짜 치환 (먼저 처리)
        s = s.Replace("오늘", KstToday.ToString("yyyy-MM-dd"))
             .Replace("어제", KstToday.AddDays(-1).ToString("yyyy-MM-dd"));

        // 오전/오후 추출 (나머지 파싱에서 사용)
        var ampm = "";
        if (s.Contains("오전")) { ampm = "AM"; s = s.Replace("오전", "").Trim(); }
        if (s.Contains("오후")) { ampm = "PM"; s = s.Replace("오후", "").Trim(); }

        // 한글 날짜: "2026년 4월 13일", "1월 3일", "1월3일", "4월 3일" (공백 유무 모두 지원)
        // "YYYY년 M월 D일" 형식 먼저 확인
        var korDateWithYear = Regex.Match(s, @"(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})\s*일");
        if (korDateWithYear.Success)
        {
            int year  = int.Parse(korDateWithYear.Groups[1].Value);
            int month = int.Parse(korDateWithYear.Groups[2].Value);
            int day   = int.Parse(korDateWithYear.Groups[3].Value);
            var dt    = new DateTime(year, month, day);
            
            // 시간 부분 파싱: "14:00", "14시", "14시 30분"
            var timeM = Regex.Match(s, @"(\d{1,2})\s*시\s*(\d{2})?\s*분?|(\d{1,2}):(\d{2})");
            if (timeM.Success)
            {
                int h, m;
                if (timeM.Groups[1].Success)
                {
                    h = int.Parse(timeM.Groups[1].Value);
                    m = timeM.Groups[2].Success ? int.Parse(timeM.Groups[2].Value) : 0;
                }
                else
                {
                    h = int.Parse(timeM.Groups[3].Value);
                    m = int.Parse(timeM.Groups[4].Value);
                }
                return ApplyAmPm(dt.AddHours(h).AddMinutes(m), ampm, h);
            }
            return dt;  // KST 날짜, 시간 없음
        }
        
        // "M월 D일" 형식 (연도는 현재 연도 사용)
        var korDate = Regex.Match(s, @"(\d{1,2})\s*월\s*(\d{1,2})\s*일");
        if (korDate.Success)
        {
            int month = int.Parse(korDate.Groups[1].Value);
            int day   = int.Parse(korDate.Groups[2].Value);
            var dt    = new DateTime(KstToday.Year, month, day);
            // 미래 날짜면 작년으로
            if (dt > KstToday.AddDays(1)) dt = dt.AddYears(-1);

            // 시간 부분 파싱: "14:00", "14시", "14시 30분"
            var timeM = Regex.Match(s, @"(\d{1,2})\s*시\s*(\d{2})?\s*분?|(\d{1,2}):(\d{2})");
            if (timeM.Success)
            {
                int h, m;
                if (timeM.Groups[1].Success)
                {
                    h = int.Parse(timeM.Groups[1].Value);
                    m = timeM.Groups[2].Success ? int.Parse(timeM.Groups[2].Value) : 0;
                }
                else
                {
                    h = int.Parse(timeM.Groups[3].Value);
                    m = int.Parse(timeM.Groups[4].Value);
                }
                return ApplyAmPm(dt.AddHours(h).AddMinutes(m), ampm, h);
            }
            return dt;  // KST 날짜, 시간 없음
        }

        // 시간 패턴만 있는 경우: "14:00", "9시 30분", "오후 3시"
        var timeOnly = Regex.Match(s, @"(\d{1,2})\s*시\s*(\d{2})?\s*분?|(\d{1,2}):(\d{2})");
        if (timeOnly.Success)
        {
            int h, m;
            if (timeOnly.Groups[1].Success)
            {
                // "9시 30분" 형식
                h = int.Parse(timeOnly.Groups[1].Value);
                m = timeOnly.Groups[2].Success ? int.Parse(timeOnly.Groups[2].Value) : 0;
            }
            else
            {
                // "14:00" 형식
                h = int.Parse(timeOnly.Groups[3].Value);
                m = int.Parse(timeOnly.Groups[4].Value);
            }
            
            // "어제"/"오늘" 치환 후면 ISO 날짜가 s에 있을 수 있음
            var isoDateInS = Regex.Match(s, @"(\d{4})-(\d{2})-(\d{2})");
            if (isoDateInS.Success)
            {
                int year  = int.Parse(isoDateInS.Groups[1].Value);
                int month = int.Parse(isoDateInS.Groups[2].Value);
                int day   = int.Parse(isoDateInS.Groups[3].Value);
                var date = new DateTime(year, month, day);
                return ApplyAmPm(date.AddHours(h).AddMinutes(m), ampm, h);
            }
            
            return ApplyAmPm(KstToday.AddHours(h).AddMinutes(m), ampm, h);
        }

        // ISO 날짜 (마지막 시도): "2025-01-03"
        if (DateTime.TryParse(s, out var iso))
            return iso;  // Kind=Unspecified → ToUtc()에서 KST로 해석

        return null;
    }

    private static DateTime ApplyAmPm(DateTime dt, string ampm, int originalHour)
    {
        if (ampm == "PM" && originalHour < 12)
            return dt.AddHours(12);
        if (ampm == "AM" && originalHour == 12)
            return dt.AddHours(-12);
        return dt;
    }

    private static DateTime BuildKstTime(DateTime date, string ampm, int hour, int min)
    {
        if (ampm == "오후" && hour < 12) hour += 12;
        if (ampm == "오전" && hour == 12) hour = 0;
        return date.AddHours(hour).AddMinutes(min);
    }

    private static bool HasTimeComponent(string s)
        => Regex.IsMatch(s, @"\d{1,2}[:\s시]\d{2}|오전|오후");
}
