<?php

namespace Personi\Helper;

class Personi extends \Lime\Helper {

    public function process($array, array $audience, array $ctx = []) {

        // Handle Variable Replacement for Strings
        if (\is_string($array)) {
            if (isset($ctx['variables']) && \is_array($ctx['variables'])) {
                return $this->replaceVariables($array, $ctx['variables']);
            }
            return $array;
        }

        if (!\is_array($array)) {
            return $array;
        }

        // Check for variant wrapper
        if (isset($array['personi:variants'])) {
            
            $variants = $array['personi:variants'];

            if (!\is_array($variants) || !\count($variants)) {
                return null;
            }

            $resolved = $this->resolve($variants, $audience, $ctx);
            
            // Recursively process the resolved content
            return $this->process($resolved, $audience, $ctx);
        }

        foreach ($array as $k => $v) {
            $array[$k] = $this->process($v, $audience, $ctx);
        }

        if (isset($array['component'], $array['data']['layout']['layout']) && $array['component'] == 'layoutVariants') {
            $array['data']['layout'] = $array['data']['layout']['layout'];
        }

        return $array;
    }

    protected function resolve($variants, array $audience, array $ctx = []) {

        $candidates = $this->prefilterVariants(\is_array($variants) ? $variants : [], $ctx);

        $default = null;
        $variant = null;
        $similarity = 0;

        foreach ($candidates as &$v) {

            if (isset($v['active']) && !$v['active']) continue;

            // Skip variants outside configured schedule window (if any)
            if (!$this->isWithinSchedule($v, $ctx)) continue;

            if (!$default) {
                $default = $v['data'] ?? null;
            }

            if (!isset($v['audience']) || !\is_array($v['audience']) || !\count($v['audience'])) continue;

            $s = $this->getSimilarityCoefficient($audience, $v['audience']);

            if ($s > $similarity) {
                $variant = $v;
                $similarity = $s;
            }
        }

        return $variant['data'] ?? $default;
    }

    protected function getSimilarityCoefficient(array $list1, array $list2) {

        $arr_intersection = \array_intersect($list1, $list2);
        $arr_union = \array_unique(\array_merge($list1, $list2));

        return \count($arr_intersection) / \count($arr_union);
    }

    /**
     * Prefilter variants to only those passing rule checks.
     * Currently checks:
     *  - active flag
     *  - schedule window (via meta.schedule or meta shorthand)
     */
    protected function prefilterVariants(array $variants, array $ctx = []): array {
        $filtered = [];

        foreach ($variants as $v) {
            
            if (!\is_array($v)) continue;
            if (isset($v['active']) && !$v['active']) continue;
            if (!$this->isWithinSchedule($v, $ctx)) continue;

            $filtered[] = $v;
        }

        return $filtered;
    }

