"""
시간 계산 로직
근무시간, 퇴근시간, 연장근무 계산
"""
from datetime import datetime, time, timedelta
from typing import Tuple, Optional


def resolve_work_datetimes(date_str: str, clock_in: str, clock_out: str) -> Tuple[datetime, datetime]:
    """출근일 기준 (출근, 퇴근) datetime. 퇴근이 출근보다 이르면 익일로 해석.

    work_records는 출근일 날짜 한 행에 clock_in/clock_out TIME만 저장하므로
    "09:00 출근 → 01:30 퇴근"(야근)은 clock_out < clock_in 으로 나타난다.
    break_records의 자정 처리(복귀 < 외출 → 익일)와 같은 규칙.

    Args:
        date_str: 출근일 'YYYY-MM-DD'
        clock_in / clock_out: 'HH:MM:SS' (또는 'HH:MM')
    """
    day = datetime.strptime(date_str, '%Y-%m-%d').date()
    fmt = '%H:%M:%S' if clock_in.count(':') == 2 else '%H:%M'
    ci = datetime.combine(day, datetime.strptime(clock_in, fmt).time())
    fmt = '%H:%M:%S' if clock_out.count(':') == 2 else '%H:%M'
    co = datetime.combine(day, datetime.strptime(clock_out, fmt).time())
    if co < ci:
        co += timedelta(days=1)
    return ci, co


def is_overnight(clock_in: str, clock_out: str) -> bool:
    """퇴근 시각이 익일인지 (문자열 비교 — 'HH:MM:SS' 는 사전순=시간순)."""
    return bool(clock_out) and clock_out < clock_in


