<?php
namespace Crawl\Services;

use Crawl\Repositories\CrawlImageRepository;
use Crawl\Repositories\CrawlLinkRepository;
use Crawl\Repositories\CrawlPageRepository;
use Crawl\Repositories\CrawlQueueRepository;
use Crawl\Repositories\CrawlRunRepository;
use Crawl\Services\Crawler\CurlMultiFetcher;
use Crawl\Services\Crawler\HtmlParser;
use Crawl\Services\Crawler\UrlNormalizer;

class CrawlWorkerService
{
    private CrawlRunRepository $runs;
    private CrawlQueueRepository $queue;
    private CrawlPageRepository $pages;
    private CrawlLinkRepository $links;
    private CrawlImageRepository $images;
    private UrlNormalizer $normalizer;
    private CurlMultiFetcher $fetcher;
    private HtmlParser $parser;
    private array $config;

    public function __construct()
    {
        $this->runs = new CrawlRunRepository();
        $this->queue = new CrawlQueueRepository();
        $this->pages = new CrawlPageRepository();
        $this->links = new CrawlLinkRepository();
        $this->images = new CrawlImageRepository();
        $this->normalizer = new UrlNormalizer();
        $this->fetcher = new CurlMultiFetcher();
        $this->parser = new HtmlParser();
        $this->config = require ROOT_PATH . '/config/app.php';
    }

    public function tick(): int
    {
        $processed = 0;
        $runs = $this->runs->allRunning();

        foreach ($runs as $run) {
            $processed += $this->processRun($run);
        }

        return $processed;
    }

