"""iOS compliance scanner — parses Info.plist, entitlements, and Swift/ObjC source."""

from __future__ import annotations

import plistlib
import re
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..engine.runner import Finding


def scan(project_root: Path) -> List[Finding]:
    """Run all iOS compliance checks and return findings."""
    findings: List[Finding] = []
    root = Path(project_root).resolve()

    plist_data = _load_info_plist(root)
    entitlements = _load_entitlements(root)
    source_contents = _collect_source(root)

    findings.extend(_check_privacy_permissions(root, plist_data, source_contents))
    findings.extend(_check_privacy_manifest(root))
    findings.extend(_check_ats(plist_data, source_contents))
    findings.extend(_check_secrets(source_contents))
    findings.extend(_check_ssl_pinning(source_contents))
    findings.extend(_check_metadata(root, plist_data))
    findings.extend(_check_entitlements(entitlements, source_contents))
    findings.extend(_check_keychain_usage(source_contents))
    findings.extend(_check_app_clips(root, plist_data))

    return findings


# ---------------------------------------------------------------------------
# File loaders
# ---------------------------------------------------------------------------

def _load_info_plist(root: Path) -> Dict[str, Any]:
    """Find and parse the first Info.plist in the project."""
    for plist_path in root.rglob("Info.plist"):
        # Skip build artifacts
        if any(part in str(plist_path) for part in ["DerivedData", "build/", ".build/"]):
            continue
        # Prevent symlink escape
        try:
            resolved = plist_path.resolve()
            if not str(resolved).startswith(str(root.resolve())):
                continue
        except OSError:
            continue
        try:
            with open(plist_path, "rb") as f:
                return plistlib.load(f)
        except (plistlib.InvalidFileException, OSError, ValueError):
            continue
    return {}


def _load_entitlements(root: Path) -> Dict[str, Any]:
    """Find and parse the first .entitlements file."""
    for ent_path in root.rglob("*.entitlements"):
        try:
            resolved = ent_path.resolve()
            if not str(resolved).startswith(str(root.resolve())):
                continue
        except OSError:
            continue
        try:
            with open(ent_path, "rb") as f:
                return plistlib.load(f)
        except (plistlib.InvalidFileException, OSError, ValueError):
            continue
    return {}


def _collect_source(root: Path, limit: int = 500) -> str:
    """Concatenate source file contents for pattern matching (capped)."""
    chunks: List[str] = []
    count = 0
    for ext in ("*.swift", "*.m", "*.mm", "*.h", "*.dart", "*.cs"):
        for src in root.rglob(ext):
            if any(p in str(src) for p in ["DerivedData", "build/", "Pods/", ".build/", ".dart_tool/"]):
                continue
            try:
                chunks.append(src.read_text(errors="ignore"))
            except OSError:
                continue
            count += 1
            if count >= limit:
                break
    return "\n".join(chunks)


# ---------------------------------------------------------------------------
# Privacy & Permissions (IOS-PRIV-*)
# ---------------------------------------------------------------------------

