<?php

declare(strict_types=1);

namespace FecaMailshots\Application;

final class TemplateRenderer
{
    /** @var null|callable(string):?array<string,mixed> */
    private $pdfAssetResolver;

    /** @param null|callable(string):?array<string,mixed> $pdfAssetResolver */
    public function __construct(?callable $pdfAssetResolver = null)
    {
        $this->pdfAssetResolver = $pdfAssetResolver;
    }

    /**
     * @param array<string, mixed> $context
     * @return array{subject:string,message:string,pdf_attachment:string,warnings:list<string>}
     */
    public function render(string $subjectTpl, string $messageTpl, string $pdfTpl, array $context): array
    {
        if (!class_exists('Twig\\Environment')) {
            throw new \RuntimeException('Twig is not available. Ensure vendor dependencies are installed.');
        }

        $loader = new \Twig\Loader\ArrayLoader([
            'subject' => $subjectTpl,
            'message' => $messageTpl,
            'pdf' => $pdfTpl,
        ]);
        $twig = new \Twig\Environment($loader, [
            'autoescape' => 'html',
            'strict_variables' => true,
            'cache' => false,
        ]);
        $twig->addFunction(new \Twig\TwigFunction(
            'pdf_asset',
            function (string $name): string {
                return $this->renderPdfAsset($name);
            },
            ['is_safe' => ['html']]
        ));

        // Provide both original keys and canonicalized token keys.
        $safeContext = $this->canonicalizeContext($context);

        return [
            'subject' => html_entity_decode((string) $twig->render('subject', $safeContext), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
            'message' => (string) $twig->render('message', $safeContext),
            'pdf_attachment' => (string) $twig->render('pdf', $safeContext),
            'warnings' => [],
        ];
    }

    private function renderPdfAsset(string $name): string
    {
        $name = trim($name);
        if ($name === '') {
            throw new \RuntimeException('pdf_asset() requires a non-empty asset name.');
        }
        if (!is_callable($this->pdfAssetResolver)) {
            throw new \RuntimeException('pdf_asset() is unavailable: PDF asset resolver is not configured.');
        }

        $asset = ($this->pdfAssetResolver)($name);
        if (!is_array($asset)) {
            throw new \RuntimeException('pdf_asset("' . $name . '") not found.');
        }

        $mimeType = trim((string) ($asset['mime_type'] ?? ''));
        $bytes = (string) ($asset['file_bytes'] ?? '');
        $widthMm = (float) ($asset['width_mm'] ?? 0);
        $heightMm = (float) ($asset['height_mm'] ?? 0);
        $justification = trim((string) ($asset['justification'] ?? ''));
        $fileName = trim((string) ($asset['file_name'] ?? ''));

        if ($mimeType === '' || $bytes === '') {
            throw new \RuntimeException('pdf_asset("' . $name . '") is missing file content.');
        }
        if ($widthMm <= 0 || $heightMm <= 0) {
            throw new \RuntimeException('pdf_asset("' . $name . '") has invalid width/height.');
        }
        if (!in_array($justification, ['in-place', 'left', 'right'], true)) {
            throw new \RuntimeException('pdf_asset("' . $name . '") has invalid justification value.');
        }

        if ($this->needsPngTranscodeForRuntime($mimeType)) {
            [$mimeType, $bytes] = $this->transcodePngToJpeg($name, $bytes);
        }

        $dataUri = 'data:' . $mimeType . ';base64,' . base64_encode($bytes);
        $imgStyle = 'width:' . $this->mm($widthMm) . ';height:' . $this->mm($heightMm) . ';';
        if ($justification === 'right') {
            $imgStyle .= 'float:right;';
        }
        $alt = htmlspecialchars($fileName !== '' ? $fileName : $name, ENT_QUOTES);
        $src = htmlspecialchars($dataUri, ENT_QUOTES);
        $img = '<img src="' . $src . '" alt="' . $alt . '" style="' . $imgStyle . '">';

        if ($justification === 'in-place') {
            return $img;
        }
        if ($justification === 'right') {
            return $img;
        }
        return '<div style="display:block;text-align:' . $justification . ';">' . $img . '</div>';
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function canonicalizeContext(array $row): array
    {
        $out = $row;
        foreach ($row as $key => $value) {
            $value = $this->formatValueForTemplate((string) $key, $value);
            $out[$key] = $value;
            $lower = strtolower((string) $key);
            $canon = preg_replace('/[^a-z0-9_]+/', '_', $lower);
            if (!is_string($canon)) {
                throw new \RuntimeException('Failed to canonicalize template context key.');
            }
            $canon = preg_replace('/_+/', '_', $canon);
            if (!is_string($canon)) {
                throw new \RuntimeException('Failed to canonicalize template context key.');
            }
            $canon = trim($canon, '_');
            if ($canon !== '' && !array_key_exists($canon, $out)) {
                $out[$canon] = $value;
            }
        }
        return $out;
    }

    /** @param mixed $value @return mixed */
    private function formatValueForTemplate(string $key, $value)
    {
        if (!$this->looksLikeBooleanField($key)) {
            return $value;
        }

        $bool = $this->coerceBooleanishValue($value);
        if ($bool === null) {
            return $value;
        }

        return $bool ? 'Yes' : 'No';
    }

    private function looksLikeBooleanField(string $key): bool
    {
        $field = strtolower((string) preg_replace('/[^a-z0-9]+/', '', strtolower(basename(str_replace('.', '/', $key)))));
        if ($field === '') {
            return false;
        }

        return str_starts_with($field, 'is')
            || str_starts_with($field, 'has')
            || str_starts_with($field, 'can')
            || in_array($field, ['selected', 'active', 'enabled', 'disabled', 'lapsed'], true);
    }

    /** @param mixed $value */
    private function coerceBooleanishValue($value): ?bool
    {
        if (is_bool($value)) {
            return $value;
        }
        if (is_int($value) || is_float($value)) {
            if ((float) $value === 0.0) {
                return false;
            }
            if (in_array((int) $value, [1, -1], true)) {
                return true;
            }
            return null;
        }
        if (!is_string($value)) {
            return null;
        }

        if ($value === "\0") {
            return false;
        }
        if ($value === "\1" || $value === "\xff") {
            return true;
        }

        $normal = strtolower(trim($value));
        if (in_array($normal, ['0', 'false', 'no', 'n', 'off'], true)) {
            return false;
        }
        if (in_array($normal, ['1', '-1', 'true', 'yes', 'y', 'on'], true)) {
            return true;
        }

        return null;
    }

    private function mm(float $value): string
    {
        $formatted = rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
        if ($formatted === '') {
            $formatted = '0';
        }
        return $formatted . 'mm';
    }

    private function needsPngTranscodeForRuntime(string $mimeType): bool
    {
        return strtolower($mimeType) === 'image/png' && !function_exists('imagecreatefrompng');
    }

    /**
     * @return array{0:string,1:string}
     */
    private function transcodePngToJpeg(string $assetName, string $pngBytes): array
    {
        $convertBinary = $this->findExecutableBinary('convert');
        if ($convertBinary === '') {
            throw new \RuntimeException(
                'pdf_asset("' . $assetName . '") uses PNG, but neither GD PNG support nor ImageMagick convert is available.'
            );
        }
        if (!function_exists('exec')) {
            throw new \RuntimeException(
                'pdf_asset("' . $assetName . '") cannot transcode PNG: exec() is unavailable in this PHP runtime.'
            );
        }

        $inputPath = tempnam(sys_get_temp_dir(), 'feca_pdf_asset_png_');
        if (!is_string($inputPath) || $inputPath === '') {
            throw new \RuntimeException('Unable to allocate temporary file for PNG transcode.');
        }
        $inputPngPath = $inputPath . '.png';
        if (!@rename($inputPath, $inputPngPath)) {
            @unlink($inputPath);
            throw new \RuntimeException('Unable to prepare temporary PNG file for transcode.');
        }
        $outputJpegPath = $inputPngPath . '.jpg';

        try {
            if (@file_put_contents($inputPngPath, $pngBytes) === false) {
                throw new \RuntimeException('Unable to write temporary PNG for transcode.');
            }

            $cmd = escapeshellarg($convertBinary) . ' '
                . escapeshellarg($inputPngPath)
                . ' -background white -alpha remove -alpha off -quality 90 '
                . escapeshellarg($outputJpegPath)
                . ' 2>&1';

            $output = [];
            $code = 0;
            @exec($cmd, $output, $code);
            if ($code !== 0 || !is_file($outputJpegPath)) {
                throw new \RuntimeException(
                    'ImageMagick convert failed for png asset "' . $assetName . '"'
                    . ($output !== [] ? ': ' . trim(implode('; ', $output)) : '.')
                );
            }

            $jpegBytes = @file_get_contents($outputJpegPath);
            if (!is_string($jpegBytes) || $jpegBytes === '') {
                throw new \RuntimeException('Converted JPEG is empty for png asset "' . $assetName . '".');
            }

            return ['image/jpeg', $jpegBytes];
        } finally {
            @unlink($inputPngPath);
            @unlink($outputJpegPath);
        }
    }

    private function findExecutableBinary(string $binaryName): string
    {
        $pathEnv = (string) getenv('PATH');
        $candidates = [];
        if ($pathEnv !== '') {
            foreach (explode(':', $pathEnv) as $dir) {
                $dir = trim($dir);
                if ($dir !== '') {
                    $candidates[] = rtrim($dir, '/') . '/' . $binaryName;
                }
            }
        }

        foreach ($candidates as $candidate) {
            if (is_string($candidate) && $candidate !== '' && is_file($candidate) && is_executable($candidate)) {
                return $candidate;
            }
        }
        return '';
    }
}
