<?php

namespace goals\classes;

use classes\db;
use goals\helpers\goals_criteria_type;
use goals\helpers\goals_criteria_progress_alert_frequency;
use goals\helpers\goals_criteria_progress_alert_destination;
use goals\helpers\goals_criteria_progress_alert_progress_type;
use goals\helpers\goals_criteria_progress_alert_style;
use goals\helpers\goals_criteria_progress_alert_weekday;
use goals\interfaces\goals_criteria_i;
use goals\traits\goals_result_parser_t;
use goals\traits\goals_target_t;
use goals\traits\goals_timeframe_t;
use goals\traits\goals_label_t;
use objects\order_items_o;

class goals_criteria implements goals_criteria_i
{
    use goals_timeframe_t,
        goals_target_t,
        goals_result_parser_t,
        goals_label_t;
    /**
     * The users criteria
     * @var goals_criteria_users $users
     */
    public goals_criteria_users $users;
    /**
     * The departments criteria
     * @var goals_criteria_departments $departments
     */
    public goals_criteria_departments $departments;
    /**
     * Criteria type (products sold, etc.)
     * @var goals_criteria_type $type
     */
    public goals_criteria_type $type = goals_criteria_type::NONE;
    /**
     * Products criteria
     * @var goals_criteria_products|null $products
     */
    public ?goals_criteria_products $products;
    /**
     * Progress alert frequency for the goal
     * @var goals_criteria_progress_alert_frequency $progress_alert_frequency
     */
    public goals_criteria_progress_alert_frequency $progress_alert_frequency;
    /**
     * Progress alert destination for the goal
     * @var goals_criteria_progress_alert_destination $progress_alert_destination
     */
    public goals_criteria_progress_alert_destination $progress_alert_destination;
    /**
     * Progress alert progress type for the goal
     * @var goals_criteria_progress_alert_progress_type $progress_alert_progress_type
     */
    public goals_criteria_progress_alert_progress_type $progress_alert_progress_type;
    /**
     * Progress alert style for the goal
     * @var goals_criteria_progress_alert_style $progress_alert_style
     */
    public goals_criteria_progress_alert_style $progress_alert_style;
    /**
     * Progress alert format (free-form string for now; rendering is handled elsewhere)
     * @var string|null $progress_alert_format
     */
    public ?string $progress_alert_format = null;
    /**
     * Weekdays on which progress alerts should be sent
     * @var goals_criteria_progress_alert_weekday[] $progress_alert_weekdays
     */
    public array $progress_alert_weekdays = [];
    /**
     * Time of day (with timezone) when progress alerts should be sent, formatted as HH:MM with timezone (e.g., 14:30Z or 14:30+02:00)
     * @var string|null $progress_alert_time_of_day
     */
    public ?string $progress_alert_time_of_day = null;
    /**
     * Constructor
     */
    public function __construct()
    {
        $this->users = new goals_criteria_users();
        $this->departments = new goals_criteria_departments();
        $this->products = new goals_criteria_products();
        $this->start = new \DateTime();
        $this->end = new \DateTime();
        $this->progress_alert_frequency = goals_criteria_progress_alert_frequency::NONE;
        $this->progress_alert_destination = goals_criteria_progress_alert_destination::NONE;
        $this->progress_alert_progress_type = goals_criteria_progress_alert_progress_type::NONE;
        $this->progress_alert_style = goals_criteria_progress_alert_style::NONE;

    }