_PRIVACY_CHECKS = [
    {
        "id": "IOS-PRIV-001",
        "plist_key": "NSCameraUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"AVCaptureSession", r"AVCaptureDevice", r"UIImagePickerController.*camera",
            # Flutter / Dart
            r"(?:camera|image_picker|mobile_scanner|barcode_scan|qr_code_scanner)", r"CameraController", r"CameraPreview",
            # React Native
            r"react-native-camera", r"react-native-vision-camera", r"expo-camera", r"launchCamera",
            # .NET MAUI / Xamarin
            r"MediaPicker", r"CapturePhotoAsync",
        ],
        "summary": "Missing NSCameraUsageDescription when camera APIs detected",
        "fix": "Add NSCameraUsageDescription key to Info.plist with a user-facing explanation.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-002",
        "plist_key": "NSPhotoLibraryUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"PHPhotoLibrary", r"UIImagePickerController", r"PHAsset",
            # Flutter / Dart
            r"image_picker", r"ImagePicker", r"photo_manager", r"pickImage",
            # React Native
            r"react-native-image-picker", r"launchImageLibrary", r"expo-image-picker",
            # .NET MAUI / Xamarin
            r"MediaPicker\.PickPhotoAsync", r"FilePicker\.PickAsync",
        ],
        "summary": "Missing NSPhotoLibraryUsageDescription when photo access detected",
        "fix": "Add NSPhotoLibraryUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-003",
        "plist_key": "NSUserTrackingUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"ASIdentifierManager", r"ATTrackingManager", r"advertisingIdentifier",
            # Flutter / Dart
            r"app_tracking_transparency", r"AppTrackingTransparency", r"requestTrackingAuthorization",
            # React Native
            r"react-native-tracking-transparency", r"getTrackingStatus",
        ],
        "summary": "Missing NSUserTrackingUsageDescription when IDFA used",
        "fix": "Add NSUserTrackingUsageDescription key to Info.plist and implement ATTrackingManager prompt.",
        "reference": "Apple App Tracking Transparency",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-004",
        "plist_key": "NSLocationWhenInUseUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"CLLocationManager", r"requestWhenInUseAuthorization", r"requestAlwaysAuthorization",
            # Flutter / Dart
            r"geolocator", r"Geolocator", r"location\.Location\(\)", r"requestPermission.*location",
            # React Native
            r"react-native-geolocation", r"expo-location", r"Geolocation\.getCurrentPosition",
            # .NET MAUI / Xamarin
            r"Geolocation\.GetLocationAsync", r"GeolocationRequest",
        ],
        "summary": "Location permission requested without justification string",
        "fix": "Add NSLocationWhenInUseUsageDescription to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-005",
        "plist_key": "NSMicrophoneUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"AVAudioSession", r"AVAudioRecorder", r"AVAudioEngine", r"requestRecordPermission",
            # Flutter / Dart
            r"(?:record|audio_session|flutter_sound|permission_handler).*(?:microphone|audio|record)",
            r"AudioRecorder", r"FlutterSoundRecorder",
            # React Native
            r"react-native-audio-recorder-player", r"expo-av.*Audio\.Recording",
            # .NET MAUI / Xamarin
            r"Plugin\.Maui\.Audio", r"IAudioRecorder",
        ],
        "summary": "Missing NSMicrophoneUsageDescription when microphone APIs detected",
        "fix": "Add NSMicrophoneUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-006",
        "plist_key": "NSContactsUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"CNContactStore", r"ABAddressBook",
            # Flutter / Dart
            r"contacts_service", r"flutter_contacts", r"getContacts",
            # React Native
            r"react-native-contacts", r"expo-contacts",
            # .NET MAUI / Xamarin
            r"Communication\.Contacts", r"PickContactAsync",
        ],
        "summary": "Missing NSContactsUsageDescription when contacts APIs detected",
        "fix": "Add NSContactsUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-009",
        "plist_key": "NSHealthShareUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"HKHealthStore", r"HKObjectType", r"HKSampleQuery", r"HealthKit",
            # Flutter / Dart
            r"health", r"HealthFactory", r"getHealthDataFromTypes",
            # React Native
            r"react-native-health", r"AppleHealthKit",
        ],
        "summary": "Missing NSHealthShareUsageDescription when HealthKit APIs detected",
        "fix": "Add NSHealthShareUsageDescription (and NSHealthUpdateUsageDescription if writing) to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1 — HealthKit",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-010",
        "plist_key": "NSHomeKitUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"HMHomeManager", r"HMAccessory", r"HomeKit",
            # Flutter / Dart
            r"homekit", r"HMHomeManager",
            # React Native
            r"react-native-homekit",
        ],
        "summary": "Missing NSHomeKitUsageDescription when HomeKit APIs detected",
        "fix": "Add NSHomeKitUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1 — HomeKit",
        "severity": "FAIL",
    },
    # --- Rules 011+ below ---
    {
        "id": "IOS-PRIV-011",
        "plist_key": "NSBluetoothAlwaysUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"CBCentralManager", r"CBPeripheralManager", r"CoreBluetooth",
            # Flutter / Dart
            r"flutter_blue_plus", r"flutter_reactive_ble", r"FlutterBluePlus",
            # React Native
            r"react-native-ble-plx", r"react-native-ble-manager",
            # .NET MAUI / Xamarin
            r"Plugin\.BLE", r"IBluetoothLE",
        ],
        "summary": "Missing NSBluetoothAlwaysUsageDescription when Bluetooth APIs detected",
        "fix": "Add NSBluetoothAlwaysUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-012",
        "plist_key": "NSCalendarsUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"EKEventStore", r"EKCalendar", r"EventKit",
            # Flutter / Dart
            r"device_calendar", r"DeviceCalendarPlugin",
            # React Native
            r"react-native-calendar-events", r"expo-calendar",
        ],
        "summary": "Missing NSCalendarsUsageDescription when calendar APIs detected",
        "fix": "Add NSCalendarsUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-013",
        "plist_key": "NSFaceIDUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"LAContext", r"evaluatePolicy.*biometry", r"LocalAuthentication",
            # Flutter / Dart
            r"local_auth",
            # React Native
            r"react-native-biometrics", r"expo-local-authentication",
        ],
        "summary": "Missing NSFaceIDUsageDescription when Face ID / biometric APIs detected",
        "fix": "Add NSFaceIDUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-014",
        "plist_key": "NSPhotoLibraryAddUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"UIImageWriteToSavedPhotosAlbum", r"creationRequestForAsset",
            # Flutter / Dart
            r"image_gallery_saver", r"gallery_saver",
            # React Native
            r"@react-native-camera-roll/camera-roll", r"expo-media-library",
        ],
        "summary": "Missing NSPhotoLibraryAddUsageDescription when write-only photo access detected",
        "fix": "Add NSPhotoLibraryAddUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-015",
        "plist_key": "NSSpeechRecognitionUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"SFSpeechRecognizer", r"SFSpeechURLRecognitionRequest",
            # Flutter / Dart
            r"speech_to_text", r"SpeechToText",
            # React Native
            r"@react-native-voice/voice",
        ],
        "summary": "Missing NSSpeechRecognitionUsageDescription when speech recognition detected",
        "fix": "Add NSSpeechRecognitionUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-016",
        "plist_key": "NSMotionUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"CMMotionManager", r"CMPedometer", r"CoreMotion",
            # Flutter / Dart
            r"sensors_plus", r"pedometer",
            # React Native
            r"react-native-sensors", r"expo-sensors",
            # .NET MAUI / Xamarin
            r"Accelerometer\.Default", r"Gyroscope\.Default",
        ],
        "summary": "Missing NSMotionUsageDescription when motion/sensor APIs detected",
        "fix": "Add NSMotionUsageDescription key to Info.plist.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
    {
        "id": "IOS-PRIV-020",
        "plist_key": "NSLocationAlwaysAndWhenInUseUsageDescription",
        "patterns": [
            # Swift / ObjC
            r"requestAlwaysAuthorization", r"allowsBackgroundLocationUpdates",
            r"startMonitoringSignificantLocationChanges",
        ],
        "summary": "Missing NSLocationAlwaysAndWhenInUseUsageDescription for background location",
        "fix": "Add NSLocationAlwaysAndWhenInUseUsageDescription to Info.plist for background location access.",
        "reference": "Apple App Store Review Guidelines 5.1.1",
        "severity": "FAIL",
    },
]


