<?php

namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
 * @codeCoverageIgnore
 */
class SeoService
{
    private const USER_AGENT = 'E-Cosplay-SEO-Checker/1.0';
    private const TIMEOUT = 10;

    public function __construct(
        private HttpClientInterface $httpClient,
        private LoggerInterface $logger,
    ) {
    }

    /**
     * Analyse les meta tags d'une page (title, description, og:*, canonical).
     *
     * @return array{title: ?string, description: ?string, og_title: ?string, og_description: ?string, og_image: ?string, canonical: ?string, issues: list<string>}
     */
    public function checkMetaTags(string $url): array
    {
        $result = [
            'title' => null,
            'description' => null,
            'og_title' => null,
            'og_description' => null,
            'og_image' => null,
            'canonical' => null,
            'issues' => [],
        ];

        $html = $this->fetchHtml($url);
        if (null === $html) {
            $result['issues'][] = 'Impossible de recuperer la page';

            return $result;
        }

        // Title
        if (preg_match('/<title[^>]*>(.*?)<\/title>/si', $html, $m)) {
            $result['title'] = trim(html_entity_decode($m[1], \ENT_QUOTES, 'UTF-8'));
        } else {
            $result['issues'][] = 'Balise <title> manquante';
        }

        if (null !== $result['title'] && \strlen($result['title']) > 60) {
            $result['issues'][] = 'Title trop long ('.\strlen($result['title']).' caracteres, max 60)';
        }

        // Meta description
        $result['description'] = $this->extractMetaTag($html, 'description');
        if (null === $result['description']) {
            $result['issues'][] = 'Meta description manquante';
        } elseif (\strlen($result['description']) > 160) {
            $result['issues'][] = 'Meta description trop longue ('.\strlen($result['description']).' caracteres, max 160)';
        }

        // Open Graph
        $result['og_title'] = $this->extractMetaTag($html, 'og:title', 'property');
        $result['og_description'] = $this->extractMetaTag($html, 'og:description', 'property');
        $result['og_image'] = $this->extractMetaTag($html, 'og:image', 'property');

        if (null === $result['og_title']) {
            $result['issues'][] = 'Meta og:title manquante';
        }
        if (null === $result['og_image']) {
            $result['issues'][] = 'Meta og:image manquante';
        }

        // Canonical
        if (preg_match('/<link[^>]+rel=["\']canonical["\'][^>]+href=["\']([^"\']+)["\']/i', $html, $m)) {
            $result['canonical'] = $m[1];
        } else {
            $result['issues'][] = 'Balise canonical manquante';
        }

        return $result;
    }

    /**
     * Verifie le fichier sitemap.xml.
     *
     * @return array{exists: bool, valid: bool, url_count: int, issues: list<string>}
     */
    public function checkSitemap(string $siteUrl): array
    {
        $result = ['exists' => false, 'valid' => false, 'url_count' => 0, 'issues' => []];
        $sitemapUrl = rtrim($siteUrl, '/').'/sitemap.xml';

        try {
            $response = $this->httpClient->request('GET', $sitemapUrl, [
                'timeout' => self::TIMEOUT,
                'headers' => ['User-Agent' => self::USER_AGENT],
            ]);

            if (200 !== $response->getStatusCode()) {
                $result['issues'][] = 'sitemap.xml retourne HTTP '.$response->getStatusCode();

                return $result;
            }

            $result['exists'] = true;
            $content = $response->getContent();

            libxml_use_internal_errors(true);
            $xml = simplexml_load_string($content);
            if (false === $xml) {
                $result['issues'][] = 'sitemap.xml invalide (XML malformed)';

                return $result;
            }

            $result['valid'] = true;
            $xml->registerXPathNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9');
            $urls = $xml->xpath('//sm:url');
            $result['url_count'] = false !== $urls ? \count($urls) : 0;

            if (0 === $result['url_count']) {
                // Essai sans namespace
                $urls = $xml->xpath('//url');
                $result['url_count'] = \count($urls);
            }
        } catch (\Throwable $e) {
            $result['issues'][] = 'Erreur acces sitemap.xml : '.$e->getMessage();
            $this->logger->error('SeoService::checkSitemap: '.$e->getMessage(), ['url' => $sitemapUrl]);
        }

        return $result;
    }

