<?php

namespace App\Services\AttendanceTracking;

use App\Models\AttendanceTracking;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Throwable;

class ViolationRuleEngineService
{
    protected array $debugCompute = [];
    protected ?array $attendanceReportedColumns = null;

    public function __construct(
        protected AttendanceTracking $attendanceTrackingModel,
        protected AttendanceParentLookupService $parentLookupService
    ) {
    }

    public function getDebugCompute(): array
    {
        return $this->debugCompute;
    }

    public function computeViolations(
        array $students,
        array $attendanceData,
        string $schoolYear,
        ?string $semester = null,
        ?array $weekRowsFallback = null
    ): array {
        $this->debugCompute = ['active_weeks' => 0, 'by_student' => 0];

        $studentCodeToId = [];
        foreach ($students as $stu) {
            $code = trim((string) ($stu['student_code'] ?? $stu['school_id'] ?? ''));
            if ($code !== '') {
                $studentCodeToId[$code] = (int) ($stu['id'] ?? 0);
            }
        }

        $byStudent = [];
        foreach ($attendanceData as $r) {
            $rawSid = $r['student_id'] ?? null;
            $sid = is_numeric($rawSid)
                ? (int) $rawSid
                : ($studentCodeToId[trim((string) $rawSid)] ?? 0);

            if ($sid <= 0) {
                continue;
            }

            $ymd  = substr((string) ($r['date'] ?? ''), 0, 10);
            $stat = strtolower(trim((string) ($r['status'] ?? '')));

            if (!in_array($stat, ['absent', 'late'], true)) {
                continue;
            }

            $byStudent[$sid][$stat][] = $ymd;
        }

        $this->debugCompute['by_student'] = count($byStudent);

        [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear);

        $weekRowsSource = null;
        if (!empty($weekRowsFallback)) {
            foreach ($weekRowsFallback as $r) {
                $st = strtolower(trim((string) ($r['status'] ?? '')));
                if ($st !== '' && !in_array($st, ['absent', 'late'], true)) {
                    $weekRowsSource = $weekRowsFallback;
                    break;
                }
            }
        }

        $activeWeekKeys = $this->getActiveWeeksFromAttendanceData($syStart, $syEnd, $schoolYear, $semester, $weekRowsSource);
        $this->debugCompute['active_weeks'] = count($activeWeekKeys);

        if (empty($activeWeekKeys)) {
            return [];
        }

        $weekIndex          = array_flip($activeWeekKeys);
        $currentWeekIdx     = count($activeWeekKeys) - 1;
        $windowStartIdx     = max(0, $currentWeekIdx - 4);
        $requiredWeeksCount = $currentWeekIdx - $windowStartIdx + 1;

        $keepLastWeeks = function (array $dates) use ($weekIndex, $windowStartIdx, $currentWeekIdx): array {
            $out = [];
            foreach ($dates as $d) {
                $key = date('o-\WW', strtotime($d));
                if (isset($weekIndex[$key])) {
                    $idx = $weekIndex[$key];
                    $ymd = substr((string) $d, 0, 10);
                    if ($idx >= $windowStartIdx && $idx <= $currentWeekIdx) {
                        $out[] = $ymd;
                    }
                }
            }
            $out = array_values(array_unique($out));
            rsort($out);
            return $out;
        };

        $classByStudent = [];
        try {
            $studentIdsForClass = array_values(array_unique(array_map(
                static fn($s) => (int) ($s['id'] ?? 0),
                $students
            )));

            if (!empty($studentIdsForClass)) {
                $rows = DB::table('student_class as sc')
                    ->select([
                        'sc.student_id',
                        'sc.class_section_id',
                        'cs.class_section_name',
                        'cs.class_id',
                        'c.class_name',
                    ])
                    ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
                    ->leftJoin('classes as c', 'c.id', '=', 'cs.class_id')
                    ->whereIn('sc.student_id', $studentIdsForClass)
                    ->where('sc.school_year', $schoolYear)
                    ->orderByDesc('sc.id')
                    ->get();

                foreach ($rows as $row) {
                    $sid = (int) ($row->student_id ?? 0);
                    if ($sid <= 0) {
                        continue;
                    }

                    if (!isset($classByStudent[$sid])) {
                        $classByStudent[$sid] = [
                            'class_section_id'   => $row->class_section_id ?? null,
                            'class_section_name' => (string) ($row->class_section_name ?? ''),
                            'class_id'           => $row->class_id ?? null,
                            'class_name'         => (string) ($row->class_name ?? ''),
                        ];
                    }
                }
            }
        } catch (Throwable $e) {
            Log::debug('computeViolations(): class prefetch failed: ' . $e->getMessage());
        }

        $out = [];
        $absCountLast5 = 0;
        $lateCountLast5 = 0;

        foreach ($students as $stu) {
            $sid  = (int) ($stu['id'] ?? 0);
            $name = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? ''));