def _check_privacy_permissions(root: Path, plist: dict, source: str) -> List[Finding]:
    findings: List[Finding] = []
    for check in _PRIVACY_CHECKS:
        has_key = check["plist_key"] in plist
        uses_api = any(re.search(p, source) for p in check["patterns"])
        if uses_api and not has_key:
            findings.append(Finding(
                rule_id=check["id"],
                severity=check["severity"],
                message=check["summary"],
                fix=check["fix"],
                reference=check["reference"],
            ))
    return findings


# ---------------------------------------------------------------------------
# Privacy Manifest (IOS-PRIV-007/008) — CRITICAL for App Store 2024+
# ---------------------------------------------------------------------------

_REQUIRED_REASON_APIS = [
    # System boot time APIs
    r"systemUptime", r"mach_absolute_time", r"NSProcessInfo.*systemUptime",
    # User defaults APIs
    r"UserDefaults", r"NSUserDefaults",
    # Active keyboards APIs
    r"activeInputModes",
    # Disk space APIs
    r"NSFileSystemFreeSize", r"freeSize", r"volumeAvailableCapacity",
    r"volumeTotalCapacity", r"systemSize", r"\bstatfs\b", r"\bstatvfs\b",
    # File timestamp APIs
    r"creationDate", r"modificationDate", r"NSURLContentModificationDateKey",
    r"NSFileCreationDate", r"\bgetattrlist\b",
]


