<?php
declare(strict_types=1);

/**
 * Scan report parser:
 * - Parses JSON safely (with defaults for ALL fields)
 * - If "report" exists: counts malicious/suspicious/potentially_suspicious/clean
 * - Supports multiple presenting fields for verdict (threat/threat_name/result/details/reason)
 * - Normalizes each report entry with defaults + derived verdict_raw + verdict_bucket
 * - Returns: counters + all other attributes (with default values if optional/missing)
 */
final class QtrExternalScanReportParser
{
    /** @var array<string,mixed> */
    private array $defaults;

    /** @var array<string,mixed> */
    private array $reportItemDefaults;

    public function __construct()
    {
        $this->defaults = [
            // Counters
            'counts' => [
                'malicious' => 0,
                'suspicious' => 0,
                'potentially_suspicious' => 0,
                'clean' => 0,
                'total' => 0,
            ],

            // Common fields (sample + typical scan metadata)
            'uri'             => '',
            'details'         => '',
            'iframes_count'   => 0,
            'download_mb'     => 0,
            'downloaded_mb'   => 0,
            'processed_files' => 0,

            // Scan/meta fields (sample)
            'report'          => [],
            'priority'        => '',
            'domains_count'   => 0,
            'url'             => '',
            'port'            => '',
            'popularity'      => '',
            'ipaddr'          => '',
            'links_count'     => 0,
            'http_server'     => '',
            'country'         => '',
            'state'           => '',
            'blocked'         => 0,
            'is_blacklisted'  => '',
            'protocol'        => '',
            'host'            => '',
            'title'           => '',
            'age'             => '',
            'rating'          => 0,

            // Error
            'error'           => null,

            // Catch-all container for unexpected extra top-level fields (optional, but useful)
            '_extra'          => [],
        ];

        $this->reportItemDefaults = [
            'filename'     => '',
            'threat'       => '',
            'threat_name'  => '',
            'result'       => '',
            'details'      => '',
            'reason'       => '',
            'filetype'     => '',
            'filesize'     => 0,
            'line'         => 0,
            'md5'          => '',
            'dumpMD5'      => '',
            'offset'       => '',
            'dump'         => null,
            'scantime'     => 0.0,

            // Derived/normalized
            'verdict_raw'    => '',
            'verdict_bucket' => 'unknown', // malicious|suspicious|potentially_suspicious|clean|unknown
        ];
    }

    /**
     * Parse scan JSON and return normalized output with defaults.
     *
     * @return array<string,mixed>
     */
    public function parse(string $json = null): array
    {
        // Handle null input - return defaults
        if ($json === null || $json === '') {
            return $this->defaults;
        }

        $decoded = json_decode($json, true);

        if (!is_array($decoded)) {
            $out = $this->defaults;
            $out['error'] = 'Invalid JSON';
            return $out;
        }

        // Separate known vs extra top-level fields
        $extra = $this->extractExtraTopLevelFields($decoded, array_keys($this->defaults));

        // Merge defaults + decoded (decoded wins)
        $out = $this->arrayMergeDefaults($this->defaults, $decoded);
        $out['_extra'] = $extra;

        // Normalize numeric-ish fields
        $out['iframes_count']   = (int)($out['iframes_count'] ?? 0);
        $out['download_mb']     = $this->toNumber($out['download_mb'] ?? 0);
        $out['downloaded_mb']   = $this->toNumber($out['downloaded_mb'] ?? 0);
        $out['domains_count']   = (int)($out['domains_count'] ?? 0);
        $out['links_count']     = (int)($out['links_count'] ?? 0);
        $out['blocked']         = (int)($out['blocked'] ?? 0);
        $out['processed_files'] = (int)($out['processed_files'] ?? 0);
        $out['rating']          = (int)($out['rating'] ?? 0);

        // Ensure report is array
        if (!is_array($out['report'])) {
            $out['report'] = [];
        }

        // Compute counts + normalize report entries
        $counts = [
            'malicious' => 0,
            'suspicious' => 0,
            'potentially_suspicious' => 0,
            'clean' => 0,
            'total' => 0,
        ];

        $normalizedReport = [];

        foreach ($out['report'] as $item) {
            if (!is_array($item)) {
                continue;
            }

            $entry = $this->arrayMergeDefaults($this->reportItemDefaults, $item);

            // Normalize types
            $entry['filesize'] = (int)($entry['filesize'] ?? 0);
            $entry['line']     = (int)($entry['line'] ?? 0);
            $entry['scantime'] = (float)($entry['scantime'] ?? 0.0);

            // Multi-field verdict extraction
            $verdictRaw = $this->firstNonEmptyString([
                $entry['threat'] ?? '',
                $entry['threat_name'] ?? '',
                $entry['result'] ?? '',
                $entry['details'] ?? '',
                $entry['reason'] ?? '',
            ]);

            $entry['verdict_raw'] = $verdictRaw;

            $bucket = $this->bucketVerdict($verdictRaw, $entry);
            $entry['verdict_bucket'] = $bucket;

            if (in_array($bucket, ['clean', 'malicious', 'suspicious', 'potentially_suspicious'], true)) {
                $counts[$bucket]++;
                $counts['total']++;
            }

            $normalizedReport[] = $entry;
        }

        $out['report']  = $normalizedReport;
        $out['counts']  = $counts;
        $out['error']   = $out['error'] ?? null;

        return $out;
    }