class TimeCalculator:
    """근무시간 계산 클래스"""

    def __init__(self, work_hours=None, lunch_duration_minutes: int = 60, dinner_duration_minutes: int = 60,
                 work_minutes: int = None):
        """
        Args:
            work_hours: 기본 근무시간 (시간 단위, float 허용 - 호환성용)
            work_minutes: 기본 근무시간 (분 단위, 지정 시 work_hours보다 우선)
            lunch_duration_minutes: 점심시간 (분)
            dinner_duration_minutes: 저녁시간 (분)
        """
        if work_minutes is not None:
            self.work_minutes = int(work_minutes)
        elif work_hours is not None:
            # 은행 반올림(banker's rounding) 회피: 6.5시간 → 390분이 되도록 명시적 반올림
            self.work_minutes = int(float(work_hours) * 60 + 0.5)
        else:
            self.work_minutes = 480

        self.lunch_duration_minutes = lunch_duration_minutes
        self.dinner_duration_minutes = dinner_duration_minutes

    @property
    def work_hours(self):
        return self.work_minutes / 60.0

    def calculate_clock_out_time(self, clock_in: datetime,
                                  include_lunch: bool = False,
                                  include_dinner: bool = False,
                                  break_minutes: int = 0) -> datetime:
        """
        퇴근시간 계산
        Args:
            clock_in: 출근 시간
            include_lunch: 점심시간 포함 여부
            include_dinner: 저녁시간 포함 여부
            break_minutes: 외출 시간 (분)
        Returns:
            datetime: 예상 퇴근 시간
        """
        total_minutes = self.work_minutes + break_minutes
        if include_lunch:
            total_minutes += self.lunch_duration_minutes
        if include_dinner:
            total_minutes += self.dinner_duration_minutes

        clock_out = clock_in + timedelta(minutes=total_minutes)
        return clock_out

    def calculate_remaining_time(self, clock_in: datetime,
                                 include_lunch: bool = False,
                                 include_dinner: bool = False,
                                 current_time: Optional[datetime] = None,
                                 break_minutes: int = 0) -> timedelta:
        """
        퇴근까지 남은 시간 계산
        Args:
            clock_in: 출근 시간
            include_lunch: 점심시간 포함 여부
            include_dinner: 저녁시간 포함 여부
            current_time: 현재 시간 (None이면 지금 시간 사용)
            break_minutes: 외출 시간 (분)
        Returns:
            timedelta: 남은 시간 (음수면 이미 퇴근 시간 경과)
        """
        if current_time is None:
            current_time = datetime.now()

        # calculate_clock_out_time()에서 이미 break_minutes를 처리하므로
        # 여기서 중복으로 추가하지 않음
        clock_out = self.calculate_clock_out_time(clock_in, include_lunch, include_dinner, break_minutes)

        remaining = clock_out - current_time
        return remaining

    def calculate_work_progress(self, clock_in: datetime,
                                include_lunch: bool = False,
                                include_dinner: bool = False,
                                current_time: Optional[datetime] = None,
                                break_minutes: int = 0,
                                overtime_used_minutes: int = 0) -> float:
        """
        근무 진행률 계산 (0.0 ~ 1.0)
        Args:
            clock_in: 출근 시간
            include_lunch: 점심시간 포함 여부
            include_dinner: 저녁시간 포함 여부
            current_time: 현재 시간
            break_minutes: 외출 시간 (분) - 필요 근무시간 증가
            overtime_used_minutes: 사용한 추가근무 (분) - 필요 근무시간 감소
        Returns:
            float: 진행률 (0.0 ~ 1.0)
        """
        if current_time is None:
            current_time = datetime.now()

        # 전체 필요 근무 시간 (초)
        # = 기본 근무시간 + 점심시간 + 저녁시간 + 외출시간 - 추가근무 사용시간
        base_work_seconds = self.work_minutes * 60
        lunch_seconds = self.lunch_duration_minutes * 60 if include_lunch else 0
        dinner_seconds = self.dinner_duration_minutes * 60 if include_dinner else 0
        total_work_seconds = base_work_seconds + lunch_seconds + dinner_seconds + (break_minutes * 60) - (overtime_used_minutes * 60)

        # 경과 시간 (초)
        elapsed_seconds = (current_time - clock_in).total_seconds()

        # 진행률 계산 (0.0 ~ 1.0 범위로 제한)
        if total_work_seconds <= 0:
            # 필요 시간이 0 이하면 100% 완료로 처리
            return 1.0

        progress = elapsed_seconds / total_work_seconds
        return max(min(progress, 1.0), 0.0)

    def calculate_total_work_time(self, clock_in: datetime,
                                   clock_out: datetime) -> float:
        """
        총 근무시간 계산 (시간 단위)
        Args:
            clock_in: 출근 시간
            clock_out: 퇴근 시간
        Returns:
            float: 총 근무시간 (시간)
        """
        work_duration = clock_out - clock_in
        return work_duration.total_seconds() / 3600

    def calculate_overtime(self, clock_in: datetime, clock_out: datetime,
                          include_lunch: bool = False, include_dinner: bool = False,
                          break_minutes: int = 0,
                          unit_minutes: int = 30) -> Tuple[int, int]:
        """
        연장근무 시간 계산 (실제 시간, unit_minutes 단위 적립)
        Args:
            clock_in: 출근 시간
            clock_out: 퇴근 시간
            include_lunch: 점심시간 포함 여부
            include_dinner: 저녁시간 포함 여부
            break_minutes: 외출 시간 (분) - 연장근무 계산에서 제외
            unit_minutes: 적립 단위 (분, 기본 30). 사용자 설정 OVERTIME_UNIT.
        Returns:
            Tuple[int, int]: (실제 연장근무 분, unit_minutes 단위 적립 분)
        """
        expected_clock_out = self.calculate_clock_out_time(clock_in, include_lunch, include_dinner, break_minutes)

        if clock_out <= expected_clock_out:
            return 0, 0  # 연장근무 없음

        overtime_duration = clock_out - expected_clock_out
        overtime_minutes = int(overtime_duration.total_seconds() / 60)

        unit = unit_minutes if unit_minutes > 0 else 30
        overtime_earned = (overtime_minutes // unit) * unit

        return overtime_minutes, overtime_earned

    def format_time_delta(self, td: timedelta) -> str:
        """
        timedelta를 HH:MM:SS 형식으로 변환
        Args:
            td: timedelta 객체
        Returns:
            str: "HH:MM:SS" 형식 문자열
        """
        total_seconds = int(abs(td.total_seconds()))
        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60
        seconds = total_seconds % 60

        sign = "-" if td.total_seconds() < 0 else ""
        return f"{sign}{hours:02d}:{minutes:02d}:{seconds:02d}"

    def format_overtime_tokens(self, total_minutes: int) -> Tuple[int, str]:
        """
        연장근무 시간을 토큰 형식으로 변환
        Args:
            total_minutes: 총 연장근무 시간 (분)
        Returns:
            Tuple[int, str]: (토큰 개수, 시간 문자열)
        """
        tokens = total_minutes // 30  # 30분 = 1토큰
        hours = total_minutes // 60
        minutes = total_minutes % 60

        if hours > 0 and minutes > 0:
            time_str = f"{hours}시간 {minutes}분"
        elif hours > 0:
            time_str = f"{hours}시간"
        else:
            time_str = f"{minutes}분"

        return tokens, time_str

    def parse_time_string(self, time_str: str) -> datetime:
        """
        시간 문자열을 datetime으로 변환
        Args:
            time_str: "HH:MM:SS" 또는 "HH:MM" 형식
        Returns:
            datetime: 오늘 날짜 + 해당 시간
        """
        try:
            # "HH:MM:SS" 형식
            if len(time_str.split(':')) == 3:
                t = datetime.strptime(time_str, "%H:%M:%S").time()
            else:
                # "HH:MM" 형식
                t = datetime.strptime(time_str, "%H:%M").time()

            return datetime.combine(datetime.today(), t)
        except ValueError as e:
            raise ValueError(f"잘못된 시간 형식: {time_str}") from e

    def is_overtime_needed(self, clock_in: datetime,
                          target_overtime_minutes: int,
                          include_lunch: bool = False,
                          include_dinner: bool = False) -> datetime:
        """
        특정 연장근무 시간을 채우기 위한 퇴근 시간 계산
        Args:
            clock_in: 출근 시간
            target_overtime_minutes: 목표 연장근무 시간 (분)
            include_lunch: 점심시간 포함 여부
            include_dinner: 저녁시간 포함 여부
        Returns:
            datetime: 목표 달성을 위한 퇴근 시간
        """
        normal_clock_out = self.calculate_clock_out_time(clock_in, include_lunch, include_dinner)
        return normal_clock_out + timedelta(minutes=target_overtime_minutes)

    def effective_work_minutes(self, date_obj: datetime, db) -> int:
        """해당 날짜의 실효 근무 시간(분).

        등록된 연차(반차/시간연차)만큼 정규 근무시간에서 차감.
        종일 연차(>= work_minutes)면 0 반환.

        Args:
            date_obj: 기준 날짜 (datetime; 시각은 무시)
            db: Database 인스턴스 (None이면 차감 없음)

        Returns:
            실효 work_minutes (>= 0)
        """
        if db is None:
            return self.work_minutes
        date_str = date_obj.strftime("%Y-%m-%d")
        leave_min = db.get_leave_minutes_for(date_str)
        return max(0, self.work_minutes - leave_min)

    def calculate_holiday_overtime(self, clock_in: datetime, current_time: datetime,
                                     include_lunch: bool = False,
                                     include_dinner: bool = False,
                                     break_minutes: int = 0,
                                     unit_minutes: int = 30) -> Tuple[int, int]:
        """
        휴일/주말 근무: 모든 시간을 연장근무로 계산.

        Args:
            clock_in: 출근 시간
            current_time: 현재(또는 퇴근) 시간
            include_lunch/dinner: 식사 시간 차감 여부
            break_minutes: 외출 시간 (분) — 연장근무에서 제외
            unit_minutes: 적립 단위 (15/30/60)

        Returns:
            (실제 연장 분, 적립 분) — 둘 다 0 이상.
        """
        elapsed_minutes = int((current_time - clock_in).total_seconds() / 60)
        if include_lunch:
            elapsed_minutes -= self.lunch_duration_minutes
        if include_dinner:
            elapsed_minutes -= self.dinner_duration_minutes
        elapsed_minutes -= break_minutes
        elapsed_minutes = max(0, elapsed_minutes)

        unit = unit_minutes if unit_minutes > 0 else 30
        earned = (elapsed_minutes // unit) * unit
        return elapsed_minutes, earned

    def is_weekend(self, date_obj: datetime) -> bool:
        """
        주말 여부 확인
        Args:
            date_obj: 확인할 날짜
        Returns:
            bool: 토요일(5) 또는 일요일(6)이면 True
        """
        return date_obj.weekday() in [5, 6]

    def is_holiday(self, date_obj: datetime, db=None) -> bool:
        """
        공휴일 여부 확인
        Args:
            date_obj: 확인할 날짜
            db: Database 인스턴스 (None이면 False 반환)
        Returns:
            bool: 공휴일이면 True
        """
        if db is None:
            return False
        date_str = date_obj.strftime("%Y-%m-%d")
        return db.is_holiday(date_str)

    def is_non_working_day(self, date_obj: datetime, db=None) -> bool:
        """
        비근무일 여부 확인 (주말 또는 공휴일)
        Args:
            date_obj: 확인할 날짜
            db: Database 인스턴스 (공휴일 체크용)
        Returns:
            bool: 주말 또는 공휴일이면 True
        """
        if self.is_weekend(date_obj):
            return True
        return self.is_holiday(date_obj, db)

    def get_day_type(self, date_obj: datetime, db=None) -> str:
        """
        근무일 유형 반환
        Args:
            date_obj: 확인할 날짜
            db: Database 인스턴스
        Returns:
            str: 'weekend', 'holiday', 'normal' 중 하나
        """
        if self.is_weekend(date_obj):
            return 'weekend'
        if self.is_holiday(date_obj, db):
            return 'holiday'
        return 'normal'


# 테스트 코드
if __name__ == "__main__":
    calc = TimeCalculator(work_hours=8, lunch_duration_minutes=60)

    print("=== 시간 계산기 테스트 ===\n")

    # 출근 시간 설정
    clock_in = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0)
    print(f"출근 시간: {clock_in.strftime('%H:%M:%S')}")

    # 퇴근 시간 계산 (점심 없음)
    clock_out_no_lunch = calc.calculate_clock_out_time(clock_in, include_lunch=False)
    print(f"퇴근 시간 (점심 제외): {clock_out_no_lunch.strftime('%H:%M:%S')}")

    # 퇴근 시간 계산 (점심 포함)
    clock_out_with_lunch = calc.calculate_clock_out_time(clock_in, include_lunch=True)
    print(f"퇴근 시간 (점심 포함): {clock_out_with_lunch.strftime('%H:%M:%S')}")

    # 현재 시간 기준 남은 시간
    remaining = calc.calculate_remaining_time(clock_in, include_lunch=True)
    print(f"\n남은 시간: {calc.format_time_delta(remaining)}")

    # 진행률
    progress = calc.calculate_work_progress(clock_in, include_lunch=True)
    print(f"진행률: {progress * 100:.1f}%")

    # 연장근무 계산
    actual_clock_out = clock_out_with_lunch + timedelta(hours=2, minutes=15)
    overtime_actual, overtime_earned = calc.calculate_overtime(
        clock_in, actual_clock_out, include_lunch=True
    )
    print(f"\n실제 퇴근: {actual_clock_out.strftime('%H:%M:%S')}")
    print(f"연장근무: {overtime_actual}분 (적립: {overtime_earned}분)")

    # 토큰 형식
    tokens, time_str = calc.format_overtime_tokens(overtime_earned)
    print(f"토큰: 🕐 × {tokens} ({time_str})")