def _check_privacy_manifest(root: Path) -> List[Finding]:
    """Check for Apple Privacy Manifest (PrivacyInfo.xcprivacy) — required since Spring 2024."""
    findings: List[Finding] = []

    privacy_manifests = list(root.rglob("PrivacyInfo.xcprivacy"))
    privacy_manifests = [p for p in privacy_manifests if "build/" not in str(p) and "DerivedData" not in str(p)]

    if not privacy_manifests:
        findings.append(Finding(
            rule_id="IOS-PRIV-007",
            severity="FAIL",
            message="Missing Privacy Manifest (PrivacyInfo.xcprivacy) — required for App Store submission",
            fix="Create a PrivacyInfo.xcprivacy file declaring privacy nutrition labels and required reason APIs.",
            reference="https://developer.apple.com/documentation/bundleresources/privacy_manifest_files",
        ))

    # Check for Required Reason APIs usage without declaration
    source = _collect_source(root)
    used_apis = []
    for pattern in _REQUIRED_REASON_APIS:
        if re.search(pattern, source):
            used_apis.append(pattern)

    if used_apis and not privacy_manifests:
        findings.append(Finding(
            rule_id="IOS-PRIV-008",
            severity="FAIL",
            message=f"Required Reason APIs used ({len(used_apis)} types) without Privacy Manifest declaration",
            fix="Declare each Required Reason API usage in PrivacyInfo.xcprivacy with the appropriate reason code.",
            reference="https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api",
        ))

    return findings


# ---------------------------------------------------------------------------
# App Transport Security (IOS-SEC-*)
# ---------------------------------------------------------------------------

def _check_ats(plist: dict, source: str) -> List[Finding]:
    findings: List[Finding] = []

    ats = plist.get("NSAppTransportSecurity", {})
    if isinstance(ats, dict) and ats.get("NSAllowsArbitraryLoads") is True:
        findings.append(Finding(
            rule_id="IOS-SEC-001",
            severity="FAIL",
            message="ATS disabled globally (NSAllowsArbitraryLoads is true)",
            fix="Remove NSAllowsArbitraryLoads or set to false. Use exception domains for specific hosts.",
            reference="Apple App Store Review Guidelines 2.1",
        ))

    http_urls = re.findall(r'http://[^\s"\'>]+', source)
    if http_urls:
        findings.append(Finding(
            rule_id="IOS-SEC-002",
            severity="WARN",
            message=f"Non-HTTPS endpoints detected ({len(http_urls)} occurrence(s))",
            fix="Upgrade all endpoints to HTTPS.",
            reference="Apple ATS documentation",
        ))

    return findings