    /**
     * Verifie le fichier robots.txt.
     *
     * @return array{exists: bool, content: string, has_sitemap: bool, has_disallow: bool, issues: list<string>}
     */
    public function checkRobotsTxt(string $siteUrl): array
    {
        $result = ['exists' => false, 'content' => '', 'has_sitemap' => false, 'has_disallow' => false, 'issues' => []];
        $robotsUrl = rtrim($siteUrl, '/').'/robots.txt';

        try {
            $response = $this->httpClient->request('GET', $robotsUrl, [
                'timeout' => self::TIMEOUT,
                'headers' => ['User-Agent' => self::USER_AGENT],
            ]);

            if (200 !== $response->getStatusCode()) {
                $result['issues'][] = 'robots.txt retourne HTTP '.$response->getStatusCode();

                return $result;
            }

            $result['exists'] = true;
            $result['content'] = $response->getContent();
            $result['has_sitemap'] = (bool) preg_match('/^Sitemap:/mi', $result['content']);
            $result['has_disallow'] = (bool) preg_match('/^Disallow:/mi', $result['content']);

            if (!$result['has_sitemap']) {
                $result['issues'][] = 'robots.txt ne reference pas de Sitemap';
            }
        } catch (\Throwable $e) {
            $result['issues'][] = 'Erreur acces robots.txt : '.$e->getMessage();
        }

        return $result;
    }

    /**
     * Verifie la presence du meta viewport (responsive).
     *
     * @return array{has_viewport: bool, viewport_content: ?string, issues: list<string>}
     */
    public function checkMobileResponsive(string $url): array
    {
        $result = ['has_viewport' => false, 'viewport_content' => null, 'issues' => []];

        $html = $this->fetchHtml($url);
        if (null === $html) {
            $result['issues'][] = 'Impossible de recuperer la page';

            return $result;
        }

        $viewport = $this->extractMetaTag($html, 'viewport');
        if (null !== $viewport) {
            $result['has_viewport'] = true;
            $result['viewport_content'] = $viewport;
        } else {
            $result['issues'][] = 'Meta viewport manquante (site non responsive)';
        }

        return $result;
    }

    /**
     * Mesure le temps de reponse HTTP.
     *
     * @return array{load_time_ms: int, status_code: int, issues: list<string>}
     */
    public function checkPageSpeed(string $url): array
    {
        $result = ['load_time_ms' => 0, 'status_code' => 0, 'issues' => []];

        try {
            $start = microtime(true);
            $response = $this->httpClient->request('GET', $url, [
                'timeout' => 30,
                'headers' => ['User-Agent' => self::USER_AGENT],
            ]);
            $response->getContent();
            $result['load_time_ms'] = (int) ((microtime(true) - $start) * 1000);
            $result['status_code'] = $response->getStatusCode();

            if ($result['load_time_ms'] > 3000) {
                $result['issues'][] = 'Page lente ('.$result['load_time_ms'].'ms, seuil recommande 3s)';
            }
        } catch (\Throwable $e) {
            $result['issues'][] = 'Erreur chargement : '.$e->getMessage();
        }

        return $result;
    }

    /**
     * Verifie le certificat SSL.
     *
     * @return array{is_https: bool, valid: bool, issuer: ?string, expires: ?string, days_remaining: ?int, issues: list<string>}
     */
    public function checkSslCertificate(string $url): array
    {
        $result = ['is_https' => false, 'valid' => false, 'issuer' => null, 'expires' => null, 'days_remaining' => null, 'issues' => []];

        $parsed = parse_url($url);
        if (!isset($parsed['host']) || 'https' !== ($parsed['scheme'] ?? '')) {
            $result['issues'][] = !isset($parsed['host']) ? 'URL invalide' : 'Site non HTTPS';
            $result['is_https'] = isset($parsed['host']) && 'https' === ($parsed['scheme'] ?? '');

            return $result;
        }

        $result['is_https'] = true;

        try {
            $this->parseSslCertificate($parsed['host'], $parsed['port'] ?? 443, $result);
        } catch (\Throwable $e) {
            $result['issues'][] = 'Erreur verification SSL : '.$e->getMessage();
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $result
     */
    private function parseSslCertificate(string $host, int|string $port, array &$result): void
    {
        $context = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false]]);
        $socket = @stream_socket_client('ssl://'.$host.':'.$port, $errno, $errstr, 10, \STREAM_CLIENT_CONNECT, $context);