    /**
     * @throws \Exception If the JSON is invalid
     */
    public static function fromJson(string $json): goals_criteria
    {
        $data = json_decode($json, true);

        if (!is_array($data) || json_last_error() !== JSON_ERROR_NONE) {
            throw new \InvalidArgumentException('Invalid JSON for goals criteria');
        }

        $criteria = new goals_criteria();

        // Sanitize type (ignore unknowns)
        if (isset($data['type'])) {
            $type = goals_criteria_type::tryFrom((string)$data['type']);
            if ($type !== null) {
                $criteria->type = $type;
            }
        }

        // Sanitize target: numeric and non-negative
        if (isset($data['target'])) {
            if (is_numeric($data['target'])) {
                $target = (float)$data['target'];
                $criteria->target = max(0, $target);
            }
        }

        // Sanitize label: trim, strip tags, collapse whitespace, max length 255
        if (isset($data['label']) && is_string($data['label'])) {
            $label = trim($data['label']);
            $label = strip_tags($label);
            $label = preg_replace('/\s+/', ' ', $label);
            if ($label !== null) {
                $label = mb_substr($label, 0, 255);
            }
            $criteria->label = ($label === '') ? null : $label;
        }

        // Parse progress alert frequency (ignore unknowns)
        if (isset($data['progress_alert_frequency']) && is_string($data['progress_alert_frequency'])) {
            $freq = goals_criteria_progress_alert_frequency::tryFrom($data['progress_alert_frequency']);
            if ($freq !== null) {
                $criteria->progress_alert_frequency = $freq;
            }
        } elseif (isset($data['progressAlertFrequency']) && is_string($data['progressAlertFrequency'])) { // allow camelCase
            $freq = goals_criteria_progress_alert_frequency::tryFrom($data['progressAlertFrequency']);
            if ($freq !== null) {
                $criteria->progress_alert_frequency = $freq;
            }
        }

        // Parse progress alert destination (ignore unknowns)
        if (isset($data['progress_alert_destination']) && is_string($data['progress_alert_destination'])) {
            $dest = goals_criteria_progress_alert_destination::tryFrom($data['progress_alert_destination']);
            if ($dest !== null) {
                $criteria->progress_alert_destination = $dest;
            }
        } elseif (isset($data['progressAlertDestination']) && is_string($data['progressAlertDestination'])) {
            $dest = goals_criteria_progress_alert_destination::tryFrom($data['progressAlertDestination']);
            if ($dest !== null) {
                $criteria->progress_alert_destination = $dest;
            }
        }

        // Parse progress alert progress type (ignore unknowns)
        if (isset($data['progress_alert_progress_type']) && is_string($data['progress_alert_progress_type'])) {
            $ptype = goals_criteria_progress_alert_progress_type::tryFrom($data['progress_alert_progress_type']);
            if ($ptype !== null) {
                $criteria->progress_alert_progress_type = $ptype;
            }
        } elseif (isset($data['progressAlertProgressType']) && is_string($data['progressAlertProgressType'])) {
            $ptype = goals_criteria_progress_alert_progress_type::tryFrom($data['progressAlertProgressType']);
            if ($ptype !== null) {
                $criteria->progress_alert_progress_type = $ptype;
            }
        }

        // Parse progress alert style (ignore unknowns)
        if (isset($data['progress_alert_style']) && is_string($data['progress_alert_style'])) {
            $style = goals_criteria_progress_alert_style::tryFrom($data['progress_alert_style']);
            if ($style !== null) {
                $criteria->progress_alert_style = $style;
            }
        } elseif (isset($data['progressAlertStyle']) && is_string($data['progressAlertStyle'])) {
            $style = goals_criteria_progress_alert_style::tryFrom($data['progressAlertStyle']);
            if ($style !== null) {
                $criteria->progress_alert_style = $style;
            }
        }

        // Parse progress alert format (string or object with render)
        if (isset($data['progress_alert_format'])) {
            $fmt = $data['progress_alert_format'];
            if (is_string($fmt)) {
                $criteria->progress_alert_format = trim($fmt);
            } elseif (is_array($fmt) || is_object($fmt)) {
                // Support structures coming from classes implementing goals_criteria_progress_alert_format_t
                // Expected keys: 'render' or 'format'
                $arr = (array)$fmt;
                if (isset($arr['render']) && is_string($arr['render'])) {
                    $criteria->progress_alert_format = trim($arr['render']);
                } elseif (isset($arr['format']) && is_string($arr['format'])) {
                    $criteria->progress_alert_format = trim($arr['format']);
                }
            }
        } elseif (isset($data['progressAlertFormat'])) {
            $fmt = $data['progressAlertFormat'];
            if (is_string($fmt)) {
                $criteria->progress_alert_format = trim($fmt);
            } elseif (is_array($fmt) || is_object($fmt)) {
                $arr = (array)$fmt;
                if (isset($arr['render']) && is_string($arr['render'])) {
                    $criteria->progress_alert_format = trim($arr['render']);
                } elseif (isset($arr['format']) && is_string($arr['format'])) {
                    $criteria->progress_alert_format = trim($arr['format']);
                }
            }
        }

        // Parse progress alert weekdays (array of strings)
        if (isset($data['progress_alert_weekdays']) && is_array($data['progress_alert_weekdays'])) {
            $seen = [];
            $weekdays = [];
            foreach ($data['progress_alert_weekdays'] as $wd) {
                if (!is_string($wd)) { continue; }
                $e = goals_criteria_progress_alert_weekday::tryFrom($wd);
                if ($e !== null) {
                    $key = $e->name;
                    if (!isset($seen[$key])) {
                        $seen[$key] = true;
                        $weekdays[] = $e;
                    }
                }
            }
            $criteria->progress_alert_weekdays = $weekdays;
        } elseif (isset($data['progressAlertWeekdays']) && is_array($data['progressAlertWeekdays'])) {
            $seen = [];
            $weekdays = [];
            foreach ($data['progressAlertWeekdays'] as $wd) {
                if (!is_string($wd)) { continue; }
                $e = goals_criteria_progress_alert_weekday::tryFrom($wd);
                if ($e !== null) {
                    $key = $e->name;
                    if (!isset($seen[$key])) {
                        $seen[$key] = true;
                        $weekdays[] = $e;
                    }
                }
            }
            $criteria->progress_alert_weekdays = $weekdays;
        }

        // Parse progress alert time of day (string with timezone)
        if (isset($data['progress_alert_time_of_day']) && is_string($data['progress_alert_time_of_day'])) {
            $criteria->progress_alert_time_of_day = trim((string)$data['progress_alert_time_of_day']);
        } elseif (isset($data['progressAlertTimeOfDay']) && is_string($data['progressAlertTimeOfDay'])) {
            $criteria->progress_alert_time_of_day = trim((string)$data['progressAlertTimeOfDay']);
        }

        // Parse timeframe with validation
        if (isset($data['start']) && is_string($data['start'])) {
            try {
                $criteria->start = new \DateTime($data['start']);
            } catch (\Exception) {
                // keep default
            }
        }
        if (isset($data['end']) && is_string($data['end'])) {
            try {
                $criteria->end = new \DateTime($data['end']);
            } catch (\Exception) {
                // keep default
            }
        }

        // Ensure end is not before start (swap if needed)
        if (($criteria->start instanceof \DateTimeInterface) && ($criteria->end instanceof \DateTimeInterface)) {
            if ($criteria->end < $criteria->start) {
                $tmp = $criteria->start;
                $criteria->start = $criteria->end;
                $criteria->end = $tmp;
            }
        }
        // Parse users (expects array of customer_numbers or objects with customer_number)
        if (isset($data['users']) && is_array($data['users'])) {
            $seen = [];
            $users = array_map(function ($item) use (&$seen) {
                $customerNumber = null;
                if (is_array($item)) {
                    if (isset($item['customer_number'])) {
                        $customerNumber = (int)$item['customer_number'];
                    } elseif (isset($item['customerNumber'])) { // allow camelCase
                        $customerNumber = (int)$item['customerNumber'];
                    }
                } elseif (is_numeric($item)) {
                    $customerNumber = (int)$item;
                }
                if ($customerNumber === null || $customerNumber <= 0) {
                    return null;
                }
                if (isset($seen[$customerNumber])) {
                    return null; // dedupe early
                }
                $seen[$customerNumber] = true;
                return (new \objects\users_o())->getUserByCustomerNumber($customerNumber);
            }, $data['users']);
            // Filter out nulls in case of malformed entries
            $users = array_values(array_filter($users));
            $criteria->users->set($users);
        }

        // Parse departments (expects array of IDs or objects with id)
        if (isset($data['departments']) && is_array($data['departments'])) {
            $seenDept = [];
            $departments = array_map(function ($item) use (&$seenDept) {
                $id = null;
                if (is_array($item)) {
                    if (isset($item['id'])) {
                        $id = (int)$item['id'];
                    }
                } elseif (is_numeric($item)) {
                    $id = (int)$item;
                }
                if ($id === null || $id <= 0) {
                    return null;
                }
                if (isset($seenDept[$id])) {
                    return null;
                }
                $seenDept[$id] = true;
                return (new \objects\departments_o())->select($id);
            }, $data['departments']);
            $departments = array_values(array_filter($departments));
            $criteria->departments->set($departments);
        }

        // Parse products (expects array of IDs or objects with id)
        if (isset($data['products']) && is_array($data['products'])) {
            $seenProd = [];
            $products = array_map(function ($item) use (&$seenProd) {
                $id = null;
                if (is_array($item)) {
                    if (isset($item['id'])) {
                        $id = (int)$item['id'];
                    }
                } elseif (is_numeric($item)) {
                    $id = (int)$item;
                }
                if ($id === null || $id <= 0) {
                    return null;
                }
                if (isset($seenProd[$id])) {
                    return null;
                }
                $seenProd[$id] = true;
                return (new \objects\products_o())->select($id);
            }, $data['products']);
            $products = array_values(array_filter($products));
            $criteria->products->set($products);
        }

        return $criteria;
    }