# ---------------------------------------------------------------------------
# Hardcoded Secrets (IOS-SEC-003)
# ---------------------------------------------------------------------------

_SECRET_PATTERNS = [
    (r'(?i)(?:api[_-]?key|apikey)\s*[:=]\s*["\'][A-Za-z0-9_\-]{16,}["\']', "API key"),
    (r'(?i)(?:secret|token|password|passwd)\s*[:=]\s*["\'][A-Za-z0-9_\-]{8,}["\']', "Secret/token"),
    (r'(?i)sk_(?:live|test)_[A-Za-z0-9]{24,}', "Stripe key"),
    (r'AIza[0-9A-Za-z_\-]{35}', "Google API key"),
    (r'(?i)aws[_-]?(?:access[_-]?key|secret)\s*[:=]\s*["\'][A-Za-z0-9/+=]{16,}["\']', "AWS credential"),
    (r'(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}', "GitHub token"),
    (r'github_pat_[A-Za-z0-9_]{20,}', "GitHub fine-grained token"),
]


def _check_secrets(source: str) -> List[Finding]:
    """Detect hardcoded API keys, tokens, and secrets in source code."""
    findings: List[Finding] = []
    found_types: List[str] = []

    for pattern, label in _SECRET_PATTERNS:
        if re.search(pattern, source):
            found_types.append(label)

    if found_types:
        findings.append(Finding(
            rule_id="IOS-SEC-003",
            severity="FAIL",
            message=f"Hardcoded secrets detected in source code: {', '.join(found_types)}",
            fix="Move secrets to environment variables, Keychain, or a secure configuration management system.",
            reference="OWASP Mobile Security — M9 Reverse Engineering",
        ))

    return findings


# ---------------------------------------------------------------------------
# SSL Certificate Pinning (IOS-SEC-004)
# ---------------------------------------------------------------------------

_SSL_PINNING_PATTERNS = [
    # Swift / ObjC
    r"URLSessionDelegate",
    r"didReceive\s+challenge.*URLAuthenticationChallenge",
    r"TrustKit",
    r"SSLPinningMode",
    r"ServerTrustManager",
    r"pinnedCertificates",
    r"evaluateServerTrust",
    # Flutter / Dart
    r"SecurityContext", r"setTrustedCertificates", r"ssl_pinning_plugin", r"http_certificate_pinning",
    # React Native
    r"react-native-ssl-pinning", r"ssl-pinning",
    # .NET MAUI / Xamarin
    r"ServerCertificateCustomValidationCallback", r"X509Certificate2",
]


def _check_ssl_pinning(source: str) -> List[Finding]:
    """Check if the app implements SSL certificate pinning."""
    findings: List[Finding] = []

    # Only warn if the app makes network calls but doesn't pin
    network_patterns = [
        # Swift / ObjC
        r"URLSession", r"Alamofire", r"AF\.", r"URLRequest", r"httpBody",
        # Flutter / Dart
        r"http\.Client", r"HttpClient", r"Dio\(", r"package:http",
        # React Native
        r"fetch\(", r"axios",
        # .NET MAUI / Xamarin
        r"HttpClient", r"HttpRequestMessage",
    ]
    makes_network_calls = any(re.search(p, source) for p in network_patterns)
    has_pinning = any(re.search(p, source) for p in _SSL_PINNING_PATTERNS)

    if makes_network_calls and not has_pinning:
        findings.append(Finding(
            rule_id="IOS-SEC-004",
            severity="WARN",
            message="No SSL certificate pinning detected for network communications",
            fix="Implement SSL pinning using URLSessionDelegate, TrustKit, or Alamofire's ServerTrustManager.",
            reference="OWASP Mobile Security — M3 Insecure Communication",
        ))

    return findings


# ---------------------------------------------------------------------------
# Metadata & Packaging (IOS-META-*)
# ---------------------------------------------------------------------------

