"""SDK matcher - applies signatures against parsed APK data."""
import re
from typing import Dict, List
from apk_analyzer.models import DEXData, ManifestData, NativeData, DetectedSDK, DetectionEvidence, SDKRisk


class SDKMatcher:
    def __init__(self, signatures: List[dict], dex: DEXData,
                 manifest: ManifestData, native: NativeData, versions: Dict[str, str]):
        self.signatures = signatures
        self.dex = dex
        self.manifest = manifest
        self.native = native
        self.versions = versions
        self._manifest_services_set = set(manifest.services)
        self._manifest_receivers_set = set(manifest.receivers)
        self._manifest_activities_set = set(manifest.activities)
        self._manifest_providers_set = set(manifest.providers)
        self._manifest_actions_set = set(manifest.intent_actions)
        self._native_set = set(native.lib_names)

    def match(self) -> List[DetectedSDK]:
        results: list[DetectedSDK] = []
        for sig in self.signatures:
            sdk = self._match_one(sig)
            if sdk:
                results.append(sdk)
        merged = self._merge_livecheck_duplicates(results)
        merged.sort(key=lambda s: (s.category, s.name, s.id))
        return merged

    @staticmethod
    def _is_livecheck_sdk(sdk: DetectedSDK) -> bool:
        return (sdk.id or "").startswith("lc_")

    @staticmethod
    def _identity_key(sdk: DetectedSDK) -> tuple[str, str, str]:
        return (
            (sdk.category or "").strip().lower(),
            (sdk.name or "").strip().lower(),
            (sdk.vendor or "").strip().lower(),
        )

    @staticmethod
    def _extend_unique(dst: list[str], src: list[str]) -> None:
        seen = set(dst)
        for item in src:
            if item not in seen:
                dst.append(item)
                seen.add(item)

    @staticmethod
    def _append_rule_hit(evidence: DetectionEvidence, rule_type: str, rule_value: str) -> None:
        if not rule_value:
            return
        hit = f"{rule_type}:{rule_value}"
        if hit not in evidence.matched_rules:
            evidence.matched_rules.append(hit)

    @staticmethod
    def _confidence_from_evidence(evidence: DetectionEvidence) -> str:
        total = (
            len(evidence.matched_packages)
            + len(evidence.matched_natives)
            + len(evidence.matched_manifest_services)
            + len(evidence.matched_manifest_receivers)
            + len(evidence.matched_string_patterns)
        )
        if total >= 2:
            return "high"
        if total == 1:
            return "medium"
        return "low"

    @staticmethod
    def _risk_rank(value: str) -> int:
        return {"unknown": 0, "low": 1, "medium": 2, "high": 3}.get((value or "").lower(), 0)

    def _merge_livecheck_into(self, base: DetectedSDK, incoming: DetectedSDK) -> None:
        self._extend_unique(base.detection_evidence.matched_packages, incoming.detection_evidence.matched_packages)
        self._extend_unique(base.detection_evidence.matched_natives, incoming.detection_evidence.matched_natives)
        self._extend_unique(
            base.detection_evidence.matched_manifest_services,
            incoming.detection_evidence.matched_manifest_services,
        )
        self._extend_unique(
            base.detection_evidence.matched_manifest_receivers,
            incoming.detection_evidence.matched_manifest_receivers,
        )
        self._extend_unique(
            base.detection_evidence.matched_string_patterns,
            incoming.detection_evidence.matched_string_patterns,
        )
        self._extend_unique(base.detection_evidence.matched_rules, incoming.detection_evidence.matched_rules)

        if not base.version and incoming.version:
            base.version = incoming.version
            base.version_source = incoming.version_source

        self._extend_unique(base.risk.permissions, incoming.risk.permissions)
        self._extend_unique(base.risk.data_collection, incoming.risk.data_collection)
        if self._risk_rank(incoming.risk.privacy_risk) > self._risk_rank(base.risk.privacy_risk):
            base.risk.privacy_risk = incoming.risk.privacy_risk
        if not base.risk.notes and incoming.risk.notes:
            base.risk.notes = incoming.risk.notes

        if len(incoming.description or "") > len(base.description or ""):
            base.description = incoming.description

        base.confidence = self._confidence_from_evidence(base.detection_evidence)

    def _merge_livecheck_duplicates(self, results: list[DetectedSDK]) -> list[DetectedSDK]:
        merged: list[DetectedSDK] = []
        livecheck_index: dict[tuple[str, str, str], int] = {}
        for sdk in results:
            if not self._is_livecheck_sdk(sdk):
                merged.append(sdk)
                continue
            key = self._identity_key(sdk)
            if key not in livecheck_index:
                livecheck_index[key] = len(merged)
                merged.append(sdk)
                continue
            self._merge_livecheck_into(merged[livecheck_index[key]], sdk)
        return merged

    def _match_one(self, sig: dict) -> DetectedSDK | None:
        det = sig.get("detection", {}) or {}
        evidence = DetectionEvidence()
        matched = False

        # 1. Package prefixes (DEX class names)
        for prefix in det.get("package_prefixes", []) or []:
            if self.dex.has_package(prefix):
                evidence.matched_packages.append(prefix)
                self._append_rule_hit(evidence, "package_prefixes", prefix)
                matched = True

        # 2. Native libs
        for lib in det.get("native_libs", []) or []:
            if lib in self._native_set:
                evidence.matched_natives.append(lib)
                self._append_rule_hit(evidence, "native_libs", lib)
                matched = True

        # 3. Manifest services
        for svc in det.get("manifest_services", []) or []:
            if svc in self._manifest_services_set:
                evidence.matched_manifest_services.append(svc)
                self._append_rule_hit(evidence, "manifest_services", svc)
                matched = True

        # 4. Manifest receivers
        for rcv in det.get("manifest_receivers", []) or []:
            if rcv in self._manifest_receivers_set:
                evidence.matched_manifest_receivers.append(rcv)
                self._append_rule_hit(evidence, "manifest_receivers", rcv)
                matched = True

        # 4.1 Manifest activities/providers/actions.
        # Reuse matched_string_patterns to keep output schema stable.
        for act in det.get("manifest_activities", []) or []:
            if act in self._manifest_activities_set:
                evidence.matched_string_patterns.append(f"manifest_activity:{act}")
                self._append_rule_hit(evidence, "manifest_activities", act)
                matched = True
        for provider in det.get("manifest_providers", []) or []:
            if provider in self._manifest_providers_set:
                evidence.matched_string_patterns.append(f"manifest_provider:{provider}")
                self._append_rule_hit(evidence, "manifest_providers", provider)
                matched = True
        for action in det.get("manifest_actions", []) or []:
            if action in self._manifest_actions_set:
                evidence.matched_string_patterns.append(f"manifest_action:{action}")
                self._append_rule_hit(evidence, "manifest_actions", action)
                matched = True

        # 5. String pool patterns
        manifest_strings = []
        if self.manifest.meta_data:
            manifest_strings.extend(self.manifest.meta_data.keys())
            manifest_strings.extend(str(v) for v in self.manifest.meta_data.values())
        for pattern in det.get("string_patterns", []) or []:
            for s in [*self.dex.string_pool, *manifest_strings]:
                if pattern in s:
                    evidence.matched_string_patterns.append(pattern)
                    self._append_rule_hit(evidence, "string_patterns", pattern)
                    matched = True
                    break

        if not matched:
            return None

        confidence = self._confidence_from_evidence(evidence)

        # Version extraction
        version, version_source = None, None
        ve = sig.get("version_extraction", {}) or {}

        # BuildConfig
        bc = ve.get("buildconfig", {}) or {}
        if bc and bc.get("class_pattern"):
            pattern = bc["class_pattern"]
            field_name = bc.get("field", "VERSION_NAME")
            for cls, ver in self.versions.items():
                if pattern in cls or cls in pattern:
                    version = ver
                    version_source = "buildconfig"
                    break

        # META-INF
        if not version:
            mi_key = ve.get("metainf_file")
            if mi_key and mi_key in self.versions:
                version = self.versions[mi_key]
                version_source = "metainf"

        # Package prefix version guess from versions dict
        if not version and evidence.matched_packages:
            for prefix in evidence.matched_packages:
                for key, ver in self.versions.items():
                    if key.startswith(prefix):
                        version = ver
                        version_source = "buildconfig"
                        break
                if version: break

        # Risk
        risk_data = sig.get("risk", {}) or {}
        risk = SDKRisk(
            permissions=risk_data.get("permissions", []) or [],
            data_collection=risk_data.get("data_collection", []) or [],
            privacy_risk=risk_data.get("privacy_risk", "unknown"),
            notes=risk_data.get("notes", ""),
        )

        return DetectedSDK(
            id=sig.get("id", ""),
            name=sig.get("name", ""),
            category=sig.get("category", "other"),
            vendor=sig.get("vendor", ""),
            description=sig.get("description", ""),
            confidence=confidence,
            version=version,
            version_source=version_source,
            detection_evidence=evidence,
            risk=risk,
        )