        if (false === $socket) {
            $result['issues'][] = 'Connexion SSL impossible : '.$errstr;

            return;
        }

        $params = stream_context_get_params($socket);
        fclose($socket);

        $cert = $params['options']['ssl']['peer_certificate'] ?? null;
        $certInfo = null !== $cert ? openssl_x509_parse($cert) : false;

        if (false === $certInfo) {
            $result['issues'][] = null === $cert ? 'Certificat non recuperable' : 'Certificat non parsable';

            return;
        }

        $result['valid'] = true;
        $result['issuer'] = $certInfo['issuer']['O'] ?? $certInfo['issuer']['CN'] ?? null;
        $result['expires'] = date('Y-m-d', $certInfo['validTo_time_t']);
        $result['days_remaining'] = (int) (($certInfo['validTo_time_t'] - time()) / 86400);

        if ($result['days_remaining'] < 0) {
            $result['valid'] = false;
            $result['issues'][] = 'Certificat expire depuis '.abs($result['days_remaining']).' jours';
        } elseif ($result['days_remaining'] < 30) {
            $result['issues'][] = 'Certificat expire dans '.$result['days_remaining'].' jours';
        }
    }

    /**
     * Verifie les headers de securite.
     *
     * @return array{headers: array<string, string>, missing: list<string>, issues: list<string>}
     */
    public function checkHeaders(string $url): array
    {
        $result = ['headers' => [], 'missing' => [], 'issues' => []];
        $expected = ['X-Frame-Options', 'Content-Security-Policy', 'Strict-Transport-Security', 'X-Content-Type-Options'];

        try {
            $response = $this->httpClient->request('HEAD', $url, [
                'timeout' => self::TIMEOUT,
                'headers' => ['User-Agent' => self::USER_AGENT],
            ]);

            $responseHeaders = $response->getHeaders(false);
            foreach ($expected as $header) {
                $lower = strtolower($header);
                if (isset($responseHeaders[$lower])) {
                    $result['headers'][$header] = implode(', ', $responseHeaders[$lower]);
                } else {
                    $result['missing'][] = $header;
                    $result['issues'][] = 'Header '.$header.' manquant';
                }
            }
        } catch (\Throwable $e) {
            $result['issues'][] = 'Erreur verification headers : '.$e->getMessage();
        }

        return $result;
    }

    /**
     * Demande a Google de re-indexer une URL (purge cache + reindex).
     * Utilise GoogleSearchService::submitUrlForIndexing en interne.
     *
     * @return array{success: bool, url: string, message: string}
     */
    public function purgeAndReindex(string $url, GoogleSearchService $googleSearch): array
    {
        if (!$googleSearch->isAvailable()) {
            return ['success' => false, 'url' => $url, 'message' => 'Google Search Console non configure'];
        }

        $result = $googleSearch->submitUrlForIndexing($url);

        if ($result) {
            $this->logger->info('SeoService::purgeAndReindex: URL soumise pour reindexation', ['url' => $url]);

            return ['success' => true, 'url' => $url, 'message' => 'URL soumise a Google pour reindexation'];
        }

        $this->logger->error('SeoService::purgeAndReindex: echec soumission URL', ['url' => $url]);

        return ['success' => false, 'url' => $url, 'message' => 'Echec de la soumission a Google Indexing API'];
    }

    /**
     * Purge et reindexe toutes les URLs d'un sitemap.
     *
     * @return array{total: int, success: int, failed: int, results: list<array{success: bool, url: string, message: string}>}
     */
    public function purgeAllFromSitemap(string $siteUrl, GoogleSearchService $googleSearch): array
    {
        $sitemap = $this->checkSitemap($siteUrl);
        $results = ['total' => 0, 'success' => 0, 'failed' => 0, 'results' => []];

        if (!$sitemap['exists'] || !$sitemap['valid']) {
            return $results;
        }

        $sitemapUrl = rtrim($siteUrl, '/').'/sitemap.xml';

        try {
            $response = $this->httpClient->request('GET', $sitemapUrl, [
                'timeout' => self::TIMEOUT,
                'headers' => ['User-Agent' => self::USER_AGENT],
            ]);

            $xml = simplexml_load_string($response->getContent());
            if (false === $xml) {
                return $results;
            }

            $xml->registerXPathNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9');
            $urls = $xml->xpath('//sm:url/sm:loc') ?: $xml->xpath('//url/loc') ?: [];

            foreach ($urls as $loc) {
                $pageUrl = (string) $loc;
                $r = $this->purgeAndReindex($pageUrl, $googleSearch);
                $results['results'][] = $r;
                ++$results['total'];
                $r['success'] ? ++$results['success'] : ++$results['failed'];
            }
        } catch (\Throwable $e) {
            $this->logger->error('SeoService::purgeAllFromSitemap: '.$e->getMessage());
        }

        return $results;
    }

    /**
     * Score SEO global (0-100) base sur tous les checks.
     *
     * @return array{score: int, details: array<string, mixed>}
     */
    public function getPageScore(string $url): array
    {
        $parsed = parse_url($url);
        $siteUrl = ($parsed['scheme'] ?? 'https').'://'.($parsed['host'] ?? '');

        $meta = $this->checkMetaTags($url);
        $sitemap = $this->checkSitemap($siteUrl);
        $robots = $this->checkRobotsTxt($siteUrl);
        $mobile = $this->checkMobileResponsive($url);
        $speed = $this->checkPageSpeed($url);
        $ssl = $this->checkSslCertificate($url);
        $headers = $this->checkHeaders($url);

        $score = 100;

        // Meta tags (30 points max)
        if (null === $meta['title']) {
            $score -= 10;
        }
        if (null === $meta['description']) {
            $score -= 10;
        }
        if (null === $meta['og_title']) {
            $score -= 5;
        }
        if (null === $meta['canonical']) {
            $score -= 5;
        }

        // Sitemap + robots (15 points)
        if (!$sitemap['exists']) {
            $score -= 10;
        }
        if (!$robots['exists']) {
            $score -= 5;
        }

        // Mobile (10 points)
        if (!$mobile['has_viewport']) {
            $score -= 10;
        }

        // Speed (15 points)
        if ($speed['load_time_ms'] > 5000) {
            $score -= 15;
        } elseif ($speed['load_time_ms'] > 3000) {
            $score -= 10;
        } elseif ($speed['load_time_ms'] > 1500) {
            $score -= 5;
        }

        // SSL (15 points)
        if (!$ssl['is_https']) {
            $score -= 15;
        } elseif (!$ssl['valid']) {
            $score -= 10;
        }

        // Headers (15 points)
        $score -= \count($headers['missing']) * 4;

        return [
            'score' => max(0, $score),
            'details' => [
                'meta' => $meta,
                'sitemap' => $sitemap,
                'robots' => $robots,
                'mobile' => $mobile,
                'speed' => $speed,
                'ssl' => $ssl,
                'headers' => $headers,
            ],
        ];
    }

    private function fetchHtml(string $url): ?string
    {
        try {
            $response = $this->httpClient->request('GET', $url, [
                'timeout' => self::TIMEOUT,
                'headers' => ['User-Agent' => self::USER_AGENT],
            ]);

            if (200 !== $response->getStatusCode()) {
                return null;
            }

            return $response->getContent();
        } catch (\Throwable $e) {
            $this->logger->error('SeoService::fetchHtml: '.$e->getMessage(), ['url' => $url]);

            return null;
        }
    }

    private function extractMetaTag(string $html, string $name, string $attribute = 'name'): ?string
    {
        $pattern = '/<meta[^>]+'.preg_quote($attribute, '/').'=["\']'.preg_quote($name, '/').
            '["\'][^>]+content=["\']([^"\']*)["\'][^>]*>/i';

        if (preg_match($pattern, $html, $m)) {
            return trim(html_entity_decode($m[1], \ENT_QUOTES, 'UTF-8'));
        }

        // Ordre inverse : content avant name/property
        $pattern2 = '/<meta[^>]+content=["\']([^"\']*)["\'][^>]+'.preg_quote($attribute, '/').'=["\']'.preg_quote($name, '/').
            '["\'][^>]*>/i';

        if (preg_match($pattern2, $html, $m)) {
            return trim(html_entity_decode($m[1], \ENT_QUOTES, 'UTF-8'));
        }

        return null;
    }
}