    /* =========================
       Helpers
    ========================== */

    /**
     * @param array<string,mixed> $defaults
     * @param mixed $data
     * @return array<string,mixed>
     */
    private function arrayMergeDefaults(array $defaults, $data): array
    {
        if (!is_array($data)) {
            return $defaults;
        }

        $out = $defaults;

        foreach ($data as $k => $v) {
            if (array_key_exists($k, $defaults) && is_array($defaults[$k]) && is_array($v)) {
                $out[$k] = $this->arrayMergeDefaults($defaults[$k], $v);
            } else {
                $out[$k] = $v;
            }
        }

        return $out;
    }

    /**
     * Convert numeric-ish values (e.g. "20", "20.5") to int/float; otherwise 0.
     * @param mixed $v
     * @return int|float
     */
    private function toNumber($v)
    {
        if (is_int($v) || is_float($v)) {
            return $v;
        }
        if (is_string($v)) {
            $s = trim($v);
            if ($s === '') {
                return 0;
            }
            if (is_numeric($s)) {
                return $s + 0;
            }
        }
        return 0;
    }

    /**
     * @param array<int,mixed> $values
     */
    private function firstNonEmptyString(array $values): string
    {
        foreach ($values as $v) {
            if (is_string($v)) {
                $s = trim($v);
                if ($s !== '') {
                    return $s;
                }
            } elseif (is_numeric($v)) {
                $s = trim((string)$v);
                if ($s !== '') {
                    return $s;
                }
            }
        }
        return '';
    }

    /**
     * Bucket verdict into: malicious / suspicious / potentially_suspicious / clean / unknown
     *
     * IMPORTANT: Replace/extend mappings to match your engine’s exact output vocabulary.
     *
     * @param array<string,mixed> $entry
     */
    private function bucketVerdict(string $raw, array $entry): string
    {
        $s = strtolower(trim($raw));

        // Clean signals
        if (
            $s === 'clean' ||
            str_contains($s, 'file is clean') ||
            str_contains($s, 'no significant issues')
        ) {
            return 'clean';
        }

        // Potentially suspicious signals
        if (
            $s === 'potentially suspicious' ||
            $s === 'potentially_suspicious' ||
            $s === 'potentially-suspicious' ||
            str_contains($s, 'potentially suspicious') ||
            str_contains($s, 'pup') ||
            str_contains($s, 'riskware') ||
            str_contains($s, 'grayware')
        ) {
            return 'potentially_suspicious';
        }

        // Suspicious signals
        if (
            $s === 'suspicious' ||
            str_contains($s, 'suspicious') ||
            str_contains($s, 'obfuscated') ||
            str_contains($s, 'eval(') ||
            str_contains($s, 'base64_decode')
        ) {
            return 'suspicious';
        }

        // Malicious signals
        if (
            $s === 'malicious' ||
            $s === 'infected' ||
            str_contains($s, 'trojan') ||
            str_contains($s, 'backdoor') ||
            str_contains($s, 'webshell') ||
            str_contains($s, 'phishing') ||
            str_contains($s, 'malware') ||
            str_contains($s, 'virus')
        ) {
            return 'malicious';
        }

        // Optional fallback: if threat exists and is not clean, treat as suspicious
        $threat = strtolower(trim((string)($entry['threat'] ?? '')));
        if ($threat !== '' && $threat !== 'clean') {
            return 'suspicious';
        }

        return 'unknown';
    }

    /**
     * Keep unexpected top-level attributes so you don't lose them.
     *
     * @param array<string,mixed> $decoded
     * @param array<int,string> $knownKeys
     * @return array<string,mixed>
     */
    private function extractExtraTopLevelFields(array $decoded, array $knownKeys): array
    {
        $known = array_flip($knownKeys);
        $extra = [];

        foreach ($decoded as $k => $v) {
            if (!isset($known[$k])) {
                $extra[$k] = $v;
            }
        }

        return $extra;
    }
}

/* -------------------------
   Example usage
-------------------------- */

// $parser = new Qtr_Scan_Report_Parser();
// $result = $parser->parse($jsonString);
// echo json_encode($result, JSON_PRETTY_PRINT);

