<?php
declare(strict_types=1);

require_once __DIR__ . '/../helpers/LeakRadarApiHelper.php';
require_once __DIR__ . '/Cache.php';

/**
 * PublicSearchService — public "find your creds" search backed by LeakRadar API.
 *
 * Replaces the previous local-DB query path (SearchModel::getCountForExtension/
 * getRecentIdentities/searchInfections/getSnapshotSummary) with an upstream
 * call to https://api.leakradar.io/search/domain/{domain}, mirrored after
 * the existing free-report integration.
 *
 * - Cache: Redis 24h on hit, 60s negative cache on upstream failure.
 * - Returns `null` on failure / empty domain — controller falls back to a
 *   scalar zero count so the view renders "Total records found: 0".
 *
 * Shape returned matches what views/findyourcreds.php consumes (4 buckets:
 * employees / third / customers / others).
 */
final class PublicSearchService
{
    private const CACHE_TTL_OK   = 86400; // 24h
    private const CACHE_TTL_FAIL = 60;    // 1 min negative cache

    private string $apiBase;

    public function __construct(string $apiBase = '')
    {
        $this->apiBase = $apiBase !== ''
            ? $apiBase
            : (getenv('LEAKRADAR_API_BASE') ?: 'https://api.leakradar.io');
    }

    /**
     * Run a domain lookup. Returns the snapshot the view expects, or null
     * if domain is empty / upstream failure / no useful data.
     *
     * @return array<string, mixed>|null
     */
    public function lookup(string $domain): ?array
    {
        $domain = $this->normalize($domain);
        if ($domain === '') {
            return null;
        }

        $cacheKey = self::cacheKey($domain);

        // Negative cache marker: the value `false` means "we tried recently
        // and failed". Skip a fresh call within TTL_FAIL.
        $cached = Cache::get($cacheKey);
        if ($cached === false) {
            return null;
        }
        if (is_array($cached)) {
            return $cached;
        }

        $resp = leakradar_api(
            'GET',
            $this->apiBase . '/search/domain/' . rawurlencode($domain),
            ['light' => 'false']
        );

        if (($resp['status'] ?? 0) !== 200 || !is_array($resp['data'] ?? null)) {
            error_log(sprintf(
                '[PublicSearchService] LeakRadar failure status=%s err=%s',
                $resp['status'] ?? '?',
                is_string($resp['error'] ?? null) ? $resp['error'] : 'n/a'
            ));
            // Negative cache so a flapping LR doesn't pummel us.
            Cache::set($cacheKey, false, self::CACHE_TTL_FAIL);
            return null;
        }

        $shaped = $this->shape($domain, $resp['data']);
        Cache::set($cacheKey, $shaped, self::CACHE_TTL_OK);
        return $shaped;
    }

    /**
     * Was the most recent lookup served from cache?
     * Used by the controller to decide whether to charge tokens.
     */
    public function wasCacheHit(string $domain): bool
    {
        $domain = $this->normalize($domain);
        if ($domain === '') {
            return false;
        }
        return Cache::get(self::cacheKey($domain)) !== null;
    }

    /** Map LeakRadar response → exact shape views/findyourcreds.php expects. */
    private function shape(string $domain, array $lr): array
    {
        $emp = (int) ($lr['employees_compromised']     ?? 0);
        $tp  = (int) ($lr['third_parties_compromised'] ?? 0);
        $cu  = (int) ($lr['customers_compromised']     ?? 0);
        // LeakRadar doesn't surface an "others" bucket; render the card with 0.
        $oth = 0;

        // LeakRadar exposes strength breakdowns for all 3 attributable buckets.
        // (NB: keys are inconsistently pluralized in the upstream payload —
        // employee_passwords / third_parties_passwords / customer_passwords.)
        $strengthEmployees = self::mapStrength($lr['employee_passwords']       ?? []);
        $strengthThird     = self::mapStrength($lr['third_parties_passwords']  ?? []);
        $strengthCustomers = self::mapStrength($lr['customer_passwords']       ?? []);

        // Aggregate totals for the page-level "strength_totals" hint
        $strengthTotals = [
            'strong'    => $strengthEmployees['strong']    + $strengthThird['strong']    + $strengthCustomers['strong'],
            'medium'    => $strengthEmployees['medium']    + $strengthThird['medium']    + $strengthCustomers['medium'],
            'weak'      => $strengthEmployees['weak']      + $strengthThird['weak']      + $strengthCustomers['weak'],
            'very_weak' => $strengthEmployees['very_weak'] + $strengthThird['very_weak'] + $strengthCustomers['very_weak'],
            'unknown'   => 0,
        ];

        // Others bucket has no breakdown upstream → empty shape (view shows
        // a "no data" badge gracefully).
        $emptyStrength = [
            'strong'    => 0,
            'medium'    => 0,
            'weak'      => 0,
            'very_weak' => 0,
            'unknown'   => 0,
        ];

        return [
            'display_term'      => $domain,
            'normalized_domain' => $domain,
            'total'             => $emp + $tp + $cu + $oth,
            'searched_by_count' => (int) ($lr['searched_by_count'] ?? 0),
            'buckets' => [
                'employees' => [
                    'label'            => 'Internal access',
                    'description'      => 'Website and email domains both match the searched term.',
                    'color'            => '#3c2266',
                    'icon'             => 'fas fa-user-tie',
                    'summary_count'    => $emp,
                    'strength_summary' => $strengthEmployees,
                ],
                'third' => [
                    'label'            => 'Third parties',
                    'description'      => 'Email domain matches, website domain differs.',
                    'color'            => '#fe7833',
                    'icon'             => 'fas fa-building',
                    'summary_count'    => $tp,
                    'strength_summary' => $strengthThird,
                ],
                'customers' => [
                    'label'            => 'Customers',
                    'description'      => 'Website domain matches, email domain differs.',
                    'color'            => '#ffef34',
                    'icon'             => 'fas fa-users',
                    'summary_count'    => $cu,
                    'strength_summary' => $strengthCustomers,
                ],
                'others' => [
                    'label'            => 'Others',
                    'description'      => 'No direct match with the searched domain or asset.',
                    'color'            => '#00b36a',
                    'icon'             => 'fas fa-layer-group',
                    'summary_count'    => $oth,
                    'strength_summary' => $emptyStrength,
                ],
            ],
            'strength_totals' => $strengthTotals,
        ];
    }

    /** Map a LeakRadar passwords block → view's strength_summary shape. */
    private static function mapStrength(array $block): array
    {
        return [
            'strong'    => (int) ($block['strong']['qty']   ?? 0),
            'medium'    => (int) ($block['medium']['qty']   ?? 0),
            'weak'      => (int) ($block['weak']['qty']     ?? 0),
            'very_weak' => (int) ($block['too_weak']['qty'] ?? 0),
            'unknown'   => 0,
        ];
    }

    private function normalize(string $term): string
    {
        if (function_exists('normalize_domain')) {
            return (string) normalize_domain($term);
        }
        return strtolower(trim($term));
    }

    private static function cacheKey(string $normalizedDomain): string
    {
        return 'pubsearch:dom:' . sha1($normalizedDomain);
    }
}
