<?php

namespace goals\services;

use goals\classes\goals_criteria;
use goals\formats\email_progress_alert_format;
use goals\formats\slack_progress_alert_format;
use goals\formats\sms_progress_alert_format;
use goals\helpers\goals_criteria_progress_alert_destination as Dest;
use goals\helpers\goals_criteria_progress_alert_progress_type as PType;
use goals\helpers\goals_criteria_progress_alert_style as Style;
use objects\departments_o;

class goals_progress_alert_renderer
{
    public static function render(goals_criteria $criteria, ?departments_o $department_to_highlight = null): string
    {
        $label = (string)($criteria->label ?? 'Goal');
        $count = (int)self::getProgress($criteria);
        $target = (int)($criteria->target ?? 0);
        $percent = $target > 0 ? round(($count / max(1, $target)) * 100, 2) : 0.0;
        $dateFmt = 'Y-m-d';
        $tfStart = $criteria->start instanceof \DateTimeInterface ? $criteria->start->format($dateFmt) : '';
        $tfEnd = $criteria->end instanceof \DateTimeInterface ? $criteria->end->format($dateFmt) : '';
        $timeframe = ($tfStart && $tfEnd) ? "$tfStart → $tfEnd" : '';
        $deptList = implode(',', $criteria->departments?->listIDs() ?? []);

        // Prepare readable names for fields
        $departmentsList = $criteria->departments?->list() ?? [];
        $usersList = $criteria->users?->list() ?? [];
        $productsList = $criteria->products?->list() ?? []; 

        $deptNames = self::commaNamesFromObjects($departmentsList, 'department');
        $customerNames = self::commaNamesFromObjects($usersList, 'user');
        $productNames = self::commaNamesFromObjects($productsList, 'product');

        // Pluralize Danish labels based on counts
        $deptLabel = (count($departmentsList) === 1) ? 'Afdeling' : 'Afdelinger';
        $customerLabel = (count($usersList) === 1) ? 'Kunde' : 'Kunder';
        $productLabel = (count($productsList) === 1) ? 'Produkt' : 'Produkter';

        $prefix = match ($criteria->progress_alert_style) {
            Style::DEPARTMENT_COMPARE => '[Dept compare] ',
            Style::COLLECTIVE => '[Collective] ',
            Style::SINGLE_DEPARTMENT => '[Single] ',
            default => ''
        };

        $body = match ($criteria->progress_alert_progress_type) {
            // New DK multi-line formats for COUNT_ONLY and ALL
            // COUNT_ONLY should show raw counts only (no targets/percent)
            PType::COUNT_ONLY => self::renderDanishPeriodSummary($criteria, includeTargets: false,
                header: [
                    $deptNames !== '' ? "$deptLabel: $deptNames" : null,
                    $customerNames !== '' ? "$customerLabel: $customerNames" : null,
                    $productNames !== '' ? "$productLabel: $productNames" : null,
                ], department: $department_to_highlight),
            PType::ALL => self::renderDanishPeriodSummary($criteria, includeTargets: $target > 0,
                header: [
                    $deptNames !== '' ? "$deptLabel: $deptNames" : null,
                    $customerNames !== '' ? "$customerLabel: $customerNames" : null,
                    $productNames !== '' ? "$productLabel: $productNames" : null,
                ], department: $department_to_highlight),
            // Legacy single-line fallbacks
            PType::PERCENTAGE_ONLY => sprintf('Progress: %s%%', number_format($percent, 2)),
            PType::COUNT_AND_TARGET => sprintf('Progress: %d / %d', $count, $target),
            default => sprintf('Progress: %s%%', number_format($percent, 2)),
        };

        // Keep old composition only for legacy one-line body strings
        $composed = $body;
        if (!str_contains($body, "\n")) {
            $parts = array_filter([
                $prefix . $label,
                $body,
                $timeframe,
                $deptList ? "Depts: $deptList" : null,
            ]);
            $composed = implode(' | ', $parts);
        }

        // If a custom template string is provided, use it as a printf-style template
        // Supported tokens: {label}, {percent}, {count}, {target}, {timeframe}, {departments}, {prefix}, {body}, {department_names}, {customer_names}, {product_names}
        if (is_string($criteria->progress_alert_format) && $criteria->progress_alert_format !== '') {
            $tmpl = $criteria->progress_alert_format;
            $map = [
                '{label}' => $label,
                '{percent}' => number_format($percent, 2),
                '{count}' => (string)$count,
                '{target}' => (string)$target,
                '{timeframe}' => $timeframe,
                '{departments}' => $deptList,
                '{prefix}' => $prefix,
                '{body}' => $body,
                '{department_names}' => $deptNames,
                '{customer_names}' => $customerNames,
                '{product_names}' => $productNames,
            ];
            $composed = strtr($tmpl, $map);
        }

        // Wrap in proper destination format class to enforce channel limits
        // Slack-specific: bold header keys like "Kunder:" → "*Kunder:*"
        if ($criteria->progress_alert_destination === Dest::SLACK) {
            $composed = self::applySlackBoldKeys($composed);
        }
        return match ($criteria->progress_alert_destination) {
            Dest::EMAIL => (new email_progress_alert_format($composed))->render(),
            Dest::SMS => (new sms_progress_alert_format($composed))->render(),
            Dest::SLACK => (new slack_progress_alert_format($composed))->render(),
            default => $composed,
        };
    }