            $absDatesAll  = array_values(array_unique($byStudent[$sid]['absent'] ?? []));
            $lateDatesAll = array_values(array_unique($byStudent[$sid]['late'] ?? []));

            $absDates  = $keepLastWeeks($absDatesAll);
            $lateDates = $keepLastWeeks($lateDatesAll);

            if (empty($absDates) && empty($lateDates)) {
                continue;
            }

            $absWeekIdx  = $this->datesToWeekIndices($absDates, $weekIndex);
            $lateWeekIdx = $this->datesToWeekIndices($lateDates, $weekIndex);
            $absCountLast5  += count($absDates);
            $lateCountLast5 += count($lateDates);

            $absenceViolation = null;
            if (!empty($absDates)) {
                $lastAbsDate = $absDates[0];

                $A2row = $this->hasNConsecutiveItems($absDates, 2, 7);
                $A3row = $this->hasNConsecutiveItems($absDates, 3, 7);
                $A4row = $this->hasNConsecutiveItems($absDates, 4, 7);

                $A_in3w2 = $this->hasNInWActiveWeeks($absWeekIdx, 2, 3);
                $A_in4w3 = $this->hasNInWActiveWeeks($absWeekIdx, 3, 4);
                $A_in5w4 = $this->hasNInWActiveWeeks($absWeekIdx, 4, 5);

                if ($A4row || $A_in5w4) {
                    $absenceViolation = [
                        'type'        => 'absence',
                        'code'        => 'ABS_4',
                        'severity'    => 4,
                        'action'      => 'expel_notify',
                        'color'       => '#ff4d4f',
                        'title'       => '4 Absences (in a row or within last 5 active weeks)',
                        'description' => 'Notify team to inform parent about expulsion and send email.',
                        'last_date'   => $lastAbsDate,
                    ];
                } elseif ($A3row || $A_in4w3) {
                    $absenceViolation = [
                        'type'        => 'absence',
                        'code'        => 'ABS_3',
                        'severity'    => 3,
                        'action'      => 'team_notify',
                        'color'       => '#fa8c16',
                        'title'       => '3 Absences (in a row or within last 4 active weeks)',
                        'description' => 'Team calls parent and sends email.',
                        'last_date'   => $lastAbsDate,
                    ];
                } elseif ($A2row || $A_in3w2) {
                    $absenceViolation = [
                        'type'        => 'absence',
                        'code'        => 'ABS_2',
                        'severity'    => 2,
                        'action'      => 'team_notify',
                        'color'       => '#fadb14',
                        'title'       => '2 Absences (in a row or within last 3 active weeks)',
                        'description' => 'Team calls parent and sends email.',
                        'last_date'   => $lastAbsDate,
                    ];
                }
            }

            $lateViolation = null;
            if (!empty($lateDates)) {
                $lastLateDate = $lateDates[0];

                $L2row   = $this->hasNConsecutiveItems($lateDates, 2, 7);
                $L3row   = $this->hasNConsecutiveItems($lateDates, 3, 7);
                $L4row   = $this->hasNConsecutiveItems($lateDates, 4, 7);

                $L_in3w2 = $this->hasNInWActiveWeeks($lateWeekIdx, 2, 3);
                $L_in4w3 = $this->hasNInWActiveWeeks($lateWeekIdx, 3, 4);

                $lateCoversAllWindowWeeks = false;
                if ($requiredWeeksCount >= 4) {
                    $lateWeeksSet = array_flip($lateWeekIdx);
                    $lateCoversAllWindowWeeks = true;
                    for ($i = $windowStartIdx; $i <= $currentWeekIdx; $i++) {
                        if (!isset($lateWeeksSet[$i])) {
                            $lateCoversAllWindowWeeks = false;
                            break;
                        }
                    }
                }

                if ($L4row || $lateCoversAllWindowWeeks) {
                    $lateViolation = [
                        'type'        => 'late',
                        'code'        => 'LATE_4',
                        'severity'    => 4,
                        'action'      => 'team_notify',
                        'color'       => '#ff4d4f',
                        'title'       => '4 Lates in a row OR in each of the last 4 active weeks',
                        'description' => 'Team calls parent and sends email.',
                        'last_date'   => $lastLateDate,
                    ];
                } else {
                    $twoLatesOneAbsWithin4 = $this->twoLatesOneAbsInWWeeks($lateWeekIdx, $absWeekIdx, 4);

                    if ($L3row || $L_in4w3 || $twoLatesOneAbsWithin4) {
                        $lateViolation = [
                            'type'        => $twoLatesOneAbsWithin4 ? 'mix' : 'late',
                            'code'        => $twoLatesOneAbsWithin4 ? 'MIX_L2A1' : 'LATE_3',
                            'severity'    => 3,
                            'action'      => 'team_notify',
                            'color'       => '#002766',
                            'title'       => $twoLatesOneAbsWithin4
                                ? '2 Lates + 1 Absence (within last 4 active weeks)'
                                : '3 Lates (in a row or within last 4 active weeks)',
                            'description' => 'Team calls parent and sends email.',
                            'last_date'   => $lastLateDate,
                        ];
                    } elseif ($L2row || $L_in3w2) {
                        $lateViolation = [
                            'type'        => 'late',
                            'code'        => 'LATE_2',
                            'severity'    => 1,
                            'action'      => 'auto_email',
                            'color'       => '#bae7ff',
                            'title'       => '2 Lates (in a row or within last 3 active weeks)',
                            'description' => 'Send automated email to parent.',
                            'last_date'   => $lastLateDate,
                        ];
                    }
                }
            }

