<?php

namespace MintyPHP\Module\Security\Service;

use Dompdf\Dompdf;
use Dompdf\Options;
use MintyPHP\Service\Branding\BrandingLogoService;
use MintyPHP\Service\Tenant\TenantLogoService;
use Throwable;

/**
 * Renders the customer-facing PDF report for a completed security check.
 *
 * Follows the core Dompdf pattern (see UserAccessPdfService): build a flat,
 * render-ready context from the check, hydrate a self-contained PHTML template
 * to an HTML string, then rasterise it with Dompdf (remote + PHP execution
 * disabled). The context builder is pure (no I/O) so it is fully unit-testable;
 * only the render step touches the filesystem / Dompdf.
 *
 * @api Consumed by the security/checks/report download action.
 */
final class SecurityReportPdfService
{
    public function __construct(
        private readonly SecurityCheckProcess $process,
        private readonly BrandingLogoService $brandingLogoService,
        private readonly ?TenantLogoService $tenantLogoService = null,
        private readonly ?SecurityReportSettingsGateway $reportSettings = null,
        private readonly ?SecurityReportTemplateService $templateService = null,
        private readonly ?SecurityReportLogoService $reportLogoService = null
    ) {
    }

    /**
     * Render the customer report PDF. Returns raw PDF bytes, or '' if the
     * renderer is unavailable, the template is missing, or rendering fails.
     *
     * @param array<string, mixed> $check    Enriched check row (SecurityCheckService::findById)
     * @param array{locale?: string, app_name?: string, tenant_uuid?: string} $options
     */
    public function renderCustomerReportPdf(array $check, array $options = []): string
    {
        if (!class_exists(Dompdf::class) || !class_exists(Options::class)) {
            return '';
        }

        $report = $this->buildReportContext($check);
        $report['app_name'] = trim((string) ($options['app_name'] ?? ''));
        $report['logo_data_uri'] = $this->resolveLogoDataUri((string) ($options['tenant_uuid'] ?? ''));
        $report['generated_at'] = date('Y-m-d H:i');

        $html = $this->renderReportHtml($report);
        if ($html === '') {
            return '';
        }

        try {
            $options = new Options();
            // Keep rendering deterministic: no remote fetches, no embedded PHP.
            $options->setIsRemoteEnabled(false);
            $options->setIsPhpEnabled(false);
            $options->setDefaultFont('DejaVu Sans');

            $dompdf = new Dompdf($options);
            $dompdf->loadHtml($html, 'UTF-8');
            $dompdf->setPaper('A4');
            $dompdf->render();
            $dompdf->addInfo('Title', (string) $report['title']);
            $dompdf->addInfo('Subject', (string) $report['title']);

            return (string) $dompdf->output();
        } catch (Throwable) {
            return '';
        }
    }

    /**
     * A filename-safe slug describing the report (customer + domain), e.g.
     * "security-check-report-acme-acme-de". Always returns a non-empty base.
     *
     * @param array<string, mixed> $check
     */
    public function buildReportFilename(array $check): string
    {
        $parts = array_filter([
            (string) ($check['debitor_name'] ?? $check['debitor_no'] ?? ''),
            (string) ($check['domain_url'] ?? $check['domain_no'] ?? ''),
        ]);
        $slug = self::slugify(implode('-', $parts));

        return 'security-check-report' . ($slug !== '' ? '-' . $slug : '') . '.pdf';
    }

    /**
     * Build the flat, render-ready report context from an enriched check row.
     * Pure transform (no filesystem / network) — translation via t() only.
     *
     * @param array<string, mixed> $check
     * @return array<string, mixed>
     */
    public function buildReportContext(array $check): array
    {
        $processState = is_array($check['process_state'] ?? null) ? $check['process_state'] : [];
        $techState = is_array($check['tech_state'] ?? null) ? $check['tech_state'] : [];
        $snapshot = is_array($check['tech_schema_snapshot'] ?? null) ? $check['tech_schema_snapshot'] : [];
        $progress = is_array($check['progress'] ?? null) ? $check['progress'] : [];
        $status = (string) ($check['status'] ?? SecurityCheckProcess::STATUS_OPEN);

        // The owner-authored, customer-facing report text captured on the final step.
        $reportFields = is_array($processState[SecurityCheckProcess::STEP_REPORT]['fields'] ?? null)
            ? $processState[SecurityCheckProcess::STEP_REPORT]['fields']
            : [];

        return [
            'title' => t('Security check report'),
            'customer_report' => trim((string) ($reportFields[SecurityCheckProcess::FIELD_CUSTOMER_REPORT] ?? '')),
            'customer_name' => (string) ($check['debitor_name'] ?? ''),
            'customer_no' => (string) ($check['debitor_no'] ?? ''),
            'domain' => (string) ($check['domain_url'] ?? ($check['domain_no'] ?? '')),
            'product' => (string) ($check['product_name'] ?? ($check['product_code'] ?? '')),
            'owner' => (string) ($check['owner_name'] ?? ''),
            'status' => $status,
            'status_label' => t(SecurityCheckProcess::statusLabel($status)),
            'summary' => [
                'done' => (int) ($progress['done'] ?? 0),
                'total' => (int) ($progress['total'] ?? 0),
                'percent' => (int) ($progress['percent'] ?? 0),
                'tech_done' => (int) ($progress['tech_done'] ?? 0),
                'tech_total' => (int) ($progress['tech_total'] ?? 0),
            ],
            'steps' => $this->buildSteps($processState),
            'tech_sections' => $this->buildTechSections($snapshot, $techState),
        ];
    }

