<?php
namespace Crawl\Controllers\Api;

use Crawl\Lib\Response;
use Crawl\Lib\Validator;
use Crawl\Repositories\CrawlRunRepository;
use Crawl\Repositories\OAuthTokenRepository;
use Crawl\Repositories\PageCategoryRepository;
use Crawl\Services\AuditService;
use Crawl\Services\CrawlManagerService;
use Crawl\Services\CrawlWorkerService;
use Crawl\Services\ExportService;
use Crawl\Services\GA4Service;
use Crawl\Services\GoogleOAuthService;
use Crawl\Services\GSCService;
use Crawl\Services\GBPService;
use Crawl\Services\PageSpeedService;

class CrawlController
{
    private CrawlManagerService $manager;
    private CrawlRunRepository $runs;
    private AuditService $audit;
    private ExportService $export;
    private CrawlWorkerService $worker;

    public function __construct()
    {
        $this->manager = new CrawlManagerService();
        $this->runs = new CrawlRunRepository();
        $this->audit = new AuditService();
        $this->export = new ExportService();
        $this->worker = new CrawlWorkerService();
    }

    public function startCrawl(int $projectId, array $body): void
    {
        $config = require ROOT_PATH . '/config/app.php';
        $defaults = $config['crawl_defaults'];

        $domain = (string)($body['domain'] ?? '');
        $maxDepth = Validator::int($body['max_depth'] ?? null, $defaults['max_depth'], 0, 8);
        $maxPages = Validator::int($body['max_pages'] ?? null, $defaults['max_pages'], 0, 10000000);
        $concurrency = Validator::int($body['concurrency'] ?? null, $defaults['concurrency'], 1, 20);
        $userAgent = trim((string)($body['user_agent'] ?? '')) ?: $config['crawler']['user_agent'];

        $run = $this->manager->startRun($projectId, $domain, $maxDepth, $maxPages, $concurrency, $userAgent);
        Response::success($run, 'Crawl started', 201);
    }

    public function getRun(int $projectId, int $runId): void
    {
        $run = $this->runs->findById($runId, $projectId);
        if (!$run) {
            Response::error('Crawl run not found', 404);
        }
        Response::success($run);
    }

    public function listRuns(int $projectId): void
    {
        $runs = $this->runs->listByProject($projectId, 100);
        Response::success($runs);
    }

    public function latestStatus(int $projectId): void
    {
        $run = $this->runs->latestForProject($projectId);
        if (!$run) {
            Response::error('No crawl run found', 404);
        }

        $runId = (int)$run['id'];
        $pending = $this->runs->countPendingQueue($runId);

        // Auto-process one worker tick during polling so crawl progress works
        // even when no background worker process is running.
        if ($run['status'] === 'running' && $pending > 0) {
            $this->worker->tick();
            $run = $this->runs->findById($runId, $projectId) ?? $run;
            $pending = $this->runs->countPendingQueue($runId);
        }

        $failed = $this->runs->countFailedQueue($runId);
        Response::success([
            'crawl_run_id' => $runId,
            'status' => $run['status'],
            'domain' => $run['domain'],
            'total_pages' => (int)$run['total_pages'],
            'pending_queue' => $pending,
            'failed_queue' => $failed,
            'started_at' => $run['started_at'],
            'finished_at' => $run['finished_at'],
            'error_message' => $run['error_message'],
        ]);
    }

    private function getRunForProject(int $projectId, int $runId): array
    {
        $run = $this->runs->findById($runId, $projectId);
        if (!$run) {
            Response::error('Crawl run not found', 404);
        }
        return $run;
    }

    public function websiteAudit(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) {
            Response::error('crawl_run_id is required', 422);
        }

        $this->getRunForProject($projectId, $runId);

        $limit = Validator::int($query['limit'] ?? null, 25, 1, 500);
        $offset = Validator::int($query['offset'] ?? null, 0, 0, 1000000);
        $sortBy = (string)($query['sort_by'] ?? 'url');
        $sortDir = (string)($query['sort_dir'] ?? 'ASC');

