<?php

namespace App\Services;

use Carbon\Carbon;

/**
 * DateService - Centralized date calculation service
 *
 * This service provides consistent and PRECISE date calculations across the entire system.
 * Designed for Japanese retirement benefit calculations (退職金計算).
 *
 * Calculation Philosophy:
 * - Full calendar months (1st to last day) = counted as whole months
 * - Partial periods = calculated precisely based on actual days
 * - Standard: 1 month = 30 days for partial month conversion
 *
 * Japanese Fiscal Year (会計年度):
 * - April 1 to March 31 = exactly 12 months (1 year)
 *
 * @package App\Services
 */
class DateService
{
    /**
     * Standard days per month for decimal conversion
     * Used when converting partial days to decimal months
     */
    const DAYS_PER_MONTH = 30;

    /**
     * Calculate months between two dates with decimal precision
     *
     * This method provides ACCURATE calculation:
     * - Full months are counted as whole numbers
     * - Partial days are converted to decimal using 30 days/month
     *
     * Examples:
     * - 2015/04/01 to 2016/03/31 = 12.00 months (full fiscal year)
     * - 2015/01/23 to 2015/03/31 = 2.27 months (2 months + 8 days)
     * - 2016/04/01 to 2016/04/19 = 0.63 months (19 days)
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return float Number of months with decimal precision
     */
    public function calculateMonthsDecimal($fromDate, $toDate): float
    {
        if ($fromDate === null || $toDate === null) {
            return 0.0;
        }

        $startDate = $this->parseDate($fromDate)->startOfDay();
        $endDate = $this->parseDate($toDate)->startOfDay();

        if ($endDate->lessThan($startDate)) {
            return 0.0;
        }

        // Special case: same day
        if ($startDate->equalTo($endDate)) {
            return round(1 / self::DAYS_PER_MONTH, 2);
        }

        // Calculate full months and remaining days
        $totalMonths = 0;
        $current = $startDate->copy();

        // Count complete calendar months
        while (true) {
            // Get the last day of current month (as date only, not datetime)
            $lastDayOfMonth = $current->copy()->endOfMonth()->startOfDay();

            // Check if we can count this as a full month
            // Condition: current starts on 1st AND end date covers the entire month
            if ($current->day === 1 && $endDate->gte($lastDayOfMonth)) {
                // Full month: starts on 1st and end date covers the entire month
                $totalMonths++;
                $current = $lastDayOfMonth->copy()->addDay(); // Move to next month

                if ($current->gt($endDate)) {
                    break;
                }
            } else {
                // Partial month - calculate remaining days
                break;
            }
        }

        // Calculate remaining partial days
        $remainingDays = 0;
        if ($current->lte($endDate)) {
            $remainingDays = $current->diffInDays($endDate) + 1;
        }

        // Convert remaining days to decimal months
        $decimalMonths = $totalMonths + ($remainingDays / self::DAYS_PER_MONTH);

        return round($decimalMonths, 2);
    }

    /**
     * Calculate months between two dates (returns integer, floor)
     *
     * For cases where you need whole months only (切り捨て - round down)
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return int Number of complete months
     */
    public function calculateMonthsFloor($fromDate, $toDate): int
    {
        return (int) floor($this->calculateMonthsDecimal($fromDate, $toDate));
    }

    /**
     * Calculate calendar months between two dates
     *
     * This counts the number of calendar months spanned, considering:
     * - Full months from 1st to last day = 1 month each
     * - Partial months at start/end are included if they cover significant portion
     *
     * For Japanese leave calculation:
     * - 04/01 to 03/31 = 12 months (full fiscal year)
     * - 01/23 to 03/31 = 3 months (Jan partial, Feb full, Mar full)
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return int Number of calendar months
     */
    public function calculateCalendarMonths($fromDate, $toDate): int
    {
        if ($fromDate === null || $toDate === null) {
            return 0;
        }

        $startDate = $this->parseDate($fromDate)->startOfDay();
        $endDate = $this->parseDate($toDate)->startOfDay();

        if ($endDate->lessThan($startDate)) {
            return 0;
        }

        // Calculate based on year/month difference
        $months = ($endDate->year - $startDate->year) * 12 + ($endDate->month - $startDate->month);

        // Add 1 if end date reaches or passes the same day of month as start,
        // OR if end date is the last day of its month (for cases like 04/01 to 03/31)
        if ($endDate->day >= $startDate->day || $endDate->day === $endDate->daysInMonth) {
            $months++;
        }

        return max(0, $months);
    }

    /**
     * Calculate the precise number of days between two dates (inclusive)
     *
     * Example: 2024/01/01 to 2024/01/03 = 3 days (1st, 2nd, 3rd)
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return int Number of days (inclusive)
     */
    public function calculateDaysInclusive($fromDate, $toDate): int
    {
        if ($fromDate === null || $toDate === null) {
            return 0;
        }

        $startDate = $this->parseDate($fromDate)->startOfDay();
        $endDate = $this->parseDate($toDate)->startOfDay();

        if ($endDate->lessThan($startDate)) {
            return 0;
        }

        return (int) $endDate->diffInDays($startDate) + 1;
    }

    /**
     * Calculate days and convert to decimal months
     *
     * Uses 30 days = 1 month standard
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return float Decimal months
     */
    public function calculateDaysAsMonths($fromDate, $toDate): float
    {
        $days = $this->calculateDaysInclusive($fromDate, $toDate);
        return round($days / self::DAYS_PER_MONTH, 2);
    }