    /**
     * @throws \Exception
     */
    public static function calculateProgressFromArray(array $criteria_array): float
    {
        $criteria = self::fromJson(json_encode($criteria_array));
        return $criteria->getProgress();
    }

    /**
     * @throws \Exception
     */
    public static function calculateDepartmentalProgressFromArray(array $criteria_array): array
    {
        $criteria = self::fromJson(json_encode($criteria_array));
        return $criteria->getDepartmentalProgress();
    }

    /**
     * Export the criteria as an associative array suitable for JSON encoding.
     * Ensures a canonical schema matching fromJson expectations.
     * @return array
     */
    public function toArray(): array
    {
        global /** @var db $db */ $db;
        // Normalize lists to identifiers only
        $users = $this->users?->listCustomerNumbers() ?? [];
        $departments = $this->departments?->listIDs() ?? [];
        $products = $this->products?->listIDs() ?? [];

        // Remove nulls and duplicates just in case
        $users = array_values(array_unique(array_filter($users, fn($v) => $v !== null)));
        $departments = array_values(array_unique(array_filter($departments, fn($v) => $v !== null)));
        $products = array_values(array_unique(array_filter($products, fn($v) => $v !== null)));

        return [
            'type' => $this->type?->name ?? goals_criteria_type::NONE->name,
            'target' => $this->target ?? 0,
            'label' => (string)$this->label,
            'start' => ($this->start instanceof \DateTimeInterface) ? $this->start->format(DATE_ATOM) : null,
            'end' => ($this->end instanceof \DateTimeInterface) ? $this->end->format(DATE_ATOM) : null,
            'users' => $users,
            'departments' => $departments,
            'products' => $products,
            'progress_alert_frequency' => $this->progress_alert_frequency?->name ?? goals_criteria_progress_alert_frequency::NONE->name,
            'progress_alert_destination' => $this->progress_alert_destination?->name ?? goals_criteria_progress_alert_destination::NONE->name,
            'progress_alert_progress_type' => $this->progress_alert_progress_type?->name ?? goals_criteria_progress_alert_progress_type::NONE->name,
            'progress_alert_style' => $this->progress_alert_style?->name ?? goals_criteria_progress_alert_style::NONE->name,
            'progress_alert_format' => $this->progress_alert_format,
            'progress_alert_weekdays' => array_map(fn($e) => ($e instanceof goals_criteria_progress_alert_weekday) ? $e->name : (string)$e, $this->progress_alert_weekdays ?? []),
            'progress_alert_time_of_day' => $this->progress_alert_time_of_day,
        ];
    }