    /**
     * High-level process milestones (label + completion date), excluding the
     * internal field values and per-step notes that are not customer-facing.
     *
     * @param array<string, mixed> $processState
     * @return list<array{number: int, label: string, done: bool, completed_at: string}>
     */
    private function buildSteps(array $processState): array
    {
        $steps = [];
        $number = 0;
        foreach ($this->process->steps() as $step) {
            $number++;
            if ($step['kind'] === SecurityCheckProcess::KIND_TECH_CHECKLIST) {
                continue;
            }
            $key = $step['key'];
            $entry = is_array($processState[$key] ?? null) ? $processState[$key] : [];
            $steps[] = [
                'number' => $number,
                'label' => t($step['label']),
                'done' => !empty($entry['done']),
                'completed_at' => trim((string) ($entry['at'] ?? '')),
            ];
        }

        return $steps;
    }

    /**
     * The frozen technical checklist grouped by section, each item carrying its
     * pass/fail state and any finding note. Items before the first section land
     * in an unlabelled leading group.
     *
     * @param array<string, mixed> $snapshot
     * @param array<string, mixed> $techState
     * @return list<array{label: string, items: list<array{label: string, done: bool, note: string}>}>
     */
    private function buildTechSections(array $snapshot, array $techState): array
    {
        $items = is_array($snapshot['items'] ?? null) ? $snapshot['items'] : [];
        $sections = [];
        $current = ['label' => '', 'items' => []];

        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }
            if (($item['type'] ?? '') === 'section') {
                if ($current['items'] !== []) {
                    $sections[] = $current;
                }
                $current = ['label' => (string) ($item['label'] ?? ''), 'items' => []];
                continue;
            }
            $key = trim((string) ($item['key'] ?? ''));
            if ($key === '') {
                continue;
            }
            $entry = is_array($techState[$key] ?? null) ? $techState[$key] : [];
            $current['items'][] = [
                'label' => (string) ($item['label'] ?? ''),
                'done' => !empty($entry['done']),
                'note' => trim((string) ($entry['note'] ?? '')),
            ];
        }
        if ($current['items'] !== []) {
            $sections[] = $current;
        }

        return $sections;
    }

    /**
     * Produce the report HTML. When a custom template is configured (and the
     * collaborating services are wired), it is filled with the report context;
     * otherwise the built-in PHTML default layout is used. Keeping the default
     * on the proven PHTML path makes the feature purely additive.
     *
     * @param array<string, mixed> $report
     */
    private function renderReportHtml(array $report): string
    {
        if ($this->reportSettings !== null && $this->templateService !== null) {
            $template = $this->reportSettings->getTemplateHtml();
            if ($template !== '') {
                return $this->templateService->render($template, $report);
            }
        }

        return $this->renderTemplate($report);
    }

    /**
     * @param array<string, mixed> $report
     */
    private function renderTemplate(array $report): string
    {
        $templatePath = dirname(__DIR__, 4) . '/templates/pdf/customer-report.phtml';
        if (!is_file($templatePath)) {
            return '';
        }

        // Isolated scope: the template only sees $report, never this service's state.
        $render = static function (string $__path, array $report): string {
            ob_start();
            include $__path;

            return (string) ob_get_clean();
        };

        return $render($templatePath, $report);
    }

    /**
     * Resolve a logo data URI: configured report logo → tenant light-logo →
     * global branding logo → static brand asset → transparent pixel. Mirrors the
     * core PDF pattern; the PDF has no theme context, so the light variant is
     * always used.
     */
    private function resolveLogoDataUri(string $tenantUuid): string
    {
        $path = '';
        $mime = '';

        if ($this->reportLogoService !== null && $this->reportLogoService->hasLogo()) {
            $logoPath = $this->reportLogoService->findLogoPath(256);
            if ($logoPath && is_file($logoPath)) {
                $path = $logoPath;
                $mime = $this->reportLogoService->detectMime($logoPath);
            }
        }

        if ($path === '' && $this->tenantLogoService !== null && $tenantUuid !== '' && $this->tenantLogoService->hasLogo($tenantUuid, TenantLogoService::THEME_LIGHT)) {
            $logoPath = $this->tenantLogoService->findLogoPath($tenantUuid, TenantLogoService::THEME_LIGHT, 128);
            if ($logoPath && is_file($logoPath)) {
                $path = $logoPath;
                $mime = $this->tenantLogoService->detectMime($logoPath);
            }
        }

        if ($path === '' && $this->brandingLogoService->hasLogo()) {
            $logoPath = $this->brandingLogoService->findLogoPath(128);
            if ($logoPath && is_file($logoPath)) {
                $path = $logoPath;
                $mime = $this->brandingLogoService->detectMime($logoPath);
            }
        }

        if ($path === '') {
            $fallback = dirname(__DIR__, 6) . '/web/brand/logo.svg';
            if (is_file($fallback)) {
                $path = $fallback;
                $mime = 'image/svg+xml';
            }
        }

        if ($path === '' || !is_file($path)) {
            return self::transparentPixelDataUri();
        }

        $content = @file_get_contents($path);
        if ($content === false || $content === '') {
            return self::transparentPixelDataUri();
        }

        return 'data:' . ($mime !== '' ? $mime : 'image/png') . ';base64,' . base64_encode($content);
    }

    private static function transparentPixelDataUri(): string
    {
        // Smallest safe placeholder so the <img> tag stays valid when no logo exists.
        return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg==';
    }

    private static function slugify(string $value): string
    {
        $value = strtolower(trim($value));
        if ($value === '') {
            return '';
        }
        $value = (string) preg_replace('/[^a-z0-9]+/', '-', $value);

        return trim($value, '-');
    }
}