    /**
     * Determine if a variant is currently within its schedule as defined in meta.
     * Accepts meta.schedule with optional keys: start, end, timezone|tz, days, times
     * - start/end: any DateTime-parsable string (ISO 8601 recommended)
     * - timezone|tz: PHP timezone identifier
     * - days: array with 0-6 (0=Sun) or day names (sun..sat)
     * - times: array of windows ("HH:MM-HH:MM" or {from, to})
     */
    protected function isWithinSchedule(array $variant, array $ctx = []): bool {
        $meta = $variant['meta'] ?? null;
        if (!\is_array($meta)) return true;

        $schedule = $meta['schedule'] ?? null;
        if (!\is_array($schedule)) {
            // Fallback: allow schedule fields directly on meta
            $hasStart = isset($meta['start']);
            $hasEnd = isset($meta['end']);
            if (!$hasStart && !$hasEnd && !isset($meta['days']) && !isset($meta['times'])) return true;
            $schedule = [
                'start' => $meta['start'] ?? null,
                'end' => $meta['end'] ?? null,
                'timezone' => $meta['timezone'] ?? ($meta['tz'] ?? null),
                'days' => $meta['days'] ?? null,
                'times' => $meta['times'] ?? null,
            ];
        }

        try {
            // Determine schedule timezone from variant meta (optional)
            $tz = null;
            if (!empty($schedule['timezone'])) {
                $tz = new \DateTimeZone((string)$schedule['timezone']);
            } elseif (!empty($schedule['tz'])) {
                $tz = new \DateTimeZone((string)$schedule['tz']);
            }

            // Build reference time "now"
            if (isset($ctx['tzoffset']) && \is_numeric($ctx['tzoffset'])) {
                // Use client-provided timezone offset in minutes (relative to UTC)
                $now = new \DateTime('now', new \DateTimeZone('UTC'));
                $now->modify(\sprintf('%+d minutes', (int)$ctx['tzoffset']));
            } else if ($tz) {
                // Fallback to schedule timezone
                $now = new \DateTime('now', $tz);
            } else {
                // Server timezone
                $now = new \DateTime('now');
            }

            if (!empty($schedule['start'])) {
                $start = $tz ? new \DateTime((string)$schedule['start'], $tz) : new \DateTime((string)$schedule['start']);
                if ($now < $start) return false;
            }

            if (!empty($schedule['end'])) {
                $end = $tz ? new \DateTime((string)$schedule['end'], $tz) : new \DateTime((string)$schedule['end']);
                if ($now > $end) return false;
            }

            if (!empty($schedule['days']) && \is_array($schedule['days'])) {
                $allowed = [];
                $map = [
                    'sun' => 0, 'sunday' => 0,
                    'mon' => 1, 'monday' => 1,
                    'tue' => 2, 'tuesday' => 2,
                    'wed' => 3, 'wednesday' => 3,
                    'thu' => 4, 'thursday' => 4,
                    'fri' => 5, 'friday' => 5,
                    'sat' => 6, 'saturday' => 6,
                ];
                foreach ($schedule['days'] as $d) {
                    if (is_int($d)) {
                        if ($d >= 0 && $d <= 6) $allowed[$d] = true;
                    } elseif (is_string($d)) {
                        $k = \strtolower($d);
                        if (isset($map[$k])) $allowed[$map[$k]] = true;
                    }
                }
                if ($allowed) {
                    $w = (int)$now->format('w');
                    if (!isset($allowed[$w])) return false;
                }
            }

            if (!empty($schedule['times']) && \is_array($schedule['times'])) {
                $nowMin = ((int)$now->format('H')) * 60 + ((int)$now->format('i'));
                $ok = false;
                foreach ($schedule['times'] as $t) {
                    $from = null; $to = null;
                    if (\is_string($t) && \strpos($t, '-') !== false) {
                        [$f, $toStr] = \array_map('trim', \explode('-', $t, 2));
                        $from = $this->parseTimeToMinutes($f);
                        $to = $this->parseTimeToMinutes($toStr);
                    } elseif (\is_array($t)) {
                        if (isset($t['from'])) $from = $this->parseTimeToMinutes((string)$t['from']);
                        if (isset($t['to'])) $to = $this->parseTimeToMinutes((string)$t['to']);
                    }
                    if ($from === null || $to === null) continue;

                    if ($from <= $to) {
                        if ($nowMin >= $from && $nowMin <= $to) { $ok = true; break; }
                    } else {
                        // overnight window (e.g., 22:00-02:00)
                        if ($nowMin >= $from || $nowMin <= $to) { $ok = true; break; }
                    }
                }
                if (!$ok) return false;
            }

            return true;
        } catch (\Throwable $e) {
            // On invalid schedule configuration, do not block the variant
            return true;
        }
    }

    protected function parseTimeToMinutes(string $hhmm): ?int {
        if (!\preg_match('/^\s*(\d{1,2}):(\d{2})\s*$/', $hhmm, $m)) return null;
        $h = (int)$m[1];
        $i = (int)$m[2];
        if ($h < 0 || $h > 23 || $i < 0 || $i > 59) return null;
        return $h * 60 + $i;
    }

    protected function replaceVariables($string, $variables) {

        if (\strpos($string, '{{') === false) {
            return $string;
        }

        return \preg_replace_callback('/\{\{\s*([a-zA-Z0-9_.]+)(?:\:(.*?))?\s*\}\}/', function($matches) use ($variables) {
            
            $key = $matches[1];
            $default = $matches[2] ?? null;
            $value = $variables;

            foreach (\explode('.', $key) as $segment) {
                if (\is_array($value) && isset($value[$segment])) {
                    $value = $value[$segment];
                } else {
                    return $default !== null ? $default : $matches[0];
                }
            }

            return \is_scalar($value) ? $value : ($default !== null ? $default : $matches[0]);

        }, $string);
    }
}