            $chosen = $this->chooseHigherSeverity($absenceViolation, $lateViolation);

            if (!$chosen && count($absDates) === 1) {
                $chosen = [
                    'type'        => 'absence',
                    'code'        => 'ABS_1',
                    'severity'    => 1,
                    'action'      => 'auto_email',
                    'color'       => '#e6f7ff',
                    'title'       => '1 Unreported Absence',
                    'description' => 'Send automated email to parent.',
                    'last_date'   => $absDates[0],
                ];
            }

            if (!$chosen) {
                continue;
            }

            $parent = $this->parentLookupService->getPrimaryParentForStudent($sid);

            [$start, $end] = $this->dayBounds($chosen['last_date']);

            $trackingQ = $this->attendanceTrackingModel->query()
                ->where('student_id', $sid)
                ->where('school_year', $schoolYear)
                ->where('date', '>=', $start)
                ->where('date', '<', $end)
                ->where('reason', 'like', '%' . $chosen['code'] . '%');

            if ($semester !== null) {
                $trackingQ->where('semester', $semester);
            }

            $tracking   = $trackingQ->orderByDesc('date')->first();
            $tracking   = $tracking?->toArray() ?? [];
            $isNotified = (bool) ($tracking['is_notified'] ?? 0);
            $notifCnt   = (int) ($tracking['notif_counter'] ?? 0);

            $cls = $classByStudent[$sid] ?? [];
            $classLabel = (string) ($cls['class_section_name'] ?? '');
            if ($classLabel === '' && !empty($cls['class_name'])) {
                $classLabel = (string) $cls['class_name'];
            }