    private static function getProgress(goals_criteria $criteria): int
    {
        $ref = new \ReflectionClass($criteria);
        if ($ref->hasMethod('getProgress')) {
            $m = $ref->getMethod('getProgress');
            $m->setAccessible(true);
            /** @var int $val */
            $val = $m->invoke($criteria);
            return (int)$val;
        }
        return 0;
    }

    private static function renderDanishPeriodSummary(
        goals_criteria $criteria,
        bool $includeTargets,
        ?array $header = null,
        ?departments_o $department = null
    ): string
    {
        /**
         * Renders a Danish multi-line progress summary for yesterday, week-to-date, and month-to-date.
         * If $includeTargets is true and a target is set, shows progress as "X ud af Y (Z%)".
         * Otherwise, shows just the raw counts.
         */
        // Collect departments to summarize (id, label, highlight) using stable ID list
        $departments = [];
        $deptIds = $criteria->departments?->listIDs() ?? [];
        foreach ($deptIds as $deptId) {
            $deptId = (int)$deptId;
            $deptName = '';
            try {
                $deptObj = (new \objects\departments_o())->select($deptId);
                if (isset($deptObj->name)) {
                    $deptName = (string)$deptObj->name->value();
                }
                if ($deptName === '') {
                    $deptName = 'Afdeling ' . (string)$deptId;
                }
            } catch (\Throwable) {
                $deptName = 'Afdeling (ukendt)';
            }
            $departments[] = [
                'id' => $deptId,
                'label' => $deptName,
                'highlight' => ($department && isset($department->id) && (int)$department->id === $deptId)
            ];
        }
        // Build the output lines
        $outLines = [];
        // Prepend header lines if provided
        if (is_array($header)) {
            foreach (array_filter($header, fn($v) => $v !== null && $v !== '') as $hLine) {
                $outLines[] = (string)$hLine;
            }
        }
        // Change grouping: render by period, then list departments under each period
        $periods = [
            [
                'label' => 'Igår',
                'range' => self::getYesterdayRange(),
            ],
            [
                'label' => 'Ugen total',
                'range' => self::getWeekToDateRange(),
            ],
            [
                'label' => 'Måneden total',
                'range' => self::getMonthToDateRange(),
            ],
        ];

        foreach ($periods as $idx => $period) {
            $label = (string)$period['label'];
            [$ps, $pe] = $period['range'];
            $range = self::clampToCriteriaWindow($criteria, $ps, $pe);

            // Period lines (Used to show total before the department breakdown)
            $periodLines = [];

            // Period header
            //$outLines[] = $label . ':';

            $total_period_count = 0;

            foreach ($departments as $deptData) {
                $deptId = $deptData['id'];
                $count = 0;
                if ($deptId !== null && $range !== null) {
                    [$rs, $re] = $range;
                    $base = $criteria->withDepartmentFilter((int)$deptId);
                    $c = clone $base;
                    // goals_criteria timeframe expects mutable \DateTime
                    $c->start = \DateTime::createFromImmutable($rs);
                    $c->end = \DateTime::createFromImmutable($re);
                    $count = self::getProgress($c);
                }
                $total_period_count += $count;

                if ($includeTargets && isset($criteria->target)) {
                    // Set the target to (operating days in period) * (daily target)
                    $target = self::getTargetForDepartmentInTimeframe(
                        $criteria,
                        $rs,
                        $re
                    );
                    $percent = $target > 0 ? round(($count / max(1, $target)) * 100, 2) : 0.0;
                    // If the $department is set and matches the current department, make the related line stand out (*text here*)
                    $line = $deptData['highlight']
                        ? "> *" . sprintf('%s: %d ud af %d (%.2f%%)', $deptData['label'], $count, $target, $percent) . "*"
                        : "- " . sprintf('%s: %d ud af %d (%.2f%%)', $deptData['label'], $count, $target, $percent);
                } else {
                    // If the $department is set and matches the current department, make the related line stand out (*text here*)
                    $line = $deptData['highlight']
                        ? "> *" .sprintf('%s: %d', $deptData['label'], $count) . "*"
                        : " - " .sprintf('%s: %d', $deptData['label'], $count);
                }
                $periodLines[] = $line;
            }
            // Total line for the period
            if ($includeTargets && isset($criteria->target)) {
                $total_target = self::getTargetForDepartmentInTimeframe(
                        $criteria,
                        $rs,
                        $re
                    ) * count($departments);
                $total_percent = $total_target > 0 ? round(($total_period_count / max(1, $total_target)) * 100, 2) : 0.0;
                $outLines[] = sprintf($label . ': %d ud af %d (%.2f%%)',
                    $total_period_count,
                    $total_target,
                    $total_percent
                );
            } else {
                $outLines[] = sprintf('%s: %d',
                    $label,
                    $total_period_count
                );
            }
            // Add a blank line between periods (but not after the last one)
            if ($idx < count($periods) - 1) {
                $periodLines[] = '';
            }
            // Add the period's department lines
            $outLines = array_merge($outLines, $periodLines);
        }

        return implode("\n", $outLines);
    }
    // --- Date range helpers for summary periods ---