    /**
     * Export the criteria to JSON
     * @return string
     */
    public function toJson(): string
    {
        return json_encode($this->toArray());
    }

    /**
     * Render a progress alert message based on current destination, progress type, style and optional template
     */
    public function renderProgressAlert(): string
    {
        return \goals\services\goals_progress_alert_renderer::render($this);
    }

    private function getProgress(): int
    {
        return match ($this->type) {
            goals_criteria_type::PRODUCT => $this->products->count($this),
            goals_criteria_type::REVENUE => $this->sumRevenue(),
            goals_criteria_type::VISITS => $this->countVisits(),
            default => 0,
        };
    }

    /**
     * Calculate revenue within the criteria filters.
     * Revenue is computed as sum(price * quantity) for matching order items.
     * Prices are assumed to be stored as integers (e.g., cents) in the database.
     */
    private function sumRevenue(): int
    {
        $datetime_start = $this->start ?? new \DateTime();
        $datetime_end = $this->end ?? new \DateTime();

        $items = order_items_o::getListByCriteria(
            department_ids: $this->departments?->listIDs() ?? [],
            product_ids: $this->products?->listIDs() ?? [],
            customer_numbers: $this->users?->listCustomerNumbers() ?? [],
            datetime_start: $datetime_start,
            datetime_end: $datetime_end,
        );

        $sum = 0;
        foreach ($items as $item) {
            $price = (int)$item->price->value();
            $qty = (int)$item->quantity->value();
            $sum += $price * $qty;
        }
        return $sum;
    }

