<?php
namespace App\Core;

/** Parsing & format tanggal, termasuk serial Excel dan format Indonesia. */
class Dates
{
    public const BULAN = [1=>'Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
    public const BULAN_SINGKAT = [1=>'Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'];
    public const HARI = ['Sunday'=>'Minggu','Monday'=>'Senin','Tuesday'=>'Selasa','Wednesday'=>'Rabu','Thursday'=>'Kamis','Friday'=>'Jumat','Saturday'=>'Sabtu'];

    /** Nama sheet bulan Indonesia -> nomor bulan. */
    public const SHEET_BULAN = [
        'JANUARI'=>1,'JAN'=>1,'FEBRUARI'=>2,'FEB'=>2,'PEBRUARI'=>2,'MARET'=>3,'MAR'=>3,
        'APRIL'=>4,'APR'=>4,'MEI'=>5,'JUNI'=>6,'JUN'=>6,'JULI'=>7,'JUL'=>7,
        'AGUSTUS'=>8,'AGU'=>8,'AGS'=>8,'SEPTEMBER'=>9,'SEP'=>9,'SEPT'=>9,
        'OKTOBER'=>10,'OKT'=>10,'NOVEMBER'=>11,'NOV'=>11,'DESEMBER'=>12,'DES'=>12,
    ];

    /**
     * Mengubah nilai apa pun menjadi 'Y-m-d' atau null.
     * Mendukung: DateTime, serial Excel, dd-mm-yyyy, dd/mm/yy, yyyy-mm-dd, "/31-05-26", angka tahun saja.
     */
    public static function parse($v, ?int $assumeYear = null): ?string
    {
        if ($v === null || $v === '') return null;
        if ($v instanceof \DateTimeInterface) return $v->format('Y-m-d');

        if (is_int($v) || is_float($v)) {
            $n = (float)$v;
            // Angka tahun saja (mis. 2025)
            if ($n >= 1900 && $n <= 2200 && floor($n) == $n) return null;
            return self::fromExcelSerial($n);
        }

        $s = trim((string)$v);
        if ($s === '' || $s === '-') return null;
        if ($s[0] === '#') return null;                 // #REF! / #VALUE!
        $s = ltrim($s, "/\\ \t");
        $s = preg_replace('/\s+/', ' ', $s) ?? $s;

        // Serial Excel dalam bentuk teks
        if (preg_match('/^\d+(\.\d+)?$/', $s)) {
            $n = (float)$s;
            if ($n >= 1900 && $n <= 2200 && floor($n) == $n) return null;
            if ($n > 0 && $n < 500000) return self::fromExcelSerial($n);
            return null;
        }

        // Nama bulan Indonesia: "31 Desember 2025"
        if (preg_match('/^(\d{1,2})\s+([A-Za-z]+)\s+(\d{2,4})$/u', $s, $m)) {
            $bln = self::SHEET_BULAN[strtoupper($m[2])] ?? null;
            if ($bln) {
                $y = self::normYear((int)$m[3]);
                return self::mk($y, $bln, (int)$m[1]);
            }
        }

        // ISO: yyyy-mm-dd atau yyyy/mm/dd
        if (preg_match('/^(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})/', $s, $m)) {
            return self::mk((int)$m[1], (int)$m[2], (int)$m[3]);
        }

        // dd-mm-yyyy / dd/mm/yy / dd.mm.yyyy  (urutan hari-bulan, gaya Indonesia)
        if (preg_match('/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{2,4})/', $s, $m)) {
            $d = (int)$m[1]; $mo = (int)$m[2]; $y = self::normYear((int)$m[3]);
            if ($d > 31 && $mo <= 12) { [$d, $mo] = [$mo, $d]; }
            // Bila hari > 12 tidak mungkin bulan, sudah benar. Bila keduanya <=12 tetap pakai d-m (Indonesia).
            return self::mk($y, $mo, $d);
        }

        // dd-mm (tanpa tahun) -> pakai assumeYear
        if ($assumeYear && preg_match('/^(\d{1,2})[-\/.](\d{1,2})$/', $s, $m)) {
            return self::mk($assumeYear, (int)$m[2], (int)$m[1]);
        }

        // Upaya terakhir
        try {
            $d = new \DateTime($s);
            $y = (int)$d->format('Y');
            if ($y >= 1900 && $y <= 2200) return $d->format('Y-m-d');
        } catch (\Throwable $e) {}
        return null;
    }

    private static function normYear(int $y): int
    {
        if ($y >= 100) return $y;
        return $y <= 69 ? 2000 + $y : 1900 + $y;
    }

    private static function mk(int $y, int $m, int $d): ?string
    {
        if ($m < 1 || $m > 12 || $d < 1 || $d > 31 || $y < 1900 || $y > 2200) return null;
        if (!checkdate($m, $d, $y)) return null;
        return sprintf('%04d-%02d-%02d', $y, $m, $d);
    }

    /** Serial Excel (sistem 1900) -> Y-m-d. */
    public static function fromExcelSerial(float $serial): ?string
    {
        if ($serial <= 0 || $serial > 500000) return null;
        // Excel menganggap 1900 tahun kabisat: serial 60 = 29-02-1900 (tidak ada)
        $days = (int)floor($serial);
        if ($days >= 60) $days -= 1;
        $ts = ($days - 25568) * 86400;   // 25568 = hari dari 1899-12-31 ke 1970-01-01 (setelah koreksi)
        $d = (new \DateTime('@' . $ts))->setTimezone(new \DateTimeZone('UTC'));
        $y = (int)$d->format('Y');
        if ($y < 1900 || $y > 2200) return null;
        return $d->format('Y-m-d');
    }

    public static function toExcelSerial(?string $ymd): ?float
    {
        if (!$ymd) return null;
        try {
            $d = new \DateTime($ymd . ' 00:00:00', new \DateTimeZone('UTC'));
        } catch (\Throwable $e) { return null; }
        $days = (int)floor($d->getTimestamp() / 86400) + 25568;
        if ($days >= 60) $days += 1;
        return (float)$days;
    }

    /** Y-m-d -> DD-MM-YYYY (atau format dari pengaturan). */
    public static function fmt(?string $ymd, ?string $format = null): string
    {
        if (!$ymd || $ymd === '0000-00-00') return '-';
        $format = $format ?: Settings::str('format.format_tanggal', 'd-m-Y');
        try { return (new \DateTime($ymd))->format($format); }
        catch (\Throwable $e) { return '-'; }
    }

    public static function fmtDateTime(?string $dt, ?string $format = null): string
    {
        if (!$dt || str_starts_with($dt, '0000')) return '-';
        $format = $format ?: Settings::str('format.format_tanggal_waktu', 'd-m-Y H:i');
        try { return (new \DateTime($dt))->format($format); }
        catch (\Throwable $e) { return '-'; }
    }

    /** "27 Juli 2026" */
    public static function fmtLong(?string $ymd): string
    {
        if (!$ymd || $ymd === '0000-00-00') return '-';
        try {
            $d = new \DateTime($ymd);
            return $d->format('j') . ' ' . self::BULAN[(int)$d->format('n')] . ' ' . $d->format('Y');
        } catch (\Throwable $e) { return '-'; }
    }

    /** "Senin, 27 Juli 2026" */
    public static function fmtFull(?string $ymd): string
    {
        if (!$ymd) return '-';
        try {
            $d = new \DateTime($ymd);
            return (self::HARI[$d->format('l')] ?? '') . ', ' . self::fmtLong($ymd);
        } catch (\Throwable $e) { return '-'; }
    }

    public static function today(): string
    {
        return (new \DateTime('now', new \DateTimeZone(Settings::str('umum.zona_waktu', 'Asia/Jakarta'))))->format('Y-m-d');
    }

    public static function now(): string
    {
        return (new \DateTime('now', new \DateTimeZone(Settings::str('umum.zona_waktu', 'Asia/Jakarta'))))->format('Y-m-d H:i:s');
    }

    public static function addDays(string $ymd, int $days): string
    {
        $d = new \DateTime($ymd);
        $d->modify(($days >= 0 ? '+' : '-') . abs($days) . ' days');
        return $d->format('Y-m-d');
    }

    /** Selisih hari b - a. */
    public static function diffDays(?string $a, ?string $b): ?int
    {
        if (!$a || !$b) return null;
        try {
            $da = new \DateTime($a . ' 00:00:00');
            $db = new \DateTime($b . ' 00:00:00');
            return (int)round(($db->getTimestamp() - $da->getTimestamp()) / 86400);
        } catch (\Throwable $e) { return null; }
    }

    public static function monthStart(string $ymd): string { return (new \DateTime($ymd))->format('Y-m-01'); }
    public static function monthEnd(string $ymd): string   { return (new \DateTime($ymd))->format('Y-m-t'); }

    public static function monthName(int $m, bool $short = false): string
    {
        return $short ? (self::BULAN_SINGKAT[$m] ?? '') : (self::BULAN[$m] ?? '');
    }

    public static function periodLabel(string $from, string $to): string
    {
        if ($from === $to) return self::fmtLong($from);
        return self::fmtLong($from) . ' s/d ' . self::fmtLong($to);
    }

    /** Rentang tanggal untuk preset periode. */
    public static function range(string $preset, ?string $ref = null): array
    {
        $ref = $ref ?: self::today();
        $d = new \DateTime($ref);
        $y = (int)$d->format('Y'); $m = (int)$d->format('n');
        switch ($preset) {
            case 'hari_ini':        return [$ref, $ref];
            case 'kemarin':         $x = self::addDays($ref, -1); return [$x, $x];
            case 'minggu_ini':      $s = (clone $d)->modify('monday this week'); return [$s->format('Y-m-d'), (clone $s)->modify('+6 days')->format('Y-m-d')];
            case 'bulan_ini':       return [sprintf('%04d-%02d-01', $y, $m), (clone $d)->format('Y-m-t')];
            case 'bulan_lalu':      $p = (clone $d)->modify('first day of last month'); return [$p->format('Y-m-01'), $p->format('Y-m-t')];
            case 'kuartal_ini':     $q = intdiv($m - 1, 3); return [sprintf('%04d-%02d-01', $y, $q * 3 + 1), (new \DateTime(sprintf('%04d-%02d-01', $y, $q * 3 + 3)))->format('Y-m-t')];
            case 'kuartal_lalu':
                $q = intdiv($m - 1, 3) - 1; $yy = $y;
                if ($q < 0) { $q = 3; $yy--; }
                return [sprintf('%04d-%02d-01', $yy, $q * 3 + 1), (new \DateTime(sprintf('%04d-%02d-01', $yy, $q * 3 + 3)))->format('Y-m-t')];
            case 'tahun_ini':       return [sprintf('%04d-01-01', $y), sprintf('%04d-12-31', $y)];
            case 'tahun_lalu':      return [sprintf('%04d-01-01', $y - 1), sprintf('%04d-12-31', $y - 1)];
            case 'ytd':             return [sprintf('%04d-01-01', $y), $ref];
            case '30_hari':         return [self::addDays($ref, -29), $ref];
            case '90_hari':         return [self::addDays($ref, -89), $ref];
            default:                return [sprintf('%04d-%02d-01', $y, $m), (clone $d)->format('Y-m-t')];
        }
    }

    public static function daysInRange(string $from, string $to): int
    {
        $n = self::diffDays($from, $to);
        return $n === null ? 0 : $n + 1;
    }
}
