<?php

namespace routes;

use classes\authentication;
use classes\department_outside_hours_statistics_service;
use classes\workfeed;
use classes\workfeed_shift_time_resolver;
use customers\economicCustomers;
use DateInterval;
use DateTime;
use DateTimeZone;
use Exception;
use objects\department_daily_report_complaints_o;
use objects\department_daily_reports_o;
use objects\department_variables_o;
use objects\departments_o;
use objects\logs_o;
use objects\users_o;
use traits\route_t;

class departmentDailyReportsRoute
{
    use route_t;

    private const SET_PRODUCT_TARGET_PERMISSION = 'set_department_daily_report_product_targets';

    public function run(): void
    {
        $this->get('/departments/daily-reports', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('list_department_daily_reports');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORTS', 'Successfully listed departments daily reports');
                $department_daily_reports_o = new department_daily_reports_o();
                // Return the list of departments
                $response->success(
                    $department_daily_reports_o
                        ->setSearchableFields([
                            // The fields that can be searched. This would otherwise make it possible to get secret information from the database, simply by searching for it and getting the result count back
                            'id',
                            'water_usage',
                            'notes',
                            'filled_by',
                            'created_at',
                            'department_id',
                        ])
                        ->listObjectsWithPaginationIfSet(
                            function ($department_report) use ($user) {
                                return [
                                    'id' => (int)$department_report['id'],
                                    'department_id' => (int)$department_report['department_id'],
                                    'water_usage' => (int)$department_report['water_usage'],
                                    'notes' => (string)$department_report['notes'],
                                    'filled_by' => (int)$department_report['filled_by'],
                                    'created_at' => (string)$department_report['created_at'],
                                ];
                            },
                            // This makes sure that the user can only see reports from the departments they explicitly have access to
                            $department_daily_reports_o->forceRestrictFilters(
                                [
                                    // This makes sure that the user can only see orders from the departments they explicitly have access to
                                    'department_id' => $user->getGroup()->getDepartments(),
                                ]
                            )
                        )
                );
            } else {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_department_daily_reports' => 'List the department daily reports, provided the user has access to the department',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->get('/departments/daily-reports/get', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('list_department_daily_reports');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Check if the required fields are set
                self::requireParameters(
                    [
                        'id',
                        'date',
                    ]
                );
                // Validate the department_id
                self::requireType(
                    (int)self::getParameter('id'),
                    self::type_int()
                );
                // Require the department_id to be above 0
                self::requireMinValue(
                    (int)self::getParameter('id'),
                    1
                );
                /** Date parameter */
                // Validate the date
                self::validateDateLocally();
                // Determine if the user has access to the department
                self::requireDepartmentAccess((int)self::getParameter('id'));
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORTS', 'Successfully listed departments daily reports');
                // Return the list of departments
                $department_daily_reports_o = new department_daily_reports_o();
                try {
                    $department_daily_report_latest = $department_daily_reports_o->selectDepartmentReportByDate(
                        (int)self::getParameter('id'),
                        (string)self::getParameter('date')
                    );
                } catch (Exception $e) {
                    // Create a new department daily report if it does not exist
                    $department_daily_report_latest = $department_daily_reports_o->add(
                        (int)self::getParameter('id'),
                        0,
                        0,
                        '',
                        $user->id,
                        (string)self::getParameter('date')
                    );
                }
                $response->success(
                    [
                        'id' => (int)$department_daily_report_latest->id,
                        'department_id' => (int)$department_daily_report_latest->department_id->value(),
                        'water_usage' => (int)$department_daily_report_latest->water_usage->value(),
                        'notes' => (string)$department_daily_report_latest->notes->value(),
                        'filled_by' => (int)$department_daily_report_latest->filled_by->value(),
                        'created_at' => (string)$department_daily_report_latest->created_at->value(),
                    ]
                );
            } else {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_department_daily_reports' => 'List the department daily reports, provided the user has access to the department',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->post('/departments/daily-reports', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('create_department_daily_reports');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Check if the required fields are set
                self::requireParameters(
                    [
                        'department_id',
                        'water_usage',
                        'notes',
                        'date'
                    ]
                );
                // Validate the department_id
                self::requireType(
                    (int)self::getParameter('department_id'),
                    self::type_int()
                );
                // Require the department_id to be above 0
                self::requireMinValue(
                    (int)self::getParameter('department_id'),
                    1
                );
                // Validate the water_usage
                self::requireType(
                    (int)self::getParameter('water_usage'),
                    self::type_int()
                );
                // Require the water_usage to be at least 0
                self::requireMinValue(
                    (int)self::getParameter('water_usage'),
                    0
                );
                // Validate the notes
                self::requireType(
                    (string)self::getParameter('notes'),
                    self::type_string()
                );
                // Require the notes to be at least 0 characters long
                self::requireMinLength(
                    'notes',
                    0
                );
                // Require the notes to be at most 4000 characters long
                self::requireMaxLength(
                    'notes',
                    4000
                );
                // Validate the date
                self::requireType(
                    (string)self::getParameter('date'),
                    self::type_string()
                );
                // Require the date to be at least 0 characters long
                self::requireMinLength(
                    'date',
                    0
                );
                // Require the date to be at most 10 characters long
                self::requireMaxLength(
                    'date',
                    10
                );
                // Validate the date format (YYYY-MM-DD)
                self::requireDateFormat(
                    (string)self::getParameter('date'),
                    self::FORMAT_DATE()
                );
                // Determine if the user has access to the department
                self::requireDepartmentAccess((int)self::getParameter('department_id'));
                // Check if there is already a report for today
                if ((new department_daily_reports_o())->hasReport((int)self::getParameter('department_id'), (string)self::getParameter('date'))) {
                    $response->error('There is already a report for today', 400);
                }
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, $user->id, 'CREATE_DEPARTMENT_DAILY_REPORTS', 'Successfully created department daily report');
                // Return the list of departments
                $response->success(
                    (new department_daily_reports_o())
                        ->add(
                            (int)self::getParameter('department_id'),
                            (int)self::getParameter('water_usage'),
                            (int)0,
                            (string)self::getParameter('notes'),
                            $user->id
                        )->asArray()
                );
            } else {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, 0, 'CREATE_DEPARTMENT_DAILY_REPORTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'create_department_daily_reports' => 'Create a new department daily report',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->post('/departments/daily-reports/complaints', function () {
            global $response;
            $this->requirePermission('create_department_daily_report_complaints');

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'CREATE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters([
                'department_id',
                'wash_date',
                'category',
                'description',
            ]);

            self::requireType(
                (int)self::getParameter('department_id'),
                self::type_int()
            );
            self::requireMinValue(
                (int)self::getParameter('department_id'),
                1
            );

            self::requireType(
                self::getParameter('description'),
                self::type_string()
            );
            self::requireMinLength(
                'description',
                1
            );
            self::requireMaxLength(
                'description',
                4000
            );

            $description = trim((string)self::getParameter('description'));
            if ($description === '') {
                $response->error('Description is required', 400);
            }

            $wash_date = $this->requireComplaintWashDateParameter('wash_date');
            if ($wash_date === null) {
                $response->error('Wash date is required', 400);
            }

            $category = $this->requireComplaintCategoryParameter('category');
            if ($category === null) {
                $response->error('Category is required', 400);
            }

            $customer_number = null;
            if (
                self::isParametersSet(['customer_number'])
                && self::getParameter('customer_number') !== null
                && trim((string)self::getParameter('customer_number')) !== ''
            ) {
                self::requireType(
                    (int)self::getParameter('customer_number'),
                    self::type_int()
                );
                self::requireMinValue(
                    (int)self::getParameter('customer_number'),
                    1
                );

                $customer_number = (int)self::getParameter('customer_number');
                $customer = (new users_o())->getOrImportCustomerByCustomerNumber($customer_number);
                if ($customer === false || !$customer->exists()) {
                    $response->error('Customer not found', 400);
                }
            }

            self::requireDepartmentAccess((int)self::getParameter('department_id'));

            $complaint = $this->dailyReportComplaintsRepository()->addComplaint(
                (int)self::getParameter('department_id'),
                $customer_number,
                $wash_date,
                $category,
                $description,
                (int)$user->id
            );

            (new logs_o())->add('departments', 'global', 1, $user->id, 'CREATE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'Successfully created department daily report complaint');

            $response->success(
                $this->dailyReportComplaintsRepository()->parseComplaint($complaint->asArray())
            );
        },
            [
                'create_department_daily_report_complaints' => 'Create department daily report customer complaints',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->get('/departments/daily-reports/complaints/customers', function () {
            global $response;

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINT_CUSTOMERS', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            $has_create_permission = $this->hasPermission('create_department_daily_report_complaints');
            $has_edit_permission = $this->hasPermission('edit_department_daily_report_complaints');
            if (!$has_create_permission && !$has_edit_permission) {
                $this->emitForbidden([
                    'create_department_daily_report_complaints',
                    'edit_department_daily_report_complaints',
                ]);
                return;
            }

            self::requireParameters(['search']);
            self::requireType(
                self::getParameter('search'),
                self::type_string()
            );

            $search = trim((string)self::getParameter('search'));
            if (mb_strlen($search) < 2) {
                $response->error('Search must be at least 2 characters', 400);
            }

            $limit = 10;
            if (
                self::isParametersSet(['limit'])
                && self::getParameter('limit') !== null
                && trim((string)self::getParameter('limit')) !== ''
            ) {
                self::requireType(
                    (int)self::getParameter('limit'),
                    self::type_int()
                );
                self::requireMinValue(
                    (int)self::getParameter('limit'),
                    1
                );
                self::requireMaxValue(
                    (int)self::getParameter('limit'),
                    20
                );

                $limit = (int)self::getParameter('limit');
            }

            try {
                $result = $this->complaintCustomerSearchService()->listCustomers(1, $limit, $search, null);
                $collection = is_array($result->collection ?? null) ? $result->collection : [];
            } catch (\Throwable $throwable) {
                $upstream_message = $this->sanitizeComplaintCustomerLookupUpstreamErrorMessage($throwable);

                (new logs_o())->add(
                    'departments',
                    'global',
                    1,
                    $user->id,
                    'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINT_CUSTOMERS_FAILED',
                    'Failed to search complaint customers from e-conomic: ' . $upstream_message
                );

                $response->error([
                    'message' => 'Failed to fetch complaint customers from e-conomic',
                    'upstream_message' => $upstream_message,
                ], 502);
            }

            $matches = array_values(array_filter(array_map(
                static function (mixed $customer): ?array {
                    $customer_number = 0;
                    $customer_name = null;

                    if (is_object($customer)) {
                        $customer_number = (int)($customer->customerNumber ?? 0);
                        $name = trim((string)($customer->name ?? ''));
                        $customer_name = $name === '' ? null : $name;
                    } elseif (is_array($customer)) {
                        $customer_number = (int)($customer['customerNumber'] ?? 0);
                        $name = trim((string)($customer['name'] ?? ''));
                        $customer_name = $name === '' ? null : $name;
                    }

                    if ($customer_number <= 0) {
                        return null;
                    }

                    return [
                        'customer_number' => $customer_number,
                        'customer_name' => $customer_name,
                    ];
                },
                $collection
            )));

            (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINT_CUSTOMERS', 'Successfully searched complaint customers');

            $response->success($matches);
        },
            [
                'create_department_daily_report_complaints' => 'Search customers when creating department daily report customer complaints',
                'edit_department_daily_report_complaints' => 'Search customers when editing department daily report customer complaints',
            ]
        );

        $this->get('/departments/daily-reports/complaints', function () {
            global $response;
            $this->requirePermission('list_department_daily_report_complaints');

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINTS', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            $repository = $this->dailyReportComplaintsRepository();

            if (self::isParametersSet(['id'])) {
                self::requireType(
                    (int)self::getParameter('id'),
                    self::type_int()
                );
                self::requireMinValue(
                    (int)self::getParameter('id'),
                    1
                );

                $complaint = $repository->select((int)self::getParameter('id'));
                if (!$complaint->exists()) {
                    $response->error('Complaint not found', 404);
                }

                self::requireDepartmentAccess((int)$complaint->department_id->value());

                (new logs_o())->add('departments', 'global', 1, $user->id, 'GET_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'Successfully retrieved department daily report complaint');

                $response->success($repository->parseComplaint($complaint->asArray()));
            }

            (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORT_COMPLAINTS', 'Successfully listed department daily report complaints');

            $response->success(
                $repository
                    ->setSearchableFields([
                        'id',
                        'department_id',
                        'customer_number',
                        'wash_date',
                        'category',
                        'description',
                        'created_by',
                        'created_at',
                    ])
                    ->listObjectsWithPaginationIfSet(
                        fn (array $complaint): array => $repository->parseComplaint($complaint),
                        $repository->forceRestrictFilters([
                            'department_id' => $user->getGroup()->getDepartments(),
                        ])
                    )
            );
        },
            [
                'list_department_daily_report_complaints' => 'List department daily report customer complaints'
            ]
        );

        $this->put('/departments/daily-reports/complaints', function () {
            global $response;
            $this->requirePermission('edit_department_daily_report_complaints');

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'EDIT_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters(['id']);
            self::requireType(
                (int)self::getParameter('id'),
                self::type_int()
            );
            self::requireMinValue(
                (int)self::getParameter('id'),
                1
            );

            $complaint = $this->dailyReportComplaintsRepository()->select((int)self::getParameter('id'));
            if (!$complaint->exists()) {
                $response->error('Complaint not found', 404);
            }

            self::requireDepartmentAccess((int)$complaint->department_id->value());

            $updates = [];

            if (self::isParametersSet(['department_id'])) {
                self::requireType(
                    (int)self::getParameter('department_id'),
                    self::type_int()
                );
                self::requireMinValue(
                    (int)self::getParameter('department_id'),
                    1
                );

                $department = (new departments_o())->select((int)self::getParameter('department_id'));
                if (!$department->exists()) {
                    $response->error('Department not found', 400);
                }

                self::requireDepartmentAccess((int)self::getParameter('department_id'));

                $updates['department_id'] = (int)self::getParameter('department_id');
            }

            if (self::isParametersSet(['description'])) {
                self::requireType(
                    self::getParameter('description'),
                    self::type_string()
                );
                self::requireMinLength(
                    'description',
                    1
                );
                self::requireMaxLength(
                    'description',
                    4000
                );

                $description = trim((string)self::getParameter('description'));
                if ($description === '') {
                    $response->error('Description is required', 400);
                }

                $updates['description'] = $description;
            }

            if (self::isParametersSet(['wash_date'])) {
                $wash_date = $this->requireComplaintWashDateParameter('wash_date');
                if ($wash_date === null) {
                    $response->error('Wash date is required', 400);
                }

                $updates['wash_date'] = $wash_date;
            }

            if (self::isParametersSet(['category'])) {
                $category = $this->requireComplaintCategoryParameter('category');
                if ($category === null) {
                    $response->error('Category is required', 400);
                }

                $updates['category'] = $category;
            }

            if (self::isParametersSet(['customer_number'])) {
                $raw_customer_number = self::getParameter('customer_number');

                if ($raw_customer_number === null || trim((string)$raw_customer_number) === '') {
                    $updates['customer_number'] = null;
                } else {
                    self::requireType(
                        (int)$raw_customer_number,
                        self::type_int()
                    );
                    self::requireMinValue(
                        (int)$raw_customer_number,
                        1
                    );

                    $customer_number = (int)$raw_customer_number;
                    $customer = (new users_o())->getOrImportCustomerByCustomerNumber($customer_number);
                    if ($customer === false || !$customer->exists()) {
                        $response->error('Customer not found', 400);
                    }

                    $updates['customer_number'] = $customer_number;
                }
            }

            if ($updates === []) {
                $response->success(
                    $this->dailyReportComplaintsRepository()->parseComplaint($complaint->asArray())
                );
            }

            $complaint->update($updates);

            (new logs_o())->add('departments', 'global', 1, $user->id, 'EDIT_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'Successfully edited department daily report complaint');

            $response->success(
                $this->dailyReportComplaintsRepository()->parseComplaint($complaint->asArray())
            );
        },
            [
                'edit_department_daily_report_complaints' => 'Edit department daily report customer complaints'
            ]
        );

        $this->delete('/departments/daily-reports/complaints', function () {
            global $response;
            $this->requirePermission('delete_department_daily_report_complaints');

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'DELETE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters(['id']);
            self::requireType(
                (int)self::getParameter('id'),
                self::type_int()
            );
            self::requireMinValue(
                (int)self::getParameter('id'),
                1
            );

            $complaint = $this->dailyReportComplaintsRepository()->select((int)self::getParameter('id'));
            if (!$complaint->exists()) {
                $response->error('Complaint not found', 404);
            }

            self::requireDepartmentAccess((int)$complaint->department_id->value());

            $complaint->deletePermanently();

            (new logs_o())->add('departments', 'global', 1, $user->id, 'DELETE_DEPARTMENT_DAILY_REPORT_COMPLAINT', 'Successfully deleted department daily report complaint');

            $response->success([
                'message' => 'Complaint deleted successfully',
            ]);
        },
            [
                'delete_department_daily_report_complaints' => 'Delete department daily report customer complaints'
            ]
        );

        $this->put('/departments/daily-reports', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('update_department_daily_reports');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Check if the required fields are set
                self::requireParameters(
                    [
                        'id',
                    ]
                );
                // Validate the id
                self::requireType(
                    (int)self::getParameter('id'),
                    self::type_int()
                );
                // Require the department_id to be above 0
                self::requireMinValue(
                    (int)self::getParameter('id'),
                    1
                );
                // Check which fields are set
                if (self::isParametersSet(['water_usage'])) {
                    // Validate the water_usage
                    self::requireType(
                        (int)self::getParameter('water_usage'),
                        self::type_int()
                    );
                    // Require the water_usage to be at least 0
                    self::requireMinValue(
                        (int)self::getParameter('water_usage'),
                        0
                    );
                }
                if (self::isParametersSet(['notes'])) {
                    // Validate the notes
                    self::requireType(
                        (string)self::getParameter('notes'),
                        self::type_string()
                    );
                    // Require the notes to be at least 0 characters long
                    self::requireMinLength(
                        (string)self::getParameter('notes'),
                        0
                    );
                    // Require the notes to be at most 4000 characters long
                    self::requireMaxLength(
                        (string)self::getParameter('notes'),
                        4000
                    );
                }
                // Get the department from the daily report
                $department_daily_report = new department_daily_reports_o();
                $department_daily_report->select((int)self::getParameter('id'));
                if (!$department_daily_report->exists()) {
                    $response->error('Department daily report not found', 404);
                }
                // Get the department
                $department = (new departments_o())->select((int)$department_daily_report->department_id->value());
                if (!$department->exists()) {
                    $response->error('Department not found', 404);
                }
                // Require the user to have access to the department
                self::requireDepartmentAccess((int)$department->id);
                // Check the parameters that are set
                if (self::isParametersSet(['water_usage'])) {
                    $department_daily_report->water_usage->set((int)self::getParameter('water_usage'));
                }
                if (self::isParametersSet(['notes'])) {
                    $department_daily_report->notes->set((string)self::getParameter('notes'));
                }
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, $user->id, 'UPDATE_DEPARTMENT_DAILY_REPORTS', 'Successfully updated department daily report');
                // Return the department daily report
                $response->success($department_daily_report->asArray());
            } else {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, 0, 'UPDATE_DEPARTMENT_DAILY_REPORTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'update_department_daily_reports' => 'Update a department daily report',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->get('/superuser/departments/{id}/overview', function () {
            global $response;
            $this->requirePermission('superuser_fetch_department');

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            $department_id_param = (string)($this->fromRoute('id') ?? '');
            if (!ctype_digit($department_id_param) || (int)$department_id_param <= 0) {
                $response->error('Parameter id must be a positive integer', 400);
            }

            self::requireParameters([
                'date',
            ]);

            self::validateDateLocally();
            $date_to = $this->getDate_to();
            $department_id = (int)$department_id_param;
            $department = (new departments_o())->select($department_id);

            if (!$department->exists()) {
                $response->error('Department not found', 404);
            }

            (new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'Successfully loaded superuser department overview');

            $response->success([
                'department' => $department->asArray(['slack_webhook' => false]),
                'overview' => $this->buildDailyReportOverview(
                    [$department_id],
                    (string)self::getParameter('date'),
                    $date_to,
                    self::hasPermission(self::SET_PRODUCT_TARGET_PERMISSION)
                ),
            ]);
        },
            [
                'superuser_fetch_department' => 'Get the superuser department overview'
            ]
        );

        $this->get('/departments/daily-reports/overview', function () {
            global $response;
            $this->requirePermission('list_department_daily_reports');
            $this->requirePermission('list_bookings');

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_OVERVIEW', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters([
                'date',
                'department_ids',
            ]);

            self::validateDateLocally();
            $date_to = $this->getDate_to();
            $department_ids = $this->normalizeDepartmentIdsParameter(self::getParameter('department_ids'));

            if ($department_ids === []) {
                $response->error('At least one department_id must be provided', 400);
            }

            foreach ($department_ids as $department_id) {
                self::requireDepartmentAccess($department_id);
            }

            (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORTS_OVERVIEW', 'Successfully listed departments daily reports overview');

            $response->success(
                $this->buildDailyReportOverview(
                    $department_ids,
                    (string)self::getParameter('date'),
                    $date_to,
                    self::hasPermission(self::SET_PRODUCT_TARGET_PERMISSION)
                )
            );
        },
            [
                'list_department_daily_reports' => 'List the department daily reports overview for departments the user can access',
                'list_bookings' => 'List order bookings for the overview bookings tile',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->put('/departments/daily-reports/product-targets', function () {
            global $response;
            $this->requirePermission(self::SET_PRODUCT_TARGET_PERMISSION);

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'SET_DEPARTMENT_DAILY_REPORT_PRODUCT_TARGET', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters([
                'department_id',
                'product_id',
                'target_percentage',
            ]);

            $department_id = (int)self::getParameter('department_id');
            if ($department_id <= 0) {
                $response->error('Parameter department_id must be a positive integer', 400);
            }

            $department = (new departments_o())->select($department_id);
            if (!$department->exists()) {
                $response->error('Department not found', 404);
            }

            self::requireDepartmentAccess($department_id);

            $product_id = (int)self::getParameter('product_id');
            if (!$this->isDailyReportProductId($product_id)) {
                $response->error('Invalid daily report product_id', 400);
            }

            $parsed_target = $this->parseDailyReportProductTargetPercentage(self::getParameter('target_percentage'));
            if (!$parsed_target['valid']) {
                $response->error($parsed_target['message'], 400);
            }

            $target_percentage = $parsed_target['value'];
            $department_variables = (new department_variables_o())->selectDepartment($department_id);
            $target_key = $this->dailyReportProductTargetVariableKey($product_id);

            if ($target_percentage === null) {
                $this->clearDailyReportProductTarget($department_variables, $target_key);
            } else {
                $department_variables->set($target_key, number_format($target_percentage, 1, '.', ''));
            }

            (new logs_o())->add('departments', 'global', 1, $user->id, 'SET_DEPARTMENT_DAILY_REPORT_PRODUCT_TARGET', 'Successfully updated department daily report product target');

            $response->success([
                'department_id' => $department_id,
                'product_id' => $product_id,
                'target_percentage' => $target_percentage,
            ]);
        },
            [
                self::SET_PRODUCT_TARGET_PERMISSION => 'Set department daily report product target percentages',
                'department_access_:department_id' => 'Access the department'
            ]
        );

        $this->get('/departments/daily-reports/product-count', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('list_department_daily_reports');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Check if the required fields are set
                self::requireParameters(
                    [
                        'date',
                        'department_id',
                        'product_id',
                        // 'date-to' (Optional)
                    ]
                );
                /** date parameter */
                // Validate the date
                self::requireType(
                    (string)self::getParameter('date'),
                    self::type_string()
                );
                // Require the date to be at least 0 characters long
                self::requireMinLength(
                    'date',
                    0
                );
                // Require the date to be at most 10 characters long
                self::requireMaxLength(
                    'date',
                    10
                );
                // Validate the date format (YYYY-MM-DD)
                self::requireDateFormat(
                    (string)self::getParameter('date'),
                    'Y-m-d'
                );
                /** date-to parameter (Optional, defaults to date parameter if not set) */
                $date_to = $this->getDate_to();
                // Validate the department_id
                self::requireType(
                    (int)self::getParameter('department_id'),
                    self::type_int()
                );
                // Require the department_id to be above 0
                self::requireMinValue(
                    (int)self::getParameter('department_id'),
                    1
                );
                // Validate the product_id
                self::requireType(
                    (int)self::getParameter('product_id'),
                    self::type_int()
                );
                // Require the product_id to be above 0
                self::requireMinValue(
                    (int)self::getParameter('product_id'),
                    1
                );
                // Determine if the user has access to the department
                self::requireDepartmentAccess((int)self::getParameter('department_id'));

                $outside_hours_service = $this->outsideHoursStatisticsService();
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORTS_PRODUCTS', 'Successfully listed departments daily reports products');
                // Return the list of products sold on the selected date
                $response->success(
                    [
                        'quantity' =>
                            (new department_daily_reports_o())
                                ->getProductsSoldOnDate(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    (int)self::getParameter('product_id'),
                                    $date_to
                                ),
                        'date' => (string)self::getParameter('date'),
                        'date_to' => $date_to,
                        'department_id' => (int)self::getParameter('department_id'),
                        'product_id' => (int)self::getParameter('product_id'),
                        'out_of' => (int)(new departments_o())->getTotalMaxAddonsInDepartment(
                            [(int)self::getParameter('product_id')],
                            (string)self::getParameter('date'),
                            $date_to,
                            (int)self::getParameter('department_id')
                        ),
                        // This is the amount of products sold on the selected date, that this product could have been sold as a part of. (Addons)
                        //'out_of' => (int)(new department_daily_reports_o())->getProductsSoldWithAddonApplicable(
                        //    (string)self::getParameter('date'),
                        //    (int)self::getParameter('department_id'),
                        //    (int)self::getParameter('product_id'),
                        //    $date_to
                        //)
                    ]
                );
            } else {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_PRODUCTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_department_daily_reports' => 'List the department daily reports, provided the user has access to the department',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->get('/departments/daily-reports/transaction-count', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('list_department_daily_reports');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Check if the required fields are set
                self::requireParameters(
                    [
                        'date',
                        'department_id'
                    ]
                );
                // Validate the date
                self::validateDateLocally();
                // Get the date to parameter, or return the date parameter if not set
                $date_to = $this->getDate_to();
                // Validate the department_id
                self::requireType(
                    (int)self::getParameter('department_id'),
                    self::type_int()
                );
                // Require the department_id to be above 0
                self::requireMinValue(
                    (int)self::getParameter('department_id'),
                    1
                );
                // Determine if the user has access to the department
                self::requireDepartmentAccess((int)self::getParameter('department_id'));

                $outside_hours_service = $this->outsideHoursStatisticsService();
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORTS_PRODUCTS', 'Successfully listed departments daily reports products');
                // Return the list of products sold on the selected date
                $response->success(
                    [
                        'quantity' =>
                            (new department_daily_reports_o())
                                ->getTransactionsOnDateCount(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    $date_to
                                ),
                        'products' =>
                            (new department_daily_reports_o())
                                ->getTransactionProductsOnDateCount(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    $date_to
                                ),
                        'earnings' =>
                            (new department_daily_reports_o())
                                ->getTransactionsOnDateEarnings(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    $date_to
                                ),
                        'washes' =>
                            (new department_daily_reports_o())
                                ->getTransactionsOnDateWashesCount(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    $date_to
                                ),
                        'water_usage' =>
                            (new department_daily_reports_o())
                                ->getTransactionsOnDateWaterUsage(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    $date_to
                                ),
                        'date' => (string)self::getParameter('date'),
                        'department_id' => (int)self::getParameter('department_id'),
                        'date_to' => $date_to,
                        'outside_hours' => $outside_hours_service->getSummary(
                            (string)self::getParameter('date'),
                            [(int)self::getParameter('department_id')],
                            $date_to
                        ),
                    ]
                );
            } else {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_PRODUCTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_department_daily_reports' => 'List the department daily reports, provided the user has access to the department',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->get('/departments/daily-reports/outside-hours-trend', function () {
            global $response;
            $this->requirePermission('list_department_daily_reports');

            $user = (new authentication())->get_user();
            if (!$user) {
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_OUTSIDE_HOURS_TREND', 'No user found, or invalid session');
                $response->error('Invalid session', 400);
            }

            self::requireParameters([
                'date',
                'date_to',
                'department_ids',
            ]);

            self::validateDateLocally();
            self::requireDateFormat(
                (string)self::getParameter('date_to'),
                'Y-m-d'
            );

            $department_ids = $this->normalizeDepartmentIdsParameter(self::getParameter('department_ids'));
            if ($department_ids === []) {
                $response->error('At least one department_id must be provided', 400);
            }

            foreach ($department_ids as $department_id) {
                self::requireDepartmentAccess($department_id);
            }

            (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORTS_OUTSIDE_HOURS_TREND', 'Successfully listed outside hours trend');

            $response->success(
                $this->outsideHoursStatisticsService()->getTrend(
                    (string)self::getParameter('date'),
                    (string)self::getParameter('date_to'),
                    $department_ids
                )
            );
        },
            [
                'list_department_daily_reports' => 'List outside-hours trend for departments the user can access',
                'department_access_:id' => 'Access the department'
            ]
        );

        $this->get('/departments/daily-reports/bookings-count', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('list_bookings');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                // Check if the required fields are set
                self::requireParameters(
                    [
                        'date',
                        'department_id'
                    ]
                );
                // Validate the date
                self::requireType(
                    (string)self::getParameter('date'),
                    self::type_string()
                );
                // Require the date to be at least 0 characters long
                self::requireMinLength(
                    'date',
                    0
                );
                // Require the date to be at most 10 characters long
                self::requireMaxLength(
                    'date',
                    10
                );
                // Validate the date format (YYYY-MM-DD)
                self::requireDateFormat(
                    (string)self::getParameter('date'),
                    'Y-m-d'
                );
                // Validate the department_id
                self::requireType(
                    (int)self::getParameter('department_id'),
                    self::type_int()
                );
                // Require the department_id to be above 0
                self::requireMinValue(
                    (int)self::getParameter('department_id'),
                    1
                );
                // Determine if the user has access to the department
                self::requireDepartmentAccess((int)self::getParameter('department_id'));

                // Log the incident
                (new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_DAILY_REPORTS_PRODUCTS', 'Successfully listed departments daily reports products');
                // Return the list of products sold on the selected date
                $response->success(
                    [
                        'pending' =>
                            (new department_daily_reports_o())
                                ->getBookingsOnDateCount(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    'pending'
                                ),
                        'completed' =>
                            (new department_daily_reports_o())
                                ->getBookingsOnDateCount(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    'completed'
                                ),
                        'cancelled' =>
                            (new department_daily_reports_o())
                                ->getBookingsOnDateCount(
                                    (string)self::getParameter('date'),
                                    (int)self::getParameter('department_id'),
                                    'cancelled'
                                ),

                    ]
                );
            } else {
                // Log the incident
                (new logs_o())->add('departments', 'global', 1, 0, 'LIST_DEPARTMENT_DAILY_REPORTS_PRODUCTS', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'list_department_daily_reports' => 'List the department daily reports, provided the user has access to the department',
                'department_access_:id' => 'Access the department'
            ]
        );
    }

    private function requireComplaintWashDateParameter(string $parameter_name): ?string
    {
        self::requireType(
            self::getParameter($parameter_name),
            self::type_string()
        );
        self::requireMinLength(
            $parameter_name,
            1
        );
        self::requireMaxLength(
            $parameter_name,
            10
        );

        $wash_date = trim((string)self::getParameter($parameter_name));
        if ($wash_date === '') {
            return null;
        }

        self::requireDateFormat(
            $wash_date,
            self::FORMAT_DATE()
        );

        return $wash_date;
    }

    private function requireComplaintCategoryParameter(string $parameter_name): ?string
    {
        self::requireType(
            self::getParameter($parameter_name),
            self::type_string()
        );
        self::requireMinLength(
            $parameter_name,
            1
        );
        self::requireMaxLength(
            $parameter_name,
            64
        );

        $category = trim((string)self::getParameter($parameter_name));
        if ($category === '') {
            return null;
        }

        if (!department_daily_report_complaints_o::isValidCategory($category)) {
            global $response;
            $response->error('Invalid complaint category', 400);
        }

        return $category;
    }

    /**
     * Get the date_to parameter, or return the date parameter if not set
     * @return string
     * @throws Exception if the date is set, but invalid
     * @see validateDateLocally
     * @see requireType
     * @see requireMinLength
     * @see requireMaxLength
     * @see requireDateFormat
     */
    function getDate_to(): string
    {
        $date_to = (string)self::getParameter('date');
        if (self::isParametersSet(['date_to'])) {
            // Validate the date
            self::validateDateLocally('date_to');
            $date_to = (string)self::getParameter('date_to');
        }
        return $date_to;
    }

    /**
     * Validate a date locally without using requireType and other functions that would throw an error
     * This is to avoid repeating the same code multiple times
     * @param string $parameterName The name of the parameter to validate
     * @throws Exception if the date is invalid
     * @see requireType
     * @see requireMinLength
     * @see requireMaxLength
     * @see requireDateFormat
     * @see getDate_to
     */
    private function validateDateLocally(string $parameterName = 'date'): void
    {
        // Require the date to be at least 0 characters long
        self::requireMinLength(
            $parameterName,
            0
        );
        // Require the date to be at most 10 characters long
        self::requireMaxLength(
            $parameterName,
            10
        );
        // Validate the date
        self::requireDateFormat(
            (string)self::getParameter($parameterName),
            self::FORMAT_DATE()
        );
    }

    /**
     * @param array<int> $department_ids
     * @return array{
     *   department_ids:array<int>,
     *   date:string,
     *   date_to:string,
     *   metrics:array<string,array<string,mixed>>,
     *   products:array<int,array<string,mixed>>
     * }
     * @throws Exception
     */
    private function buildDailyReportOverview(array $department_ids, string $date, string $date_to, bool $include_product_targets = false): array
    {
        $repository = $this->dailyReportRepository();
        $transaction_summary = $repository->getTransactionSummaryForDepartments($date, $department_ids, $date_to);
        $booking_summary = $repository->getBookingSummaryForDepartments($date, $department_ids, $date_to);

        $product_definitions = $this->getDailyReportProductDefinitions();
        $product_summary_lookup = $repository->getProductOverviewForDepartments(
            $date,
            $department_ids,
            array_column($product_definitions, 'product_id'),
            $date_to
        );

        $complaints_metric = $this->buildComplaintsMetric($department_ids, $date, $date_to);

        $night_wash_metric = $this->buildNightWashMetric($department_ids, $date, $date_to);

        $overtime_metric = $this->buildOvertimeMetric($department_ids, $date, $date_to);

        $product_target_lookup = [];
        if ($include_product_targets && count($department_ids) === 1) {
            $product_target_lookup = $this->getDailyReportProductTargetsForDepartment((int)$department_ids[0], $product_definitions);
        }

        return $this->assembleDailyReportOverview(
            $department_ids,
            $date,
            $date_to,
            $transaction_summary,
            $booking_summary,
            $product_definitions,
            $product_summary_lookup,
            $complaints_metric,
            $night_wash_metric,
            $overtime_metric,
            $product_target_lookup
        );
    }

    /**
     * @param array<int> $department_ids
     * @param array{quantity:int,products:int,earnings:int,washes:int,water_usage:int} $transaction_summary
     * @param array{completed:int,total:int} $booking_summary
     * @param array<int,array{product_id:int,slug:string,title:string}> $product_definitions
     * @param array<int,array{product_id:int,quantity:int,out_of:int}> $product_summary_lookup
     * @param array<string,mixed> $complaints_metric
     * @param array<string,mixed> $night_wash_metric
     * @param array<string,mixed> $overtime_metric
     * @param array<int,float> $product_target_lookup
     * @return array{
     *   department_ids:array<int>,
     *   date:string,
     *   date_to:string,
     *   metrics:array<string,array<string,mixed>>,
     *   products:array<int,array<string,mixed>>
     * }
     */
    private function assembleDailyReportOverview(
        array $department_ids,
        string $date,
        string $date_to,
        array $transaction_summary,
        array $booking_summary,
        array $product_definitions,
        array $product_summary_lookup,
        array $complaints_metric,
        array $night_wash_metric,
        array $overtime_metric,
        array $product_target_lookup = []
    ): array {
        $products = [];
        foreach ($product_definitions as $definition) {
            $product_id = (int)$definition['product_id'];
            $product_summary = $product_summary_lookup[$product_id] ?? [
                'product_id' => $product_id,
                'quantity' => 0,
                'out_of' => 0,
            ];

            $products[] = [
                'product_id' => $product_id,
                'slug' => (string)$definition['slug'],
                'title' => (string)$definition['title'],
                'state' => 'ready',
                'value' => (int)($product_summary['quantity'] ?? 0),
                'out_of' => (int)($product_summary['out_of'] ?? 0),
                'target_percentage' => array_key_exists($product_id, $product_target_lookup)
                    ? (float)$product_target_lookup[$product_id]
                    : null,
                'target_department_id' => array_key_exists($product_id, $product_target_lookup)
                    ? (int)$department_ids[0]
                    : null,
            ];
        }

        return [
            'department_ids' => array_values(array_map('intval', $department_ids)),
            'date' => $date,
            'date_to' => $date_to,
            'metrics' => [
                'bookings' => $this->metricPayload(
                    (int)($booking_summary['completed'] ?? 0),
                    (int)($booking_summary['total'] ?? 0)
                ),
                'complaints' => $complaints_metric,
                'night_washes' => $night_wash_metric,
                'revenue' => $this->metricPayload((int)($transaction_summary['earnings'] ?? 0)),
                'washes' => $this->metricPayload((int)($transaction_summary['washes'] ?? 0)),
                'products_sold' => $this->metricPayload((int)($transaction_summary['products'] ?? 0)),
                'transactions' => $this->metricPayload((int)($transaction_summary['quantity'] ?? 0)),
                'water_usage' => $this->metricPayload((int)($transaction_summary['water_usage'] ?? 0)),
                'overtime' => $overtime_metric,
            ],
            'products' => $products,
        ];
    }

    /**
     * @param mixed $department_ids
     * @return array<int>
     */
    private function normalizeDepartmentIdsParameter(mixed $department_ids): array
    {
        $normalized = [];
        $appendDepartmentIds = function (mixed $value) use (&$appendDepartmentIds, &$normalized): void {
            if (is_array($value)) {
                foreach ($value as $nested_value) {
                    $appendDepartmentIds($nested_value);
                }
                return;
            }

            if (is_string($value) && str_contains($value, ',')) {
                foreach (explode(',', $value) as $segment) {
                    $appendDepartmentIds(trim($segment));
                }
                return;
            }

            $id = (int)$value;
            if ($id > 0) {
                $normalized[$id] = $id;
            }
        };

        $appendDepartmentIds($department_ids);

        return array_values($normalized);
    }

    private function isDailyReportProductId(int $product_id): bool
    {
        return in_array(
            $product_id,
            array_map(static fn(array $definition): int => (int)$definition['product_id'], $this->getDailyReportProductDefinitions()),
            true
        );
    }

    /**
     * @return array{valid: bool, value: ?float, message: string}
     */
    private function parseDailyReportProductTargetPercentage(mixed $target_percentage): array
    {
        if ($target_percentage === null) {
            return ['valid' => true, 'value' => null, 'message' => ''];
        }

        if (is_string($target_percentage)) {
            $target_percentage = trim($target_percentage);
            if ($target_percentage === '') {
                return ['valid' => true, 'value' => null, 'message' => ''];
            }
        }

        if (!is_int($target_percentage) && !is_float($target_percentage) && !(is_string($target_percentage) && is_numeric($target_percentage))) {
            return ['valid' => false, 'value' => null, 'message' => 'Parameter target_percentage must be numeric, null, or empty'];
        }

        $target_percentage = round((float)$target_percentage, 1);
        if ($target_percentage < 0.0 || $target_percentage > 100.0) {
            return ['valid' => false, 'value' => null, 'message' => 'Parameter target_percentage must be between 0 and 100'];
        }

        return ['valid' => true, 'value' => $target_percentage, 'message' => ''];
    }

    /**
     * @param array<int,array{product_id:int,slug:string,title:string}> $product_definitions
     * @return array<int,float>
     * @throws Exception
     */
    protected function getDailyReportProductTargetsForDepartment(int $department_id, array $product_definitions): array
    {
        $department_variables = (new department_variables_o())->selectDepartment($department_id);
        $targets = [];

        foreach ($product_definitions as $definition) {
            $product_id = (int)$definition['product_id'];
            $stored_target = $department_variables->getVariable($this->dailyReportProductTargetVariableKey($product_id));

            if ($stored_target === null || $stored_target === '' || !is_numeric($stored_target)) {
                continue;
            }

            $targets[$product_id] = round((float)$stored_target, 1);
        }

        return $targets;
    }

    protected function clearDailyReportProductTarget(department_variables_o $department_variables, string $target_key): void
    {
        $existing_targets = $department_variables->getFieldsWhere([
            'department_id' => $department_variables->department_id,
            'variable' => $target_key,
        ], ['id']);

        if (!$existing_targets) {
            return;
        }

        department_variables_o::delete_object('department_variables', (int)$existing_targets[0]['id']);
        $department_variables->objectChanged();
    }

    private function dailyReportProductTargetVariableKey(int $product_id): string
    {
        return 'daily_report_product_target_percentage_' . $product_id;
    }

    /**
     * @return array<int,array{product_id:int,slug:string,title:string}>
     */
    private function getDailyReportProductDefinitions(): array
    {
        return [
            ['product_id' => 24, 'slug' => 'spot-free-lastbil', 'title' => 'Spot Free (Lastbil)'],
            ['product_id' => 25, 'slug' => 'faelg-flex', 'title' => 'Fælg flex pr. enhed'],
            ['product_id' => 27, 'slug' => 'extraordinary-10-min', 'title' => 'Ekstraordinær pr. 10 min inkl. kemi'],
            ['product_id' => 26, 'slug' => 'hoejglans', 'title' => 'Højglans - Voksforsegling pr. enhed'],
            ['product_id' => 21, 'slug' => 'undervognsskyl', 'title' => 'Undervognsskyl pr. enhed'],
            ['product_id' => 22, 'slug' => 'double-duty-kemi', 'title' => 'Tillæg for Specialsæbe - DD'],
        ];
    }

    /**
     * @param array<int> $department_ids
     * @return array<string,mixed>
     * @throws Exception
     */
    private function buildNightWashMetric(array $department_ids, string $date, string $date_to): array
    {
        return $this->outsideHoursStatisticsService()->toOverviewMetric(
            $this->outsideHoursStatisticsService()->getSummary($date, $department_ids, $date_to)
        );
    }

    /**
     * @param array<int> $department_ids
     * @return array<string,mixed>
     */
    private function buildComplaintsMetric(array $department_ids, string $date, string $date_to): array
    {
        return $this->metricPayload(
            $this->dailyReportComplaintsRepository()->countForDepartmentsInRange($department_ids, $date, $date_to)
        );
    }

    /**
     * @param array<int> $department_ids
     * @return array<string,mixed>
     * @throws Exception
     */
    private function buildOvertimeMetric(array $department_ids, string $date, string $date_to): array
    {
        $departments = $this->fetchDepartmentsByIds($department_ids);
        if (count($departments) !== count($department_ids)) {
            return $this->metricPayload(
                null,
                null,
                'unavailable',
                'Overarbejde er ikke tilgængelig for alle valgte afdelinger.'
            );
        }

        $workfeed_department_ids_by_department = $this->resolveWorkfeedDepartmentIdsByDepartmentId($departments);
        if (count($workfeed_department_ids_by_department) !== count($department_ids)) {
            return $this->metricPayload(
                null,
                null,
                'unavailable',
                'Overarbejde kræver Workfeed-kobling for alle valgte afdelinger.'
            );
        }

        $range_start = new DateTime($date . ' 00:00:00');
        $range_end_exclusive = new DateTime($date_to . ' 00:00:00');
        $range_end_exclusive->add(new DateInterval('P1D'));

        $query_start = clone $range_start;
        $query_start->sub(new DateInterval('P1D'));

        $overtime_hours = $this->sumOvertimeHoursForShifts(
            $this->fetchWorkfeedShifts($query_start, $range_end_exclusive),
            $workfeed_department_ids_by_department,
            $range_start,
            $range_end_exclusive
        );

        return $this->metricPayload($overtime_hours);
    }

    /**
     * @param array<int|string,mixed> $metric
     * @return array<string,mixed>
     */
    private function metricPayload(mixed $value, mixed $out_of = null, string $state = 'ready', string $message = null): array
    {
        return [
            'state' => $state,
            'value' => $value,
            'out_of' => $out_of,
            'message' => $message,
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $opening_hours_by_department_id
     * @return bool
     */
    private function isOutsideOpeningHours(string $created_at, array $opening_hours): bool
    {
        $timestamp = strtotime($created_at);
        if ($timestamp === false) {
            return false;
        }

        $weekday = match ((int)date('N', $timestamp)) {
            1 => 'monday',
            2 => 'tuesday',
            3 => 'wednesday',
            4 => 'thursday',
            5 => 'friday',
            6 => 'saturday',
            7 => 'sunday',
            default => null,
        };

        if ($weekday === null) {
            return false;
        }

        $opening_start = $opening_hours[$weekday . '_start'] ?? null;
        $opening_end = $opening_hours[$weekday . '_end'] ?? null;
        if (!is_string($opening_start) || trim($opening_start) === '' || !is_string($opening_end) || trim($opening_end) === '') {
            return true;
        }

        $wash_time = date('H:i', $timestamp);
        $opening_start_time = date('H:i', strtotime($opening_start));
        $opening_end_time = date('H:i', strtotime($opening_end));

        return !($wash_time >= $opening_start_time && $wash_time <= $opening_end_time);
    }

    /**
     * @param array<int,mixed> $shifts
     * @param array<int,string> $workfeed_department_ids_by_department
     */
    private function sumOvertimeHoursForShifts(
        array $shifts,
        array $workfeed_department_ids_by_department,
        DateTime $range_start,
        DateTime $range_end_exclusive
    ): float {
        $department_lookup = [];
        foreach ($workfeed_department_ids_by_department as $workfeed_department_id) {
            $normalized = trim((string)$workfeed_department_id);
            if ($normalized !== '') {
                $department_lookup[$normalized] = true;
            }
        }

        if ($department_lookup === []) {
            return 0.0;
        }

        $total_hours = 0.0;
        foreach ($shifts as $shift) {
            $shift_department_id = $this->extractWorkfeedDepartmentId($shift);
            if ($shift_department_id === null || !isset($department_lookup[$shift_department_id])) {
                continue;
            }

            $total_hours += $this->calculateShiftOvertimeHoursInRange(
                $this->normalizeWorkfeedRecord($shift),
                $range_start,
                $range_end_exclusive
            );
        }

        return round($total_hours, 2);
    }

    private function calculateShiftOvertimeHoursInRange(array $record, DateTime $range_start, DateTime $range_end_exclusive): float
    {
        return workfeed_shift_time_resolver::calculateOvertimeHoursInRange($record, $range_start, $range_end_exclusive);
    }

    /**
     * @param array<int,mixed> $departments
     * @return array<int,string>
     */
    private function resolveWorkfeedDepartmentIdsByDepartmentId(array $departments): array
    {
        $resolved_ids = [];
        $workfeed_departments = null;

        foreach ($departments as $department) {
            $department_id = $this->departmentIdFromValue($department);
            if ($department_id < 1) {
                continue;
            }

            $configured_id = $this->getConfiguredWorkfeedDepartmentId($department);
            if ($configured_id !== null) {
                $resolved_ids[$department_id] = $configured_id;
                continue;
            }

            if ($workfeed_departments === null) {
                $workfeed_departments = $this->fetchWorkfeedDepartments();
            }
            if ($workfeed_departments === []) {
                continue;
            }

            $department_name = $this->departmentNameFromValue($department);
            if ($department_name === '') {
                continue;
            }

            $matched_id = $this->matchWorkfeedDepartmentIdByName($department_name, $workfeed_departments);
            if ($matched_id !== null) {
                $resolved_ids[$department_id] = $matched_id;
            }
        }

        return $resolved_ids;
    }

    private function getConfiguredWorkfeedDepartmentId(mixed $department): ?string
    {
        foreach ([
            'workfeed_department_id',
            'workfeedDepartmentId',
            'workfeed_departmentID',
            'workfeed_department',
        ] as $key) {
            $value = $this->departmentVariableValue($department, $key);
            if (!is_string($value)) {
                continue;
            }

            $trimmed = trim($value);
            if ($trimmed !== '') {
                return $trimmed;
            }
        }

        return null;
    }

    /**
     * @param array<int,mixed> $workfeed_departments
     */
    private function matchWorkfeedDepartmentIdByName(string $department_name, array $workfeed_departments): ?string
    {
        $needle = $this->normalizeDepartmentName($department_name);
        foreach ($workfeed_departments as $entry) {
            $record = $this->normalizeWorkfeedRecord($entry);
            $name = trim((string)($record['name'] ?? ''));
            $id = trim((string)($record['id'] ?? ''));
            if ($name === '' || $id === '') {
                continue;
            }

            if ($this->normalizeDepartmentName($name) === $needle) {
                return $id;
            }
        }

        return null;
    }

    private function normalizeDepartmentName(string $name): string
    {
        $collapsed = preg_replace('/\s+/', ' ', trim($name));

        return strtolower($collapsed ?? trim($name));
    }

    /**
     * @return array<int,mixed>
     */
    private function normalizeWorkfeedCollection(mixed $payload): array
    {
        if (is_array($payload)) {
            return $payload;
        }

        if (!is_object($payload)) {
            return [];
        }

        foreach (['data', 'items', 'results', 'shifts', 'departments'] as $key) {
            if (!isset($payload->$key)) {
                continue;
            }

            $value = $payload->$key;
            if (is_array($value)) {
                return $value;
            }
            if (is_object($value)) {
                return array_values(get_object_vars($value));
            }
        }

        return [];
    }

    /**
     * @return array<string,mixed>
     */
    private function normalizeWorkfeedRecord(mixed $record): array
    {
        if (is_array($record)) {
            return $record;
        }
        if (is_object($record)) {
            return get_object_vars($record);
        }

        return [];
    }

    private function extractWorkfeedDepartmentId(mixed $shift): ?string
    {
        $record = $this->normalizeWorkfeedRecord($shift);

        $department_id = $record['departmentID'] ?? $record['departmentId'] ?? null;
        if (($department_id === null || $department_id === '') && isset($record['department'])) {
            $department = $this->normalizeWorkfeedRecord($record['department']);
            $department_id = $department['id'] ?? $department['departmentID'] ?? $department['departmentId'] ?? null;
        }

        if ($department_id === null) {
            return null;
        }

        $normalized = trim((string)$department_id);
        return $normalized === '' ? null : $normalized;
    }

    private function parseDateTimeValue(mixed $value): ?DateTime
    {
        if (is_string($value)) {
            $normalized = trim($value);
            if ($normalized === '') {
                return null;
            }

            try {
                return new DateTime($normalized);
            } catch (Exception) {
                if (!is_numeric($normalized)) {
                    return null;
                }
                $value = (float)$normalized;
            }
        }

        if (is_int($value) || is_float($value)) {
            if (!is_finite((float)$value)) {
                return null;
            }

            $timestamp = (float)$value;
            if ($timestamp > 9999999999) {
                $timestamp /= 1000;
            }

            try {
                $date = new DateTime('@' . (string)(int)round($timestamp));
                $date->setTimezone(new DateTimeZone('UTC'));
                return $date;
            } catch (Exception) {
                return null;
            }
        }

        $record = $this->normalizeWorkfeedRecord($value);
        foreach (['seconds', '_seconds', 'epochSeconds', 'timestamp'] as $key) {
            if (!array_key_exists($key, $record)) {
                continue;
            }

            $parsed = $this->parseDateTimeValue($record[$key]);
            if ($parsed !== null) {
                return $parsed;
            }
        }

        return null;
    }

    private function getNestedRecordValue(array $record, string $path): mixed
    {
        $segments = explode('.', $path);
        $current = $record;

        foreach ($segments as $segment) {
            if (is_array($current)) {
                if (!array_key_exists($segment, $current)) {
                    return null;
                }
                $current = $current[$segment];
                continue;
            }

            if (is_object($current)) {
                if (!property_exists($current, $segment)) {
                    return null;
                }
                $current = $current->$segment;
                continue;
            }

            return null;
        }

        return $current;
    }

    /**
     * @param array<int,string> $paths
     */
    private function firstShiftDateTimeFromPaths(array $record, array $paths): ?DateTime
    {
        foreach ($paths as $path) {
            $parsed = $this->parseDateTimeValue($this->getNestedRecordValue($record, $path));
            if ($parsed !== null) {
                return $parsed;
            }
        }

        return null;
    }

    /**
     * @param array<int,string> $paths
     */
    private function lastShiftDateTimeFromPaths(array $record, array $paths): ?DateTime
    {
        $latest = null;
        foreach ($paths as $path) {
            $parsed = $this->parseDateTimeValue($this->getNestedRecordValue($record, $path));
            if ($parsed === null) {
                continue;
            }

            if ($latest === null || $parsed->getTimestamp() > $latest->getTimestamp()) {
                $latest = $parsed;
            }
        }

        return $latest;
    }

    private function hasShiftApproval(array $record): bool
    {
        if (!array_key_exists('approval', $record)) {
            return false;
        }

        $approval = $record['approval'];
        if ($approval === null) {
            return false;
        }
        if (is_array($approval)) {
            return $approval !== [];
        }
        if (is_object($approval)) {
            return get_object_vars($approval) !== [];
        }

        return true;
    }

    private function resolveEffectiveShiftEnd(array $record, DateTime $shift_start, DateTime $shift_end): DateTime
    {
        if ($this->hasShiftApproval($record)) {
            return $shift_end;
        }

        $update_time = $this->parseDateTimeValue($record['updateTime'] ?? null);
        if ($update_time === null) {
            return $shift_end;
        }

        $shift_start_ts = $shift_start->getTimestamp();
        $shift_end_ts = $shift_end->getTimestamp();
        $update_ts = $update_time->getTimestamp();

        if ($update_ts <= $shift_end_ts) {
            return $shift_end;
        }

        $max_unapproved_extension_seconds = 6 * 3600;
        if (($update_ts - $shift_end_ts) > $max_unapproved_extension_seconds) {
            return $shift_end;
        }
        if (($update_ts - $shift_start_ts) > 24 * 3600) {
            return $shift_end;
        }

        return $update_time;
    }

    /**
     * @param array<int> $department_ids
     * @return array<int,array<string,mixed>>
     */
    protected function fetchOpeningHoursByDepartmentId(array $department_ids): array
    {
        global $db;

        $normalized_department_ids = array_values(array_unique(array_map('intval', $department_ids)));
        $normalized_department_ids = array_values(array_filter($normalized_department_ids, static fn(int $id): bool => $id > 0));
        if ($normalized_department_ids === []) {
            return [];
        }

        $department_ids_sql = implode(',', $normalized_department_ids);
        $sql = "SELECT * FROM department_time_bookings_opening_hours WHERE department IN ($department_ids_sql)";
        $result = $db->query($sql);
        if (!is_object($result) || $result->num_rows === 0) {
            return [];
        }

        $rows = [];
        while ($row = $result->fetch_assoc()) {
            $rows[(int)($row['department'] ?? 0)] = $row;
        }

        return $rows;
    }

    /**
     * @param array<int> $department_ids
     * @return array<int,mixed>
     * @throws Exception
     */
    protected function fetchDepartmentsByIds(array $department_ids): array
    {
        $departments = [];
        foreach ($department_ids as $department_id) {
            $department = (new departments_o())->select((int)$department_id);
            if (!$department->exists()) {
                continue;
            }
            $departments[] = $department;
        }

        return $departments;
    }

    /**
     * @return array<int,mixed>
     */
    protected function fetchWorkfeedDepartments(): array
    {
        try {
            return $this->normalizeWorkfeedCollection((new workfeed())->listDepartments());
        } catch (Exception) {
            return [];
        }
    }

    /**
     * @return array<int,mixed>
     */
    protected function fetchWorkfeedShifts(DateTime $query_start, DateTime $range_end_exclusive): array
    {
        try {
            return $this->normalizeWorkfeedCollection((new workfeed())->listShifts([
                'startFrom' => $query_start->format(DateTime::ATOM),
                'startTo' => $range_end_exclusive->format(DateTime::ATOM),
            ]));
        } catch (Exception) {
            return [];
        }
    }

    protected function dailyReportRepository(): object
    {
        return new department_daily_reports_o();
    }

    protected function dailyReportComplaintsRepository(): object
    {
        return new department_daily_report_complaints_o();
    }

    protected function complaintCustomerSearchService(): economicCustomers
    {
        return new economicCustomers();
    }

    protected function outsideHoursStatisticsService(): department_outside_hours_statistics_service
    {
        return new department_outside_hours_statistics_service();
    }

    private function sanitizeComplaintCustomerLookupUpstreamErrorMessage(\Throwable $throwable): string
    {
        $message = trim($throwable->getMessage());
        if ($message === '') {
            return 'Unexpected e-conomic integration error.';
        }

        $message = preg_replace('/\s+/', ' ', $message);
        if (!is_string($message)) {
            return 'Unexpected e-conomic integration error.';
        }

        return substr($message, 0, 500);
    }

    private function departmentIdFromValue(mixed $department): int
    {
        if (is_array($department)) {
            return (int)($department['id'] ?? 0);
        }
        if (is_object($department)) {
            return (int)($department->id ?? 0);
        }

        return 0;
    }

    private function departmentNameFromValue(mixed $department): string
    {
        if (is_array($department)) {
            return trim((string)($department['name'] ?? ''));
        }
        if (is_object($department) && isset($department->name)) {
            $value = $department->name;
            if (is_object($value) && method_exists($value, 'value')) {
                return trim((string)$value->value());
            }

            return trim((string)$value);
        }

        return '';
    }

    private function departmentVariableValue(mixed $department, string $key): mixed
    {
        if (is_array($department)) {
            return $department[$key] ?? null;
        }

        if (is_object($department) && isset($department->variables) && is_object($department->variables) && method_exists($department->variables, 'getVariable')) {
            return $department->variables->getVariable($key);
        }

        return null;
    }

}