            $out[] = [
                'id'               => $sid,
                'name'             => $name,
                'class_name'       => $classLabel,
                'class_section_name' => (string) ($cls['class_section_name'] ?? ''),
                'className'        => (string) ($cls['class_name'] ?? ''),
                'class_id'         => $cls['class_id'] ?? null,
                'class_section_id' => $cls['class_section_id'] ?? null,
                'parent_email'     => $parent['email'] ?? '',
                'parent_name'      => $parent['parent_name'] ?? '',
                'parent_phone'     => $parent['phone'] ?? null,
                'violation'        => $chosen['title'],
                'violation_code'   => $chosen['code'],
                'type'             => $chosen['type'],
                'severity'         => $chosen['severity'],
                'action'           => $chosen['action'],
                'color'            => $chosen['color'],
                'last_absence'     => $absDates[0] ?? null,
                'last_late'        => $lateDates[0] ?? null,
                'last_date'        => $chosen['last_date'],
                'is_notified'      => $isNotified,
                'notif_counter'    => $notifCnt,
                'absences'         => $absDates,
                'lates'            => $lateDates,
            ];
        }

        $this->debugCompute['abs_last5']  = $absCountLast5;
        $this->debugCompute['late_last5'] = $lateCountLast5;

        return $out;
    }

    public function chooseHigherSeverity(?array $a, ?array $b): ?array
    {
        if ($a && $b) {
            if ($a['severity'] === $b['severity']) {
                $prio = ['expel_notify' => 3, 'team_notify' => 2, 'auto_email' => 1];
                return ($prio[$a['action']] ?? 0) >= ($prio[$b['action']] ?? 0) ? $a : $b;
            }
            return $a['severity'] > $b['severity'] ? $a : $b;
        }

        return $a ?: $b;
    }

    public function attendanceReportedColumns(string $table = 'attendance_data'): array
    {
        if ($this->attendanceReportedColumns !== null) {
            return $this->attendanceReportedColumns;
        }

        $this->attendanceReportedColumns = [
            'is_reported' => Schema::hasColumn($table, 'is_reported'),
            'reported'    => Schema::hasColumn($table, 'reported'),
        ];

        return $this->attendanceReportedColumns;
    }

    public function applyUnreportedAttendanceFilter($qb, string $table = 'attendance_data'): void
    {
        $cols = $this->attendanceReportedColumns($table);

        if (!empty($cols['is_reported'])) {
            $qb->whereRaw("(LOWER(TRIM(COALESCE(is_reported, ''))) NOT IN ('yes','1','true','y','on'))");
        }
        if (!empty($cols['reported'])) {
            $qb->whereRaw("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))");
        }

        $qb->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent-reported%')")
           ->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent reported%')");

        if (Schema::hasTable('parent_attendance_reports')) {
            $qb->whereRaw("NOT EXISTS (
                SELECT 1 FROM parent_attendance_reports par
                WHERE par.student_id = {$table}.student_id
                  AND DATE(par.report_date) = DATE({$table}.date)
                  AND par.type IN ('absent','late')
            )");
        }
    }

    public function deriveSchoolYearBounds(string $schoolYear): array
    {
        if (!preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) {
            $y      = (int) date('Y');
            $startY = (int) (date('n') >= 8 ? $y : $y - 1);
            return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $startY + 1)];
        }

        $startY = (int) $m[1];
        $endY   = (int) $m[2];
        return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $endY)];
    }

    public function schoolYearForDate(string $ymd, ?string $fallbackSchoolYear = null): string
    {
        $ts = strtotime($ymd);
        if ($ts === false) {
            return (string) $fallbackSchoolYear;
        }

        $y = (int) date('Y', $ts);
        $m = (int) date('n', $ts);
        $startY = ($m >= 8) ? $y : $y - 1;

        return sprintf('%d-%d', $startY, $startY + 1);
    }

    public function dayBounds(string $ymd): array
    {
        $d = substr($ymd, 0, 10);
        return [
            $d . ' 00:00:00',
            date('Y-m-d', strtotime($d . ' +1 day')) . ' 00:00:00',
        ];
    }

    public function getActiveWeeksFromAttendanceData(
        string $startYmd,
        string $endYmd,
        ?string $schoolYear = null,
        ?string $semester = null,
        ?array $weekRowsFallback = null
    ): array {
        $weeks = [];

        if (!empty($weekRowsFallback)) {
            foreach ($weekRowsFallback as $r) {
                $d = $r['date'] ?? null;
                if (!$d) {
                    continue;
                }
                $ts = strtotime($d);
                if ($ts === false) {
                    continue;
                }
                $ymd = date('Y-m-d', $ts);
                if ($ymd < $startYmd || $ymd > $endYmd) {
                    continue;
                }
                $wk = date('o-\WW', $ts);
                $weeks[$wk] = true;
            }
        }

        if (empty($weeks)) {
            $rows = DB::table('attendance_data')
                ->selectRaw('DATE(date) AS ymd')
                ->where('school_year', $schoolYear)
                ->whereRaw('DATE(date) >= ?', [$startYmd])
                ->whereRaw('DATE(date) <= ?', [$endYmd])
                ->where(function ($q) {
                    $q->whereNull('reason')
                        ->orWhere(function ($q2) {
                            $q2->where('reason', 'not like', '%break%')
                               ->where('reason', 'not like', '%cancel%');
                        });
                })
                ->when($semester !== null && $semester !== '', fn($q) => $q->where('semester', $semester))
                ->groupBy(DB::raw('DATE(date)'))
                ->orderBy('ymd')
                ->get();

            foreach ($rows as $r) {
                $wk = date('o-\WW', strtotime($r->ymd));
                $weeks[$wk] = true;
            }
        }

        $keys = array_keys($weeks);
        sort($keys, SORT_NATURAL);

        return $keys;
    }

    public function datesToWeekIndices(array $dates, array $weekIndex): array
    {
        $set = [];
        foreach ($dates as $ymd) {
            $wk = date('o-\WW', strtotime($ymd));
            if (isset($weekIndex[$wk])) {
                $set[$weekIndex[$wk]] = true;
            }
        }
        $idx = array_keys($set);
        sort($idx);
        return $idx;
    }

    public function windowWeeksForViolationCode(string $code): int
    {
        $code = strtoupper(trim($code));
        if ($code === '') {
            return 5;
        }
        if (Str::startsWith($code, 'ABS_2') || Str::startsWith($code, 'LATE_2')) {
            return 3;
        }
        if (Str::startsWith($code, 'ABS_3') || Str::startsWith($code, 'LATE_3') || Str::startsWith($code, 'MIX')) {
            return 4;
        }
        if (Str::startsWith($code, 'LATE_4')) {
            return 4;
        }
        if (Str::startsWith($code, 'ABS_4')) {
            return 5;
        }
        return 5;
    }

    public function isoWeekStartYmd(string $weekKey): string
    {
        if (preg_match('/^(\d{4})-W(\d{2})$/', $weekKey, $m)) {
            return date('Y-m-d', strtotime($m[1] . '-W' . $m[2] . '-1'));
        }
        return date('Y-m-d');
    }

    public function activeWeekWindowStartYmd(
        string $endYmd,
        int $windowWeeks,
        string $schoolYear,
        ?string $semester
    ): string {
        $windowWeeks = max(1, $windowWeeks);
        [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear);
        $endBound = $endYmd;

        if ($syEnd !== '' && $endBound > $syEnd) {
            $endBound = $syEnd;
        }

        $activeWeekKeys = $this->getActiveWeeksFromAttendanceData($syStart, $endBound, $schoolYear, $semester, null);

        if (empty($activeWeekKeys)) {
            $days = ($windowWeeks * 7) - 1;
            return date('Y-m-d', strtotime($endBound . ' -' . $days . ' days'));
        }

        $activeWeekKeys = array_values($activeWeekKeys);
        $weekIndex = array_flip($activeWeekKeys);
        $endWeekKey = date('o-\WW', strtotime($endBound));

        if (!isset($weekIndex[$endWeekKey])) {
            $endWeekKey = null;
            for ($i = count($activeWeekKeys) - 1; $i >= 0; $i--) {
                $wkStart = $this->isoWeekStartYmd($activeWeekKeys[$i]);
                if ($wkStart <= $endBound) {
                    $endWeekKey = $activeWeekKeys[$i];
                    break;
                }
            }
            if ($endWeekKey === null) {
                $endWeekKey = end($activeWeekKeys);
            }
        }

        $currentIdx = $weekIndex[$endWeekKey] ?? (count($activeWeekKeys) - 1);
        $windowStartIdx = max(0, $currentIdx - ($windowWeeks - 1));
        $startWeekKey = $activeWeekKeys[$windowStartIdx] ?? $activeWeekKeys[0];

        return $this->isoWeekStartYmd($startWeekKey);
    }

    public function hasNConsecutiveItems(array $datesDesc, int $n, int $daysApart): bool
    {
        $N = count($datesDesc);
        if ($N < $n) {
            return false;
        }

        $dates = $datesDesc;
        sort($dates);

        $run = 1;
        for ($i = 1; $i < $N; $i++) {
            $prev = Carbon::parse($dates[$i - 1]);
            $curr = Carbon::parse($dates[$i]);
            $diff = $prev->diffInDays($curr);

            if ($diff === $daysApart) {
                $run++;
                if ($run >= $n) {
                    return true;
                }
            } else {
                $run = 1;
            }
        }

        return false;
    }

    public function hasNInWActiveWeeks(array $weekIdx, int $n, int $w): bool
    {
        $N = count($weekIdx);
        if ($N < $n) {
            return false;
        }

        $l = 0;
        for ($r = 0; $r < $N; $r++) {
            while ($weekIdx[$r] - $weekIdx[$l] > ($w - 1)) {
                $l++;
            }
            if (($r - $l + 1) >= $n) {
                return true;
            }
        }

        return false;
    }

    public function twoLatesOneAbsInWWeeks(array $lateIdx, array $absIdx, int $w): bool
    {
        if (count($lateIdx) < 2 || count($absIdx) < 1) {
            return false;
        }

        $L = $lateIdx;
        $A = $absIdx;
        $iL1 = 0;
        $iL2 = 1;
        $iA = 0;

        while ($iL2 < count($L)) {
            $windowStart = $L[$iL1];
            $windowEnd   = $windowStart + ($w - 1);

            if ($L[$iL2] > $windowEnd) {
                $iL1++;
                $iL2 = $iL1 + 1;
                continue;
            }

            while ($iA < count($A) && $A[$iA] < $windowStart) {
                $iA++;
            }

            if ($iA < count($A) && $A[$iA] <= $windowEnd) {
                return true;
            }

            $iL2++;
            if ($iL2 >= count($L)) {
                $iL1++;
                $iL2 = $iL1 + 1;
            }
        }

        return false;
    }
}