    /**
     * Get the amount of days that alerts will be sent during the timeframe
     * @param goals_criteria $criteria
     * @param \DateTimeImmutable $rs
     * @param \DateTimeImmutable $re
     * @return int
     */
    public function countOperatingDaysInTimeframe(
        goals_criteria $criteria,
        \DateTimeImmutable $rs,
        \DateTimeImmutable $re
    ): int
    {
        $operatingWeekDays = $criteria->operating_days_of_week ?? [1, 2, 3, 4, 5]; // Default to Mon-Fri
        $operatingDays = 0;
        $current = $rs;
        while ($current <= $re) {
            $dayOfWeek = (int)$current->format('N'); // 1 (Mon) to 7 (Sun)
            if (in_array($dayOfWeek, $operatingWeekDays, true)) {
                $operatingDays++;
            }
            $current = $current->modify('+1 day');
        }
        return $operatingDays;
    }


    private static function getDailyTarget(goals_criteria $criteria): float
    {
        // Normalize criteria start/end to immutable safely (supports both DateTime and DateTimeImmutable)
        $s = $criteria->start ?? null;
        $e = $criteria->end ?? null;
        $sImm = $s instanceof \DateTimeImmutable
            ? $s
            : ($s instanceof \DateTime
                ? \DateTimeImmutable::createFromMutable($s)
                : new \DateTimeImmutable('now'));
        $eImm = $e instanceof \DateTimeImmutable
            ? $e
            : ($e instanceof \DateTime
                ? \DateTimeImmutable::createFromMutable($e)
                : new \DateTimeImmutable('now'));

        $operatingDays = (new goals_progress_alert_renderer)->countOperatingDaysInTimeframe(
            $criteria,
            $sImm,
            $eImm
        );
        if ($operatingDays <= 0) {
            return 0.0;
        }
        return ((int)($criteria->target ?? 0)) / $operatingDays;
    }

    /**
     * @param goals_criteria $criteria
     * @param \DateTimeImmutable $rs
     * @param \DateTimeImmutable $re
     * @return int
     */
    private static function getTargetForDepartmentInTimeframe(
        goals_criteria $criteria,
        \DateTimeImmutable $rs,
        \DateTimeImmutable $re
    ): int
    {
        $dailyTarget = self::getDailyTarget($criteria);
        if ($dailyTarget <= 0.0) {
            return 0;
        }
        $operatingDays = (new goals_progress_alert_renderer)->countOperatingDaysInTimeframe(
            $criteria,
            $rs,
            $re
        );
        return (int)round(($dailyTarget * $operatingDays) / count($criteria->departments?->listIDs() ?? [1]));
    }

    private static function getYesterdayRange(): array
    {
        $now = new \DateTimeImmutable('now');
        $y = $now->modify('-1 day');
        return [$y->setTime(0, 0, 0), $y->setTime(23, 59, 59)];
    }

    private static function getWeekToDateRange(): array
    {
        $now = new \DateTimeImmutable('now');
        $monday = $now->modify('monday this week');
        return [$monday->setTime(0, 0, 0), $now->setTime(23, 59, 59)];
    }

    private static function getMonthToDateRange(): array
    {
        $now = new \DateTimeImmutable('now');
        $first = $now->modify('first day of this month');
        return [$first->setTime(0, 0, 0), $now->setTime(23, 59, 59)];
    }

