<?php

namespace App\Services\PrintRequests;

use App\Models\AdminNotificationSubject;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\PrintRequest;
use App\Models\User;
use App\Services\ApplicationUrlService;
use App\Services\Email\EmailDispatchService;
use Carbon\CarbonInterface;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

/**
 * legacy {@see \App\Controllers\PrintRequests} parity (SPA JSON + uploads).
 */
class PrintRequestsPortalService
{
    public function __construct(
        private EmailDispatchService $mail,
        private ApplicationUrlService $urls,
    ) {}

    public function storageDirectory(): string
    {
        return storage_path('app/private/print_requests');
    }

    public function ensureStorageDirectoryExists(): void
    {
        $dir = $this->storageDirectory();
        if (! is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
    }

    /**
     * @return list<string>
     */
    public function candidateAbsolutePaths(string $storedName): array
    {
        $safe = basename(trim($storedName));

        return [
            $this->storageDirectory().DIRECTORY_SEPARATOR.$safe,
            public_path('uploads/print_requests/'.$safe),
        ];
    }

    public function resolveExistingFilePath(string $storedName): ?string
    {
        if (trim($storedName) === '') {
            return null;
        }

        foreach ($this->candidateAbsolutePaths($storedName) as $path) {
            if (is_file($path)) {
                return $path;
            }
        }

        return null;
    }

    /**
     * @return array{sundays: list<string>, times: list<string>}
     */
    public function buildRequiredByOptions(): array
    {
        $tz = new \DateTimeZone('America/Chicago');
        $currentDate = new \DateTime('now', $tz);
        $currentYear = (int) $currentDate->format('Y');
        $endYear = ($currentDate->format('n') > 6) ? $currentYear + 1 : $currentYear;
        $endDate = new \DateTime("first Sunday of June {$endYear}", $tz);
        $endDate->modify('+1 week');

        $sundays = [];
        $date = new \DateTime('now', $tz);
        $date->setTime(0, 0, 0);
        if ($date->format('w') !== '0') {
            $date->modify('next Sunday');
        }
        while ($date < $endDate) {
            $sundays[] = $date->format('Y-m-d');
            $date->modify('+1 week');
        }

        $times = [];
        $start = new \DateTime('10:00', $tz);
        $end = new \DateTime('13:00', $tz);
        $interval = new \DateInterval('PT15M');
        $period = new \DatePeriod($start, $interval, $end);
        foreach ($period as $time) {
            $times[] = $time->format('H:i');
        }

        return [
            'sundays' => $sundays,
            'times' => $times,
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function teacherPrintRequests(int $teacherId): array
    {
        return DB::table('print_requests as pr')
            ->select('pr.*', 'admins.firstname as admin_firstname', 'admins.lastname as admin_lastname')
            ->leftJoin('users as admins', 'admins.id', '=', 'pr.admin_id')
            ->where('pr.teacher_id', $teacherId)
            ->orderBy('pr.id', 'desc')
            ->get()
            ->map(fn ($r) => $this->normalizePrintRequestRow((array) $r))
            ->all();
    }

    /**
     * Admin queue ordering per legacy raw SQL.
     *
     * @return list<array<string, mixed>>
     */
    public function adminPrintRequests(): array
    {
        return DB::table('print_requests as pr')
            ->select(
                'pr.*',
                'u.firstname',
                'u.lastname',
                'cs.class_section_name',
                'admins.firstname as admin_firstname',
                'admins.lastname as admin_lastname'
            )
            ->leftJoin('users as u', 'u.id', '=', 'pr.teacher_id')
            ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'pr.class_id')
            ->leftJoin('users as admins', 'admins.id', '=', 'pr.admin_id')
            ->orderByRaw("CASE
                    WHEN pr.status = 'not_assigned' THEN 1
                    WHEN pr.status = 'assigned' THEN 2
                    WHEN pr.status = 'done' THEN 3
                    WHEN pr.status = 'delivered' THEN 4
                    ELSE 5
                END ASC")
            ->orderBy('pr.required_by', 'asc')
            ->get()
            ->map(fn ($r) => $this->normalizePrintRequestRow((array) $r))
            ->all();
    }

    public function defaultClassSectionIdForTeacher(int $teacherId): ?int
    {
        $row = DB::table('teacher_class as tc')
            ->join('classSection as cs', 'tc.class_section_id', '=', 'cs.class_section_id')
            ->where('tc.teacher_id', $teacherId)
            ->whereNotNull('tc.class_section_id')
            ->select('cs.class_section_id')
            ->orderBy('tc.class_section_id')
            ->first();

        return $row ? (int) $row->class_section_id : null;
    }

    /**
     * @param array<string, mixed> $requestData merged row after update for notifications
     */
    public function notifyAdminsForPrintRequest(int $printRequestId, array $requestData, string $event = 'created'): void
    {
        try {
            $event = strtolower(trim($event));
            if (
                ! Schema::hasTable('admin_notification_subjects')
                || ! Schema::hasTable('notifications')
                || ! Schema::hasTable('user_notifications')
            ) {
                return;
            }

            $adminIds = AdminNotificationSubject::query()
                ->where('subject', 'print_requests')
                ->pluck('admin_id')
                ->map(fn ($id) => (int) $id)
                ->unique()
                ->filter(fn ($id) => $id > 0)
                ->values()
                ->all();

            if ($adminIds === []) {
                return;
            }

            $teacherId = (int) ($requestData['teacher_id'] ?? 0);
            $teacherName = '';
            if ($teacherId > 0) {
                $teacher = User::query()->find($teacherId);
                if ($teacher) {
                    $teacherName = trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? ''));
                }
            }
            if ($teacherName === '') {
                $teacherName = $teacherId > 0 ? ('Teacher #'.$teacherId) : 'Teacher';
            }

            $className = '';
            $classId = $requestData['class_id'] ?? null;
            if ($classId !== null && $classId !== '') {
                $className = (string) (ClassSection::getClassSectionNameBySectionId((int) $classId) ?? '');
            }

            $eventMap = [
                'created' => [
                    'title' => 'New Print Request',
                    'intro' => 'A new print request has been submitted.',
                    'lead' => 'New print request from ',
                ],
                'updated' => [
                    'title' => 'Print Request Updated',
                    'intro' => 'A print request has been updated.',
                    'lead' => 'Updated print request from ',
                ],
                'deleted' => [
                    'title' => 'Print Request Removed',
                    'intro' => 'A print request has been deleted.',
                    'lead' => 'Print request removed for ',
                ],
            ];
            $eventConfig = $eventMap[$event] ?? $eventMap['created'];

            $filePath = trim((string) ($requestData['file_path'] ?? ''));
            $isCopyRequest = $filePath === '';

            $messageParts = [$eventConfig['lead'].$teacherName];
            if ($className !== '') {
                $messageParts[] = 'Class: '.$className;
            }
            if (! empty($requestData['num_copies'])) {
                $messageParts[] = 'Copies: '.(int) $requestData['num_copies'];
            }
            if (! empty($requestData['required_by'])) {
                $messageParts[] = 'Needed by: '.$requestData['required_by'];
            }
            if (! empty($requestData['page_selection'])) {
                $messageParts[] = 'Pages: '.$requestData['page_selection'];
            }
            if (! empty($requestData['pickup_method'])) {
                $messageParts[] = 'Pickup: '.$requestData['pickup_method'];
            }
            $message = implode(' | ', $messageParts);

            $actionUrl = $this->urls->spaAdminPrintRequestsUrl();

            $payload = [
                'title' => $eventConfig['title'],
                'message' => $message,
                'target_group' => 'admin',
                'delivery_channels' => 'in_app,email',
                'priority' => 'normal',
                'status' => 'pending',
                'action_url' => $isCopyRequest ? '' : $actionUrl,
                'scheduled_at' => function_exists('utc_now') ? utc_now() : now(),
                'school_year' => Configuration::getConfigValueByKey('school_year'),
                'semester' => Configuration::getConfigValueByKey('semester'),
            ];

            $notificationFields = Schema::getColumnListing('notifications');
            if ($notificationFields !== []) {
                $payload = array_intersect_key($payload, array_flip($notificationFields));
            }

            $notificationId = (int) DB::table('notifications')->insertGetId(array_merge($payload, [
                'created_at' => now(),
                'updated_at' => now(),
            ]));

            if ($notificationId <= 0) {
                return;
            }

            $userNotificationFields = Schema::getColumnListing('user_notifications');
            $allowedUn = array_flip($userNotificationFields !== [] ? $userNotificationFields : []);

            foreach ($adminIds as $adminId) {
                $row = [
                    'notification_id' => $notificationId,
                    'user_id' => $adminId,
                    'is_read' => 0,
                    'delivered' => 0,
                ];
                if ($allowedUn !== []) {
                    $row = array_intersect_key($row, $allowedUn);
                }
                DB::table('user_notifications')->insert($row);
            }

            $adminRows = User::query()
                ->select(['id', 'firstname', 'lastname', 'email'])
                ->whereIn('id', $adminIds)
                ->get();

            $subject = $eventConfig['title'];
            foreach ($adminRows as $adminRow) {
                $email = trim((string) ($adminRow->email ?? ''));
                if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
                    continue;
                }
                $body = '<p>'.e($eventConfig['intro']).'</p>'
                    .'<p><strong>From:</strong> '.e($teacherName).'</p>'
                    .($className !== '' ? '<p><strong>Class:</strong> '.e($className).'</p>' : '')
                    .(! empty($requestData['num_copies']) ? '<p><strong>Copies:</strong> '.(int) $requestData['num_copies'].'</p>' : '')
                    .(! empty($requestData['required_by']) ? '<p><strong>Needed by:</strong> '.e((string) $requestData['required_by']).'</p>' : '')
                    .(! empty($requestData['page_selection']) ? '<p><strong>Pages:</strong> '.e((string) $requestData['page_selection']).'</p>' : '')
                    .(! empty($requestData['pickup_method']) ? '<p><strong>Pickup Method:</strong> '.e((string) $requestData['pickup_method']).'</p>' : '')
                    .($isCopyRequest ? '' : '<p><a href="'.e($actionUrl).'">View print requests</a></p>');

                $this->mail->send($email, $subject, $body, 'notifications');
            }
        } catch (\Throwable $e) {
            logger()->error('Print request notification failed: '.$e->getMessage());
        }
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    public function normalizePrintRequestRow(array $row): array
    {
        foreach (['required_by'] as $key) {
            if (! isset($row[$key])) {
                continue;
            }
            $v = $row[$key];
            if ($v instanceof CarbonInterface) {
                $row[$key] = $v->format('Y-m-d H:i:s');
            }
        }

        return $row;
    }

    /**
     * @return array<string, list<string>>
     */
    public static function allowedStatusTransitions(): array
    {
        return [
            'not_assigned' => ['assigned'],
            'assigned' => ['not_assigned', 'done'],
            'done' => ['delivered'],
            'delivered' => [],
        ];
    }

    public function teacherOwnsRequest(PrintRequest $request, int $teacherUserId): bool
    {
        return (int) $request->teacher_id === $teacherUserId;
    }

    public function teacherMayEditOrDelete(PrintRequest $request): bool
    {
        return in_array((string) $request->status, ['not_assigned', 'assigned'], true);
    }

    /**
     * Teacher dashboard payload (parity with legacy teacher_index).
     *
     * @return array{print_requests: list<array<string, mixed>>, sundays: list<string>, times: list<string>, class_id: int|null}
     */
    public function teacherBootstrap(int $teacherId): array
    {
        $opts = $this->buildRequiredByOptions();

        return [
            'print_requests' => $this->teacherPrintRequests($teacherId),
            'sundays' => $opts['sundays'],
            'times' => $opts['times'],
            'class_id' => $this->defaultClassSectionIdForTeacher($teacherId),
        ];
    }

    /**
     * @param  array<string, mixed>  $data  validated: class_id, page_selection?, num_copies, required_by, pickup_method
     */
    public function storeTeacherUpload(array $data, UploadedFile $file, int $teacherId): PrintRequest
    {
        $this->ensureStorageDirectoryExists();
        $ext = $this->safeExtensionForUpload($file);
        $stored = Str::uuid()->toString().($ext !== '' ? '.'.$ext : '');
        $file->move($this->storageDirectory(), $stored);

        $row = [
            'teacher_id' => $teacherId,
            'class_id' => (int) $data['class_id'],
            'file_path' => $stored,
            'page_selection' => isset($data['page_selection']) ? trim((string) $data['page_selection']) : null,
            'num_copies' => (int) $data['num_copies'],
            'required_by' => $data['required_by'],
            'pickup_method' => (string) $data['pickup_method'],
            'status' => 'not_assigned',
        ];

        $model = PrintRequest::query()->create($row);
        $payload = array_merge($row, ['id' => $model->id, 'teacher_id' => $teacherId]);
        $this->notifyAdminsForPrintRequest((int) $model->id, $payload, 'created');

        return $model->fresh();
    }

    /**
     * @param  array<string, mixed>  $data  num_copies, required_by, pickup_method, page_selection?
     */
    public function updateTeacherFields(PrintRequest $request, array $data, ?UploadedFile $newFile): PrintRequest
    {
        $update = [
            'num_copies' => (int) $data['num_copies'],
            'required_by' => $data['required_by'],
            'pickup_method' => (string) $data['pickup_method'],
            'page_selection' => isset($data['page_selection']) ? trim((string) $data['page_selection']) : null,
        ];

        if ($newFile !== null) {
            $this->ensureStorageDirectoryExists();
            $old = trim((string) ($request->file_path ?? ''));
            if ($old !== '') {
                foreach ($this->candidateAbsolutePaths($old) as $abs) {
                    if (is_file($abs)) {
                        @unlink($abs);
                        break;
                    }
                }
            }
            $ext = $this->safeExtensionForUpload($newFile);
            $stored = Str::uuid()->toString().($ext !== '' ? '.'.$ext : '');
            $newFile->move($this->storageDirectory(), $stored);
            $update['file_path'] = $stored;
        }

        $request->update($update);
        $merged = array_merge($request->toArray(), $update, ['id' => $request->id]);
        $this->notifyAdminsForPrintRequest((int) $request->id, $merged, 'updated');

        return $request->fresh();
    }

    private function safeExtensionForUpload(UploadedFile $file): string
    {
        $allowed = [
            'application/pdf' => 'pdf',
            'image/jpeg' => 'jpg',
            'image/png' => 'png',
            'text/plain' => 'txt',
            'application/msword' => 'doc',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
        ];

        $mime = strtolower((string) $file->getMimeType());
        if (! isset($allowed[$mime])) {
            throw new \InvalidArgumentException('Unsupported upload type.');
        }

        if ((int) $file->getSize() > 5 * 1024 * 1024) {
            throw new \InvalidArgumentException('Uploaded file too large. Max 5MB.');
        }

        return $allowed[$mime];
    }

    public function applyAdminStatus(PrintRequest $request, string $newStatus, int $adminUserId): PrintRequest
    {
        $current = (string) $request->status;
        $allowed = self::allowedStatusTransitions();
        if (! isset($allowed[$current]) || ! in_array($newStatus, $allowed[$current], true)) {
            if ($current === $newStatus) {
                return $request;
            }
            throw new \InvalidArgumentException('Invalid status transition.');
        }

        $data = ['status' => $newStatus];
        if ($newStatus === 'assigned') {
            $data['admin_id'] = $adminUserId;
        } elseif ($newStatus === 'not_assigned') {
            $data['admin_id'] = null;
        }

        $request->update($data);

        return $request->fresh();
    }

    public function deleteTeacherRequest(PrintRequest $request): void
    {
        $this->notifyAdminsForPrintRequest((int) $request->id, $request->toArray(), 'deleted');

        $fp = trim((string) ($request->file_path ?? ''));
        if ($fp !== '') {
            foreach ($this->candidateAbsolutePaths($fp) as $abs) {
                if (is_file($abs)) {
                    @unlink($abs);
                    break;
                }
            }
        }

        $request->delete();
    }

    public function copyFromExisting(PrintRequest $source, int $teacherId): PrintRequest
    {
        $fp = trim((string) ($source->file_path ?? ''));
        if ($fp === '' || $this->resolveExistingFilePath($fp) === null) {
            throw new \InvalidArgumentException('The original file is unavailable for copying.');
        }

        $row = [
            'teacher_id' => $teacherId,
            'class_id' => (int) $source->class_id,
            'file_path' => $fp,
            'page_selection' => $source->page_selection,
            'num_copies' => (int) $source->num_copies,
            'required_by' => $source->required_by,
            'pickup_method' => (string) $source->pickup_method,
            'status' => 'not_assigned',
        ];

        $model = PrintRequest::query()->create($row);
        $this->notifyAdminsForPrintRequest((int) $model->id, array_merge($row, ['id' => $model->id, 'teacher_id' => $teacherId]), 'created');

        return $model->fresh();
    }

    /**
     * Copy-center request without uploading a file (legacy createCopy).
     *
     * @param  array<string, mixed>  $data  class_id, num_copies, required_by, pickup_method
     */
    public function storeHandCopy(array $data, int $teacherId): PrintRequest
    {
        $row = [
            'teacher_id' => $teacherId,
            'class_id' => (int) $data['class_id'],
            'file_path' => '',
            'page_selection' => null,
            'num_copies' => (int) $data['num_copies'],
            'required_by' => $data['required_by'],
            'pickup_method' => (string) $data['pickup_method'],
            'status' => 'not_assigned',
        ];

        $model = PrintRequest::query()->create($row);
        $this->notifyAdminsForPrintRequest((int) $model->id, array_merge($row, ['id' => $model->id, 'teacher_id' => $teacherId]), 'created');

        return $model->fresh();
    }

    /**
     * Teacher may download own files; admins may download any.
     */
    public function authorizeDownload(PrintRequest $request, int $userId, bool $userIsPrintAdmin): bool
    {
        if ($userIsPrintAdmin) {
            return true;
        }

        return $this->teacherOwnsRequest($request, $userId);
    }

    public function absolutePathForDownload(PrintRequest $request): ?string
    {
        $fp = trim((string) ($request->file_path ?? ''));
        if ($fp === '') {
            return null;
        }

        return $this->resolveExistingFilePath($fp);
    }
}