    /**
     * Calculate working years as a decimal value
     *
     * Example: 10 years 6 months = 10.5
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return float Years as decimal (rounded to 1 decimal place)
     */
    public function calculateWorkingYearsDecimal($fromDate, $toDate): float
    {
        if ($fromDate === null || $toDate === null) {
            return 0.0;
        }

        $months = $this->calculateMonthsDecimal($fromDate, $toDate);
        return round($months / 12, 1);
    }

    /**
     * Calculate total working months using calendar month method
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return int Total months
     */
    public function calculateWorkingMonths($fromDate, $toDate): int
    {
        return $this->calculateCalendarMonths($fromDate, $toDate);
    }

    /**
     * Calculate working period and return formatted string (Japanese format)
     *
     * Example: 10年6ヶ月
     *
     * @param Carbon|string|null $fromDate Start date
     * @param Carbon|string|null $toDate End date
     * @return string Formatted period string
     */
    public function calculateWorkingPeriodFormatted($fromDate, $toDate): string
    {
        $totalMonths = $this->calculateCalendarMonths($fromDate, $toDate);
        return $this->formatYearsMonths($totalMonths);
    }

    /**
     * Format total months as years and months display (Japanese format)
     *
     * Example: 125 months = 10年5ヶ月
     *
     * @param int|float $totalMonths Total months
     * @return string Formatted string
     */
    public function formatYearsMonths($totalMonths): string
    {
        $totalMonths = (int) floor($totalMonths);

        if ($totalMonths <= 0) {
            return '0年0ヶ月';
        }

        $years = (int) floor($totalMonths / 12);
        $months = $totalMonths % 12;

        return $years . '年' . $months . 'ヶ月';
    }

    /**
     * Convert months to years (decimal)
     *
     * @param int|float $months Number of months
     * @return float Years as decimal (rounded to 1 decimal place)
     */
    public function convertMonthsToYears($months): float
    {
        return round($months / 12, 1);
    }

    /**
     * Floor a number to specified decimal places
     *
     * This is used for rounding down affected leave months to ensure
     * fair calculation for employees (有利な方向に切り捨て)
     *
     * Examples:
     * - floorToDecimal(2.27, 1) = 2.2
     * - floorToDecimal(0.63, 1) = 0.6
     * - floorToDecimal(14.99, 1) = 14.9
     *
     * @param float $value The value to floor
     * @param int $decimals Number of decimal places (default: 1)
     * @return float Floored value
     */
    public function floorToDecimal(float $value, int $decimals = 1): float
    {
        $multiplier = pow(10, $decimals);
        return floor($value * $multiplier) / $multiplier;
    }

    /**
     * Check if a date range overlaps with another date range
     * Returns the overlapping portion if it exists
     *
     * @param Carbon|string $checkStart Start of the range to check
     * @param Carbon|string $checkEnd End of the range to check
     * @param Carbon|string $rangeStart Start of the reference range
     * @param Carbon|string $rangeEnd End of the reference range
     * @return array|null ['start' => Carbon, 'end' => Carbon] or null if no overlap
     */
    public function getOverlappingPeriod($checkStart, $checkEnd, $rangeStart, $rangeEnd): ?array
    {
        $checkStart = $this->parseDate($checkStart);
        $checkEnd = $this->parseDate($checkEnd);
        $rangeStart = $this->parseDate($rangeStart);
        $rangeEnd = $this->parseDate($rangeEnd);

        // No overlap: check period is completely outside range
        if ($checkEnd->lessThan($rangeStart) || $checkStart->greaterThan($rangeEnd)) {
            return null;
        }

        // Calculate overlap start and end
        $overlapStart = $checkStart->greaterThan($rangeStart) ? $checkStart : $rangeStart;
        $overlapEnd = $checkEnd->lessThan($rangeEnd) ? $checkEnd : $rangeEnd;

        return [
            'start' => $overlapStart->copy(),
            'end' => $overlapEnd->copy()
        ];
    }

    /**
     * Get the fiscal year for a given date (Japanese fiscal year: April to March)
     *
     * @param Carbon|string|null $date The date
     * @return int The fiscal year
     */
    public function getFiscalYear($date): int
    {
        if ($date === null) {
            $date = Carbon::now();
        }

        $date = $this->parseDate($date);

        return $date->month < 4 ? $date->year - 1 : $date->year;
    }

    /**
     * Get the start date of a fiscal year
     *
     * @param int $fiscalYear The fiscal year
     * @return Carbon The start date (April 1)
     */
    public function getFiscalYearStart(int $fiscalYear): Carbon
    {
        return Carbon::create($fiscalYear, 4, 1)->startOfDay();
    }

    /**
     * Get the end date of a fiscal year
     *
     * @param int $fiscalYear The fiscal year
     * @return Carbon The end date (March 31)
     */
    public function getFiscalYearEnd(int $fiscalYear): Carbon
    {
        return Carbon::create($fiscalYear + 1, 3, 31)->endOfDay();
    }

    /**
     * Parse a date value into a Carbon instance
     *
     * @param Carbon|string $date The date to parse
     * @return Carbon
     */
    protected function parseDate($date): Carbon
    {
        if ($date instanceof Carbon) {
            return $date->copy();
        }

        return Carbon::parse($date);
    }
}