def _check_metadata(root: Path, plist: dict) -> List[Finding]:
    findings: List[Finding] = []

    # IOS-META-001 — App icons
    icon_sets = list(root.rglob("AppIcon.appiconset"))
    if not icon_sets:
        findings.append(Finding(
            rule_id="IOS-META-001",
            severity="FAIL",
            message="Missing required app icons (no AppIcon.appiconset found)",
            fix="Add required app icon assets to Assets.xcassets/AppIcon.appiconset.",
            reference="Apple Human Interface Guidelines — App Icons",
        ))

    # IOS-META-002 — Bundle identifier format
    bundle_id = plist.get("CFBundleIdentifier", "")
    if bundle_id and not re.match(r'^[a-zA-Z][a-zA-Z0-9\-\.]*$', bundle_id):
        # Ignore Xcode variables like $(PRODUCT_BUNDLE_IDENTIFIER)
        if "$(" not in bundle_id:
            findings.append(Finding(
                rule_id="IOS-META-002",
                severity="FAIL",
                message=f"Bundle identifier has invalid format: {bundle_id}",
                fix="Ensure CFBundleIdentifier uses reverse domain notation (e.g. com.example.app).",
                reference="Apple Developer Documentation — CFBundleIdentifier",
            ))

    # IOS-META-003 — Launch screen
    has_launch_key = "UILaunchStoryboardName" in plist
    has_launch_file = (
        bool(list(root.rglob("LaunchScreen.storyboard")))
        or bool(list(root.rglob("LaunchScreen.xib")))
    )
    if not has_launch_key and not has_launch_file:
        findings.append(Finding(
            rule_id="IOS-META-003",
            severity="WARN",
            message="Missing launch screen configuration",
            fix="Add a LaunchScreen.storyboard or set UILaunchStoryboardName in Info.plist.",
            reference="Apple Human Interface Guidelines — Launch Screens",
        ))

    # IOS-META-004 — Minimum deployment target
    min_target = plist.get("MinimumOSVersion", "")
    if min_target:
        try:
            major = int(min_target.split(".")[0])
            if major < 16:
                findings.append(Finding(
                    rule_id="IOS-META-004",
                    severity="INFO",
                    message=f"Minimum deployment target is iOS {min_target} — consider raising to iOS 16+",
                    fix="Update MinimumOSVersion to at least 16.0 to access modern APIs.",
                    reference="Apple Developer Documentation — Deployment Target",
                ))
        except (ValueError, IndexError):
            pass

    return findings


# ---------------------------------------------------------------------------
# Entitlements (IOS-ENT-*)
# ---------------------------------------------------------------------------

def _check_entitlements(entitlements: dict, source: str) -> List[Finding]:
    findings: List[Finding] = []

    # IOS-ENT-001 — iCloud enabled but unused
    icloud_ids = entitlements.get("com.apple.developer.icloud-container-identifiers")
    if icloud_ids:
        icloud_patterns = [
        r"CKContainer", r"NSUbiquitousKeyValueStore", r"CloudKit",
        # Flutter / Dart
        r"icloud_storage",
    ]
        uses_icloud = any(re.search(p, source) for p in icloud_patterns)
        if not uses_icloud:
            findings.append(Finding(
                rule_id="IOS-ENT-001",
                severity="WARN",
                message="iCloud entitlement enabled but no iCloud usage detected in source",
                fix="Remove iCloud entitlement if not used, or implement iCloud functionality.",
                reference="Apple Developer Documentation — iCloud",
            ))

    # IOS-ENT-002 — Push notification entitlement missing but APNs used
    has_aps = "aps-environment" in entitlements
    push_patterns = [
        r"UNUserNotificationCenter", r"registerForRemoteNotifications", r"didRegisterForRemoteNotificationsWithDeviceToken",
        # Flutter / Dart
        r"firebase_messaging", r"FirebaseMessaging", r"onMessage",
        # React Native
        r"react-native-push-notification", r"@react-native-firebase/messaging",
        # .NET MAUI / Xamarin
        r"PushNotificationChannel",
    ]
    uses_push = any(re.search(p, source) for p in push_patterns)
    if uses_push and not has_aps:
        findings.append(Finding(
            rule_id="IOS-ENT-002",
            severity="FAIL",
            message="Push notification entitlement missing but APNs APIs used",
            fix="Add aps-environment entitlement to your app's entitlements file.",
            reference="Apple Developer Documentation — Push Notifications",
        ))

    return findings