    private static function clampToCriteriaWindow(goals_criteria $criteria, \DateTimeImmutable $start, \DateTimeImmutable $end): ?array
    {
        $sTs = $start->getTimestamp();
        $eTs = $end->getTimestamp();
        if ($criteria->start instanceof \DateTimeInterface) {
            $cs = $criteria->start->getTimestamp();
            if ($sTs < $cs) { $sTs = $cs; }
        }
        if ($criteria->end instanceof \DateTimeInterface) {
            $ce = $criteria->end->getTimestamp();
            if ($eTs > $ce) { $eTs = $ce; }
        }
        if ($sTs > $eTs) { return null; }
        // Preserve the timezone of provided start/end
        $tz = $start->getTimezone();
        $s = (new \DateTimeImmutable('@' . $sTs))->setTimezone($tz);
        $e = (new \DateTimeImmutable('@' . $eTs))->setTimezone($tz);
        return [$s, $e];
    }

    /**
     * For Slack destination, bold specific Danish header keys and period labels at line starts.
     * Examples:
     *  - "Kunder: DHL" → "*Kunder:* DHL"
     *  - "Igår: 5" → "*Igår:* 5"
     * Avoids double-bold if already applied.
     */
    private static function applySlackBoldKeys(string $text): string
    {
        // Keys to bold (singular/plural)
        $keys = [
            'Afdeling', 'Afdelinger',
            'Kunde', 'Kunder',
            'Produkt', 'Produkter',
            // Period labels
            'Igår', 'Ugen total', 'Måneden total',
        ];

        $lines = preg_split("/\r?\n/", $text) ?: [$text];
        $out = [];
        foreach ($lines as $line) {
            $trimmed = ltrim($line);
            $prefixSpaces = substr($line, 0, strlen($line) - strlen($trimmed));

            $modified = false;
            foreach ($keys as $key) {
                // Avoid if already bolded (starts with *Key:*)
                $alreadyBold = preg_match('/^\\*' . preg_quote($key, '/') . ':\\*/u', $trimmed) === 1;
                if ($alreadyBold) { $modified = true; break; }

                // Match start-of-line key followed by colon and a space or end
                if (preg_match('/^' . preg_quote($key, '/') . ':(\s|$)/u', $trimmed) === 1) {
                    $trimmed = preg_replace(
                        '/^' . preg_quote($key, '/') . ':/u',
                        '*'.$key.':*',
                        $trimmed,
                        1
                    );
                    $modified = true;
                    break;
                }
            }
            $out[] = $prefixSpaces . $trimmed;
        }
        return implode("\n", $out);
    }

    /**
     * Try to extract comma-separated names from domain objects using common patterns.
     * Fallback to IDs or customer numbers when necessary.
     * @param array $objects
     * @param string $type one of: department|user|product
     */
    private static function commaNamesFromObjects(array $objects, string $type): string
    {
        $names = [];
        foreach ($objects as $obj) {
            try {
                switch ($type) {
                    case 'department':
                        // departments_o has ->name object_property
                        if (isset($obj->name)) {
                            $val = (string)$obj->name->value();
                            if ($val !== '') { $names[] = $val; break; }
                        }
                        // fallback to id
                        if (isset($obj->id)) { $names[] = (string)$obj->id; }
                        break;
                    case 'user':
                        // Prefer economic customer name when possible
                        if (isset($obj->customer_number)) {
                            $num = (int)$obj->customer_number->value();
                            // users_o provides getCustomerNames([numbers]) → [num => name]
                            if (method_exists($obj, 'getCustomerNames')) {
                                try {
                                    $map = $obj->getCustomerNames([$num]);
                                    $key = (string)$num;
                                    if (isset($map[$key]) && is_string($map[$key]) && $map[$key] !== '') {
                                        $names[] = $map[$key];
                                        break;
                                    }
                                } catch (\Throwable) {
                                    // ignore and fall back
                                }
                            }
                        }
                        // Fallback to display_name if available
                        if (isset($obj->display_name)) {
                            $val = (string)$obj->display_name->value();
                            if ($val !== '') { $names[] = $val; break; }
                        }
                        // Last resort: show customer number
                        if (isset($obj->customer_number)) { $names[] = (string)$obj->customer_number->value(); }
                        break;
                    case 'product':
                        if (isset($obj->name)) {
                            $val = (string)$obj->name->value();
                            if ($val !== '') { $names[] = $val; break; }
                        }
                        if (isset($obj->id)) { $names[] = (string)$obj->id; }
                        break;
                }
            } catch (\Throwable) {
                // ignore and continue
            }
        }
        return implode(', ', $names);
    }
}