    private function processRun(array $run): int
    {
        $runId = (int)$run['id'];
        $maxPages = (int)$run['max_pages'];
        $maxDepth = (int)$run['max_depth'];
        $concurrency = max(1, (int)$run['concurrency']);

        if ($maxPages > 0 && (int)$run['total_pages'] >= $maxPages) {
            $this->runs->syncTotalPagesFromRows($runId);
            $this->runs->updateStatus($runId, 'completed');
            return 0;
        }

        $batch = $this->queue->claimBatch($runId, $concurrency);
        if (empty($batch)) {
            if ($this->queue->pendingCount($runId) === 0) {
                $this->runs->syncTotalPagesFromRows($runId);
                $this->runs->updateStatus($runId, 'completed');
            }
            return 0;
        }

        $urls = array_map(static fn(array $item): string => $item['url'], $batch);
        $userAgent = $run['user_agent'] ?? $this->config['crawler']['user_agent'];
        $responses = $this->fetcher->fetchMany(
            $urls,
            $this->config['crawler']['timeout_seconds'],
            $userAgent
        );

        $processed = 0;

        foreach ($batch as $idx => $item) {
            $queueId = (int)$item['id'];
            $depth = (int)$item['depth'];
            $url = $item['url'];
            $res = $responses[$idx] ?? null;

            if ($res === null || $res['error']) {
                $this->queue->markFailed($queueId, $res['error'] ?? 'Unknown fetch error');
                continue;
            }

            try {
                $contentType = strtolower((string)($res['content_type'] ?? ''));
                $isHtml = str_contains($contentType, 'text/html');
                $statusCode = (int)($res['status_code'] ?? 0);
                $body = (string)($res['body'] ?? '');
                $effectiveUrl = (string)($res['effective_url'] ?? '');

                if ($this->isFetchFailure($statusCode, $effectiveUrl, $body, $isHtml)) {
                    $this->queue->markFailed($queueId, 'Invalid HTTP response: status=' . $statusCode);
                    continue;
                }

                $parsed = [
                    'title' => null,
                    'title_length' => 0,
                    'meta_description' => null,
                    'meta_description_length' => 0,
                    'h1' => null,
                    'h2_list' => [],
                    'h3_list' => [],
                    'canonical_url' => null,
                    'meta_robots' => null,
                    'json_ld_schema' => [],
                    'links' => [],
                    'images' => [],
                    'visible_text' => '',
                ];

                if ($isHtml) {
                    $parsed = $this->parser->parse($res['body'], $res['effective_url'], $this->normalizer);
                }

                $visibleText    = $parsed['visible_text'] ?? '';
                $wordCount      = $visibleText === '' ? 0 : str_word_count($visibleText);
                $contentHash    = $visibleText === '' ? null : sha1($visibleText);
                $nearHash       = $visibleText === '' ? null : $this->simhash($visibleText);
                $textMetrics    = $this->textMetrics($visibleText);
                $bodyLen        = strlen($body);
                $textRatio      = ($bodyLen > 0 && $visibleText !== '')
                                    ? round((strlen($visibleText) / $bodyLen) * 100, 2)
                                    : 0.0;
                $normalizedEffectiveUrl = $this->normalizer->normalize($res['effective_url']) ?? $item['normalized_url'];

                // Detect redirect: if effective URL differs from queued URL, record the chain
                $originalUrl        = $item['url'];
                $redirectUrl        = ($effectiveUrl !== $originalUrl && $statusCode === 200) ? null : null;
                $redirectChainLen   = 0;
                if ($effectiveUrl !== $originalUrl) {
                    $redirectUrl      = $effectiveUrl;
                    $redirectChainLen = 1; // cURL followed; exact hop count not available without manual chain
                }

                $this->pages->upsert($runId, [
                    'url'                      => $effectiveUrl,
                    'normalized_url'           => $normalizedEffectiveUrl,
                    'status_code'              => $statusCode,
                    'content_type'             => $res['content_type'],
                    'title'                    => $parsed['title'],
                    'title_length'             => (int)$parsed['title_length'],
                    'meta_description'         => $parsed['meta_description'],
                    'meta_description_length'  => (int)$parsed['meta_description_length'],
                    'h1'                       => $parsed['h1'],
                    'h2_list'                  => json_encode($parsed['h2_list'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                    'h3_list'                  => json_encode($parsed['h3_list'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                    'canonical_url'            => $parsed['canonical_url'],
                    'meta_robots'              => $parsed['meta_robots'],
                    'x_robots_tag'             => $res['headers']['x-robots-tag'] ?? null,
                    'word_count'               => $wordCount,
                    'response_time'            => $res['response_time'],
                    'depth'                    => $depth,
                    'content_hash'             => $contentHash,
                    'near_duplicate_hash'      => $nearHash,
                    'json_ld_schema'           => json_encode($parsed['json_ld_schema'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                    'redirect_url'             => $redirectUrl,
                    'redirect_chain_length'    => $redirectChainLen,
                    'content_length'           => isset($res['headers']['content-length']) ? (int)$res['headers']['content-length'] : null,
                    'transferred_bytes'        => $bodyLen > 0 ? $bodyLen : null,
                    'sentence_count'           => $textMetrics['sentence_count'],
                    'flesch_reading_ease'      => $textMetrics['flesch'],
                    'text_ratio'               => $textRatio,
                    'last_modified'            => $res['headers']['last-modified'] ?? null,
                    'redirect_type'            => $res['redirect_type'] ?? null,
                    'http_version'             => $res['http_version'] ?? 'HTTP/1.1',
                ]);

                $linkRows = [];
                foreach ($parsed['links'] as $link) {
                    $to = $link['to_url'];
                    $isInternal = $this->normalizer->isInternal($run['domain'], $to);
                    $nofollow = str_contains($link['rel'] ?? '', 'nofollow');

                    $linkRows[] = [
                        'from_url'     => $effectiveUrl,
                        'to_url'       => $to,
                        'anchor_text'  => $link['anchor_text']  ?? '',
                        'link_context' => $link['link_context'] ?? 'Body',
                        'is_internal'  => $isInternal,
                        'is_nofollow'  => $nofollow,
                    ];

                    if ($isInternal && $depth + 1 <= $maxDepth && ($maxPages === 0 || (int)$run['total_pages'] + $processed < $maxPages)) {
                        $normalized = $this->normalizer->normalize($to);
                        if ($normalized !== null) {
                            $this->queue->enqueue($runId, $to, $normalized, $depth + 1);
                        }
                    }
                }

                $this->links->insertMany($runId, $linkRows);

                $this->images->clearByPage($runId, $effectiveUrl);
                // Enrich images with file_size from Content-Length if fetched as resource
                $enrichedImages = $parsed['images'];
                $this->images->insertMany($runId, $effectiveUrl, $enrichedImages);

                $this->queue->markDone($queueId);
                $processed++;
            } catch (\Throwable $e) {
                $this->queue->markFailed($queueId, $e->getMessage());
            }
        }

        if ($processed > 0) {
            $this->runs->syncTotalPagesFromRows($runId);
        }

        if ($this->queue->pendingCount($runId) === 0) {
            $this->runs->syncTotalPagesFromRows($runId);
            $this->runs->updateStatus($runId, 'completed');
        }

        return $processed;
    }

    private function isFetchFailure(int $statusCode, string $effectiveUrl, string $body, bool $isHtml): bool
    {
        if ($statusCode <= 0) {
            return true;
        }
        if ($effectiveUrl === '') {
            return true;
        }
        if ($isHtml && trim($body) === '') {
            return true;
        }

        return false;
    }

    private function textMetrics(string $text): array
    {
        if (trim($text) === '') return ['sentence_count' => 0, 'flesch' => 0.0];

        $sentences = array_filter(
            preg_split('/[.!?]+\s*/u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: [],
            fn($s) => str_word_count(trim($s)) >= 3
        );
        $sentenceCount = max(1, count($sentences));

        $words     = preg_split('/\s+/', trim($text), -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $wordCount = max(1, count($words));
        $syllables = max(1, array_sum(array_map([$this, 'countSyllables'], $words)));

        $flesch = 206.835 - (1.015 * ($wordCount / $sentenceCount)) - (84.6 * ($syllables / $wordCount));
        $flesch = (float)max(0, min(100, round($flesch, 1)));

        return ['sentence_count' => $sentenceCount, 'flesch' => $flesch];
    }

    private function countSyllables(string $word): int
    {
        $word = strtolower(preg_replace('/[^a-z]/i', '', $word));
        if (strlen($word) <= 3) return 1;
        $word  = preg_replace('/(?:[^laeiouy]es|ed|[^laeiouy]e)$/', '', $word);
        $word  = preg_replace('/^y/', '', $word);
        $count = (int)preg_match_all('/[aeiouy]{1,2}/', $word);
        return max(1, $count);
    }

    private function simhash(string $text): string
    {
        $tokens = preg_split('/\W+/', strtolower($text), -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $weights = array_fill(0, 64, 0);

        foreach ($tokens as $token) {
            $hash = hash('sha256', $token);
            $bin = str_pad(base_convert(substr($hash, 0, 16), 16, 2), 64, '0', STR_PAD_LEFT);
            for ($i = 0; $i < 64; $i++) {
                $weights[$i] += ($bin[$i] === '1') ? 1 : -1;
            }
        }

        $out = '';
        for ($i = 0; $i < 64; $i++) {
            $out .= $weights[$i] >= 0 ? '1' : '0';
        }

        return $out;
    }
}