    /**
     * Count visits within the criteria filters.
     * In this domain a "visit" corresponds to unique order records.
     */
    private function countVisits(): int
    {
        $datetime_start = $this->start ?? new \DateTime();
        $datetime_end = $this->end ?? new \DateTime();

        $items = order_items_o::getListByCriteria(
            department_ids: $this->departments?->listIDs() ?? [],
            product_ids: $this->products?->listIDs() ?? [],
            customer_numbers: $this->users?->listCustomerNumbers() ?? [],
            datetime_start: $datetime_start,
            datetime_end: $datetime_end,
        );

        // Use an associative array to track unique order IDs
        $unique_orders = [];
        foreach ($items as $item) {
            $order_id = (int)$item->order_id->value();
            $unique_orders[$order_id] = true; // Value doesn't matter
        }

        return count($unique_orders);
    }

    /**
     * Public accessor for the current result based on criteria type.
     * Allows external callers (e.g., goals class) to retrieve the computed result
     * without duplicating switch logic.
     */
    public function currentResult(): int|float
    {
        return $this->getProgress();
    }

    public function validateAndSanitize(): void
    {
        // Ensure target is non-negative
        if ($this->target < 0) {
            $this->target = 0;
        }

        // Ensure timeframe is valid
        if (($this->start instanceof \DateTimeInterface) && ($this->end instanceof \DateTimeInterface)) {
            if ($this->end < $this->start) {
                $tmp = $this->start;
                $this->start = $this->end;
                $this->end = $tmp;
            }
        }

        // Ensure unknown enum values are reset to defaults
        if (!in_array($this->type, goals_criteria_type::cases())) {
            $this->type = goals_criteria_type::NONE;
        }
        if (!in_array($this->progress_alert_frequency, goals_criteria_progress_alert_frequency::cases())) {
            $this->progress_alert_frequency = goals_criteria_progress_alert_frequency::NONE;
        }
        if (!in_array($this->progress_alert_destination, goals_criteria_progress_alert_destination::cases())) {
            $this->progress_alert_destination = goals_criteria_progress_alert_destination::NONE;
        }
        if (!in_array($this->progress_alert_progress_type, goals_criteria_progress_alert_progress_type::cases())) {
            $this->progress_alert_progress_type = goals_criteria_progress_alert_progress_type::NONE;
        }
        if (!in_array($this->progress_alert_style, goals_criteria_progress_alert_style::cases())) {
            $this->progress_alert_style = goals_criteria_progress_alert_style::NONE;
        }

        // Sanitize label
        if (is_string($this->label)) {
            $label = trim($this->label);
            $label = strip_tags($label);
            $label = preg_replace('/\s+/', ' ', $label);
            if ($label !== null) {
                $label = mb_substr($label, 0, 255);
                $this->label = $label;
            }
        }

        // Sanitize progress alert format (length depends on destination)
        if (is_string($this->progress_alert_format)) {
            $fmt = trim($this->progress_alert_format);
            $fmt = strip_tags($fmt);
            $fmt = preg_replace('/\s+/', ' ', $fmt);
            if ($fmt !== null) {
                $maxLen = match ($this->progress_alert_destination) {
                    \goals\helpers\goals_criteria_progress_alert_destination::SMS => 160,
                    default => 1024,
                };
                $fmt = mb_substr($fmt, 0, $maxLen);
                $this->progress_alert_format = $fmt === '' ? null : $fmt;
            }
        }

        // Sanitize weekdays: keep only valid enum values, dedupe, and normalize order (Mon..Sun)
        if (is_array($this->progress_alert_weekdays)) {
            $seen = [];
            $clean = [];
            foreach ($this->progress_alert_weekdays as $wd) {
                $enum = $wd instanceof goals_criteria_progress_alert_weekday
                    ? $wd
                    : (is_string($wd) ? goals_criteria_progress_alert_weekday::tryFrom($wd) : null);
                if ($enum !== null) {
                    $k = $enum->name;
                    if (!isset($seen[$k])) {
                        $seen[$k] = true;
                        $clean[] = $enum;
                    }
                }
            }
            // Order by weekday index
            $order = [
                'MONDAY' => 1,
                'TUESDAY' => 2,
                'WEDNESDAY' => 3,
                'THURSDAY' => 4,
                'FRIDAY' => 5,
                'SATURDAY' => 6,
                'SUNDAY' => 7,
            ];
            usort($clean, function ($a, $b) use ($order) {
                $an = $a->name ?? (string)$a;
                $bn = $b->name ?? (string)$b;
                return ($order[$an] ?? 99) <=> ($order[$bn] ?? 99);
            });
            $this->progress_alert_weekdays = $clean;
        }

        // Sanitize time of day with timezone (HH:MMZ or HH:MM±HH:MM)
        if (is_string($this->progress_alert_time_of_day)) {
            $time = trim($this->progress_alert_time_of_day);
            // Accept Z, +HH:MM, -HH:MM (offset colon optional: +HHMM)
            $pattern = '/^([01]\d|2[0-3]):[0-5]\d(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$/';
            if (!preg_match($pattern, $time)) {
                // Try to normalize offsets like +HHMM to +HH:MM
                if (preg_match('/^([01]\d|2[0-3]):([0-5]\d)([+-])(\d{2})(\d{2})$/', $time, $m)) {
                    $time = sprintf('%s:%s%s%s:%s', $m[1], $m[2], $m[3], $m[4], $m[5]);
                }
            }
            if (preg_match($pattern, $time)) {
                $this->progress_alert_time_of_day = $time;
            } else {
                $this->progress_alert_time_of_day = null;
            }
        }
    }

    /**
     * @throws \Exception
     */
    private function getDepartmentalProgress(): array
    {
        $results = [];
        $department_ids = $this->departments->listIDs();
        foreach ($department_ids as $dept_id) {
            $dept_criteria = clone $this;
            $dept_criteria->departments->set([(new \objects\departments_o())->select($dept_id)]);
            $results[$dept_id] = $dept_criteria->getProgress();
        }
        // reset the departments to original
        $this->departments->set(array_map(fn($id) => (new \objects\departments_o())->select($id), $department_ids));
        return $results;
    }

    public function withDepartmentFilter(int $department_id): goals_criteria
    {
        // Clone the criteria and create a fresh departments collection to avoid mutating the original
        $new_criteria = clone $this;
        $new_criteria->departments = new goals_criteria_departments();
        $new_criteria->departments->set([(new \objects\departments_o())->select($department_id)]);
        return $new_criteria;
    }

    public function getAlertDaysForDepartment(int $deptId): array
    {
        $alert_days = [];
        foreach ($this->progress_alert_weekdays as $weekday) {
            if ($weekday instanceof goals_criteria_progress_alert_weekday) {
                $alert_days[] = $weekday;
            }
        }
        return $alert_days;
    }
}