# ---------------------------------------------------------------------------
# Keychain Usage Audit (IOS-SEC-005)
# ---------------------------------------------------------------------------

_SENSITIVE_USERDEFAULTS_PATTERNS = [
    # Swift / ObjC
    r'(?i)UserDefaults[^}]*(?:password|token|secret|apiKey|api_key|accessToken|access_token|refreshToken|refresh_token)',
    r'(?i)(?:set|setValue)\s*\([^)]*(?:password|token|secret|apiKey|api_key|accessToken|refreshToken)[^)]*,\s*forKey',
    r'(?i)NSUserDefaults[^}]*(?:password|token|secret|apiKey|api_key|accessToken)',
    # Flutter / Dart (SharedPreferences instead of secure storage)
    r'(?i)SharedPreferences[^}]*(?:password|token|secret|apiKey|api_key|accessToken|refreshToken)',
    r'(?i)(?:setString|setInt)\s*\([^)]*(?:password|token|secret|apiKey|api_key|accessToken)',
    # React Native (AsyncStorage instead of secure storage)
    r'(?i)AsyncStorage[^}]*(?:password|token|secret|apiKey|api_key|accessToken)',
    # .NET MAUI / Xamarin (Preferences instead of secure storage)
    r'(?i)Preferences\.Set[^}]*(?:password|token|secret|apiKey|api_key|accessToken)',
]


def _check_keychain_usage(source: str) -> List[Finding]:
    """Detect sensitive data stored in UserDefaults instead of Keychain."""
    findings: List[Finding] = []

    for pattern in _SENSITIVE_USERDEFAULTS_PATTERNS:
        if re.search(pattern, source):
            findings.append(Finding(
                rule_id="IOS-SEC-005",
                severity="FAIL",
                message="Sensitive data (passwords/tokens) stored in UserDefaults instead of Keychain",
                fix="Use Keychain Services (SecItemAdd/SecItemCopyMatching) or a wrapper like KeychainAccess for sensitive data.",
                reference="Apple Security — Keychain Services",
            ))
            break

    return findings


# ---------------------------------------------------------------------------
# App Clips (IOS-META-005)
# ---------------------------------------------------------------------------

def _check_app_clips(root: Path, plist: dict) -> List[Finding]:
    """Check App Clip configuration — requires iOS 14+."""
    findings: List[Finding] = []

    # Detect App Clip presence
    app_clip_indicators = list(root.rglob("*.appclip")) + list(root.rglob("AppClip"))
    source = _collect_source(root)
    has_app_clip_code = bool(re.search(r'NSUserActivity|appClip|NSAppClip|app_clip_url', source))

    if app_clip_indicators or has_app_clip_code:
        min_target = plist.get("MinimumOSVersion", "")
        if min_target:
            try:
                major = int(min_target.split(".")[0])
                if major < 14:
                    findings.append(Finding(
                        rule_id="IOS-META-005",
                        severity="FAIL",
                        message=f"App Clips require iOS 14+ but MinimumOSVersion is {min_target}",
                        fix="Set MinimumOSVersion to at least 14.0 for App Clip targets.",
                        reference="Apple Developer Documentation — App Clips",
                    ))
            except (ValueError, IndexError):
                pass

    return findings