        $filters = [
            'search'          => $query['search']          ?? null,
            'status_code'     => $query['status_code']     ?? null,
            'status_code_min' => $query['status_code_min'] ?? null,
            'status_code_max' => $query['status_code_max'] ?? null,
            'indexability'    => $query['indexability']    ?? null,
            'content_type'    => $query['content_type']    ?? null,
            'type_filter'     => $query['type_filter']     ?? null,
            'hide_noise'      => $query['hide_noise']      ?? null,
            'depth_min'       => $query['depth_min']       ?? null,
            'depth_max'       => $query['depth_max']       ?? null,
            'word_count_min'  => $query['word_count_min']  ?? null,
            'word_count_max'  => $query['word_count_max']  ?? null,
        ];

        Response::success($this->audit->websiteAudit($projectId, $runId, $filters, $limit, $offset, $sortBy, $sortDir));
    }

    public function technicalIssues(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) {
            Response::error('crawl_run_id is required', 422);
        }

        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->technicalIssues($runId));
    }

    public function technicalDetail(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) {
            Response::error('crawl_run_id is required', 422);
        }

        $this->getRunForProject($projectId, $runId);
        $limit  = Validator::int($query['limit']  ?? null, 25, 1, 500);
        $offset = Validator::int($query['offset'] ?? null, 0, 0, 1000000);
        Response::success($this->audit->technicalDetail($runId, $limit, $offset));
    }

    public function websiteLevelSummary(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) {
            Response::error('crawl_run_id is required', 422);
        }

        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->websiteLevelSummary($runId));
    }

    public function pageDetail(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        $url = (string)($query['url'] ?? '');

        if ($runId <= 0 || $url === '') {
            Response::error('crawl_run_id and url are required', 422);
        }

        $this->getRunForProject($projectId, $runId);

        $data = $this->audit->pageDetail($runId, $url);
        if (!$data) {
            Response::error('Page not found', 404);
        }

        Response::success($data);
    }

    public function tabs(int $projectId, int $runId, array $query): void
    {
        $this->getRunForProject($projectId, $runId);

        $tab = (string)($query['tab'] ?? 'all_urls');
        $limit = Validator::int($query['limit'] ?? null, 25, 1, 500);
        $offset = Validator::int($query['offset'] ?? null, 0, 0, 1000000);
        $rows = $this->audit->tabData($runId, $tab, $limit, $offset);

        Response::success([
            'crawl_run_id' => $runId,
            'tab' => $tab,
            'rows' => $rows,
            'limit' => $limit,
            'offset' => $offset,
        ]);
    }

    public function fullAudit(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);

        $limit   = Validator::int($query['limit'] ?? null, 200, 10, 1000);
        $filters = [
            'search'          => $query['search']          ?? null,
            'status_code_min' => $query['status_code_min'] ?? null,
            'status_code_max' => $query['status_code_max'] ?? null,
            'type_filter'     => $query['type_filter']     ?? null,
            'indexability'    => $query['indexability']    ?? null,
            'hide_noise'      => $query['hide_noise']      ?? null,
        ];
        Response::success($this->audit->fullAudit($projectId, $runId, $filters, $limit));
    }

    public function healthScore(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->healthScore($runId));
    }

    public function duplicateGroups(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->duplicateGroups($runId));
    }

    public function canonicalAudit(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->canonicalAudit($runId));
    }

    public function headingAudit(int $projectId, array $query): void
    {
        $runId  = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);
        $limit  = Validator::int($query['limit']  ?? null, 25, 1, 500);
        $offset = Validator::int($query['offset'] ?? null, 0, 0, 1000000);
        Response::success($this->audit->headingAudit($runId, $limit, $offset));
    }

    public function linkEquity(int $projectId, array $query): void
    {
        $runId  = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);
        $limit  = Validator::int($query['limit']  ?? null, 25, 1, 500);
        $offset = Validator::int($query['offset'] ?? null, 0, 0, 1000000);
        Response::success($this->audit->linkEquity($runId, $limit, $offset));
    }

    public function crawlBudget(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->crawlBudget($runId));
    }

    public function siteHealth(int $projectId, array $query): void
    {
        $runId  = (int)($query['crawl_run_id'] ?? 0);
        $domain = (string)($query['domain'] ?? '');
        if ($runId <= 0 || $domain === '') { Response::error('crawl_run_id and domain are required', 422); }
        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->siteHealth($domain, $runId));
    }

    public function crawlComparison(int $projectId, array $query): void
    {
        $runA = (int)($query['run_a'] ?? 0);
        $runB = (int)($query['run_b'] ?? 0);
        if ($runA <= 0 || $runB <= 0) { Response::error('run_a and run_b are required', 422); }
        Response::success($this->audit->crawlComparison($runA, $runB));
    }

    public function schemaValidation(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id is required', 422); }
        $this->getRunForProject($projectId, $runId);
        Response::success($this->audit->schemaValidation($runId));
    }

    // ── Page categories ────────────────────────────────────────────���─────

    public function listCategories(int $projectId): void
    {
        $repo = new PageCategoryRepository();
        $repo->seedDefaults($projectId);
        Response::success($repo->allForProject($projectId));
    }

    public function createCategory(int $projectId, array $body): void
    {
        if (empty($body['name'])) { Response::error('name is required', 422); }
        $repo = new PageCategoryRepository();
        $id   = $repo->create($projectId, $body);
        Response::success($repo->allForProject($projectId), 'Category created', 201);
    }

    public function updateCategory(int $projectId, int $catId, array $body): void
    {
        if (empty($body['name'])) { Response::error('name is required', 422); }
        $repo = new PageCategoryRepository();
        $repo->update($catId, $projectId, $body);
        Response::success($repo->allForProject($projectId));
    }

    public function healthSummary(): void
    {
        $rows   = (new \Crawl\Repositories\ProjectRepository())->allWithHealth();
        $result = array_map(function (array $row): array {
            $score = \Crawl\Repositories\ProjectRepository::computeHealthScore($row);
            return [
                'id'            => (int)$row['id'],
                'business_name' => $row['business_name'],
                'website_url'   => $row['website_url'] ?? '',
                'latest_run_id' => $row['latest_run_id'] ? (int)$row['latest_run_id'] : null,
                'crawl_total'   => (int)($row['crawl_total'] ?? 0),
                'crawl_status'  => $row['crawl_status'] ?? null,
                'finished_at'   => $row['finished_at']  ?? null,
                'crawl_domain'  => $row['crawl_domain'] ?? '',
                'health_score'  => $score,
                'error_pages'   => (int)($row['error_pages']   ?? 0),
                'missing_title' => (int)($row['missing_title'] ?? 0),
                'thin_content'  => (int)($row['thin_content']  ?? 0),
            ];
        }, $rows);
        Response::success($result);
    }

    public function autoDiff(int $projectId): void
    {
        $runs      = $this->runs->listByProject($projectId, 10);
        $completed = array_values(array_filter($runs, fn($r) => $r['status'] === 'completed'));

        if (count($completed) < 2) {
            Response::success(['available' => false]);
            return;
        }

        $diff              = (new \Crawl\Services\CrawlComparisonService())->compare(
            (int)$completed[1]['id'],  // older run
            (int)$completed[0]['id']   // newer run
        );
        $diff['available'] = true;
        $diff['run_new']   = (int)$completed[0]['id'];
        $diff['run_old']   = (int)$completed[1]['id'];
        Response::success($diff);
    }

    public function deleteCategory(int $projectId, int $catId): void
    {
        (new PageCategoryRepository())->delete($catId, $projectId);
        Response::success(null, 'Deleted');
    }

    public function pageSpeed(int $projectId, array $query): void
    {
        $url   = (string)($query['url']          ?? '');
        $runId = (int)($query['crawl_run_id']     ?? 0);
        if ($url === '') { Response::error('url is required', 422); return; }

        $svc = new PageSpeedService();
        if (!$svc->isConfigured()) {
            Response::error('PAGESPEED_API_KEY not set in .env — add it to enable PageSpeed tests', 501);
            return;
        }

        set_time_limit(120);
        $result = $svc->analyseUrl($url);

        // Persist if a crawl_run_id was provided
        if ($runId > 0) {
            $repo = new \Crawl\Repositories\PageSpeedRepository();
            foreach (['mobile', 'desktop'] as $strategy) {
                $d = $result[$strategy] ?? [];
                if (empty($d) || isset($d['error'])) continue;
                $sc = $d['scores']      ?? [];
                $lm = $d['lab_metrics'] ?? [];
                $repo->upsert($runId, $projectId, $url, $strategy, [
                    'score'         => $sc['performance']   ?? null,
                    'score_seo'     => $sc['seo']           ?? null,
                    'score_a11y'    => $sc['accessibility'] ?? null,
                    'score_bp'      => $sc['best_practices']?? null,
                    'fcp'           => $lm['fcp_ms'] ? round($lm['fcp_ms']/1000, 3) : null,
                    'lcp'           => $lm['lcp_ms'] ? round($lm['lcp_ms']/1000, 3) : null,
                    'tbt'           => $lm['tbt_ms'] ? round($lm['tbt_ms']/1000, 3) : null,
                    'cls'           => isset($lm['cls']) ? (float)$lm['cls'] : null,
                    'si'            => $lm['si_ms']  ? round($lm['si_ms']/1000, 3)  : null,
                    'tti'           => $lm['tti_ms'] ? round($lm['tti_ms']/1000, 3) : null,
                    'opportunities' => $d['opportunities'] ?? [],
                ]);
            }
        }

        Response::success($result);
    }

    public function pageSpeedStored(int $projectId, array $query): void
    {
        $runId = (int)($query['crawl_run_id'] ?? 0);
        if ($runId <= 0) { Response::error('crawl_run_id required', 422); return; }
        $repo = new \Crawl\Repositories\PageSpeedRepository();
        Response::success($repo->forRun($runId, $projectId));
    }

    public function export(int $projectId, int $runId, array $query): void
    {
        $this->getRunForProject($projectId, $runId);

        $tab = (string)($query['tab'] ?? 'all_urls');
        $fmt = strtolower((string)($query['fmt'] ?? 'csv'));
        $rows = $this->audit->tabData($runId, $tab, 10000, 0);

        if ($fmt === 'json') {
            Response::download(
                json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                "crawl-{$runId}-{$tab}.json",
                'application/json'
            );
        }

        if ($fmt === 'csv') {
            Response::download(
                $this->export->toCsv($rows),
                "crawl-{$runId}-{$tab}.csv",
                'text/csv'
            );
        }

        Response::error('Invalid export format', 422);
    }

    // ── Performance: GA4 ─────────────────────────────────────────────────────

    public function performanceOverview(int $projectId, array $query): void
    {
        $project    = (new \Crawl\Repositories\ProjectRepository())->find($projectId);
        if (!$project) { Response::error('Project not found', 404); return; }

        $propertyId = $project['ga4_property_id'] ?? '';
        if (!$propertyId) { Response::success(['error' => 'no_property_id']); return; }

        $days = (int)($query['days'] ?? 30);
        Response::success((new GA4Service())->getOverview($projectId, $propertyId, $days));
    }

    public function performanceTopPages(int $projectId, array $query): void
    {
        $project    = (new \Crawl\Repositories\ProjectRepository())->find($projectId);
        if (!$project) { Response::error('Project not found', 404); return; }

        $days       = (int)($query['days'] ?? 30);
        $limit      = (int)($query['limit'] ?? 50);
        $propertyId = $project['ga4_property_id'] ?? '';
        $siteUrl    = $project['gsc_site_url']    ?? '';

        $ga4Pages   = $propertyId ? (new GA4Service())->getTopPages($projectId, $propertyId, $days, $limit) : [];
        $gscPages   = $siteUrl    ? (new GSCService())->getPagePerformance($projectId, $siteUrl, $days, $limit) : [];

        // Index GSC by URL path
        $gscIndex = [];
        foreach ($gscPages as $row) {
            $path = parse_url($row['url'], PHP_URL_PATH) ?: $row['url'];
            $gscIndex[$path] = $row;
        }

        // Merge GA4 + GSC by path
        $merged = array_map(function (array $row) use ($gscIndex): array {
            $gsc = $gscIndex[$row['path']] ?? null;
            return array_merge($row, [
                'clicks'      => $gsc['clicks']      ?? null,
                'impressions' => $gsc['impressions']  ?? null,
                'ctr'         => $gsc['ctr']          ?? null,
                'position'    => $gsc['position']     ?? null,
            ]);
        }, $ga4Pages);

        Response::success(['pages' => $merged, 'days' => $days]);
    }

    public function performanceTrend(int $projectId, array $query): void
    {
        $project    = (new \Crawl\Repositories\ProjectRepository())->find($projectId);
        if (!$project) { Response::error('Project not found', 404); return; }

        $propertyId = $project['ga4_property_id'] ?? '';
        if (!$propertyId) { Response::success(['trend' => [], 'error' => 'no_property_id']); return; }

        $days = (int)($query['days'] ?? 30);
        Response::success(['trend' => (new GA4Service())->getDailyTrend($projectId, $propertyId, $days)]);
    }

    // ── Performance: GSC ─────────────────────────────────────────────────────

    public function performanceSearch(int $projectId, array $query): void
    {
        $project = (new \Crawl\Repositories\ProjectRepository())->find($projectId);
        if (!$project) { Response::error('Project not found', 404); return; }

        $siteUrl = $project['gsc_site_url'] ?? '';
        if (!$siteUrl) { Response::success(['error' => 'no_site_url']); return; }

        $days    = (int)($query['days'] ?? 30);
        $summary = (new GSCService())->getSummary($projectId, $siteUrl, $days);
        $queries = (new GSCService())->getTopQueries($projectId, $siteUrl, $days);
        Response::success(['summary' => $summary, 'queries' => $queries, 'days' => $days]);
    }

    // ── Performance: GBP ─────────────────────────────────────────────────────

    public function performanceLocal(int $projectId, array $query): void
    {
        $project  = (new \Crawl\Repositories\ProjectRepository())->find($projectId);
        if (!$project) { Response::error('Project not found', 404); return; }

        $locationId = $project['gbp_location_id'] ?? '';
        if (!$locationId) { Response::success(['error' => 'no_location_id']); return; }

        $days   = (int)($query['days'] ?? 30);
        Response::success((new GBPService())->getMetrics($projectId, $locationId, $days));
    }

    // ── OAuth: connection status + disconnect ─────────────────────────────────

    public function oauthStatus(int $projectId): void
    {
        $tokens = (new OAuthTokenRepository())->statusForProject($projectId);
        $oauth  = new GoogleOAuthService();
        Response::success([
            'configured' => $oauth->isConfigured(),
            'ga4'  => isset($tokens['ga4'])  ? ['connected' => true, 'email' => $tokens['ga4']['account_email'],  'property_id' => $tokens['ga4']['property_id'],  'updated_at' => $tokens['ga4']['updated_at']]  : ['connected' => false],
            'gsc'  => isset($tokens['gsc'])  ? ['connected' => true, 'email' => $tokens['gsc']['account_email'],  'property_id' => $tokens['gsc']['property_id'],  'updated_at' => $tokens['gsc']['updated_at']]  : ['connected' => false],
            'gbp'  => isset($tokens['gbp'])  ? ['connected' => true, 'email' => $tokens['gbp']['account_email'],  'property_id' => $tokens['gbp']['property_id'],  'updated_at' => $tokens['gbp']['updated_at']]  : ['connected' => false],
        ]);
    }

    public function oauthDisconnect(int $projectId, array $body): void
    {
        $service = $body['service'] ?? '';
        if (!in_array($service, ['ga4', 'gsc', 'gbp'], true)) {
            Response::error('Invalid service', 422);
            return;
        }
        (new OAuthTokenRepository())->delete($projectId, $service);
        Response::success(['disconnected' => $service]);
    }

    public function oauthConnect(int $projectId, array $body): void
    {
        $service = $body['service'] ?? '';
        if (!in_array($service, ['ga4', 'gsc', 'gbp'], true)) {
            Response::error('Invalid service', 422);
            return;
        }
        $oauth = new GoogleOAuthService();
        if (!$oauth->isConfigured()) {
            Response::error('Google OAuth is not fully configured in Audit/.env (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI).', 503);
            return;
        }
        $url = $oauth->buildAuthUrl($service, $projectId);
        Response::success(['redirect_url' => $url]);
    }
}
