# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""License compliance scanner.

Scans dependencies to detect and validate software licenses using SPDX identifiers.
Helps organizations enforce license compliance policies.
"""

from __future__ import annotations

import json
import logging
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from pathlib import Path
from typing import Any

from .manifest_parser import Dependency, Ecosystem, ManifestParser

logger = logging.getLogger(__name__)


class LicenseCategory(Enum):
    """License categories for compliance."""

    PERMISSIVE = "permissive"  # MIT, Apache, BSD - generally safe
    WEAK_COPYLEFT = "weak_copyleft"  # LGPL, MPL - library linking OK
    STRONG_COPYLEFT = "strong_copyleft"  # GPL, AGPL - requires source release
    PROPRIETARY = "proprietary"  # Commercial/proprietary licenses
    PUBLIC_DOMAIN = "public_domain"  # CC0, Unlicense
    UNKNOWN = "unknown"  # Unrecognized licenses


class ComplianceStatus(Enum):
    """License compliance status."""

    PERMITTED = "permitted"  # License is allowed
    RESTRICTED = "restricted"  # License requires review
    INCOMPATIBLE = "incompatible"  # License is not allowed
    UNKNOWN = "unknown"  # License not recognized


# Common SPDX license identifiers and their categories
SPDX_LICENSE_DATA: dict[str, dict[str, Any]] = {
    # Permissive licenses
    "MIT": {"category": LicenseCategory.PERMISSIVE, "name": "MIT License"},
    "Apache-2.0": {"category": LicenseCategory.PERMISSIVE, "name": "Apache License 2.0"},
    "BSD-2-Clause": {"category": LicenseCategory.PERMISSIVE, "name": "BSD 2-Clause"},
    "BSD-3-Clause": {"category": LicenseCategory.PERMISSIVE, "name": "BSD 3-Clause"},
    "ISC": {"category": LicenseCategory.PERMISSIVE, "name": "ISC License"},
    "0BSD": {"category": LicenseCategory.PERMISSIVE, "name": "Zero-Clause BSD"},
    "Zlib": {"category": LicenseCategory.PERMISSIVE, "name": "zlib License"},
    "BSL-1.0": {"category": LicenseCategory.PERMISSIVE, "name": "Boost Software License 1.0"},
    "X11": {"category": LicenseCategory.PERMISSIVE, "name": "X11 License"},
    "Artistic-2.0": {"category": LicenseCategory.PERMISSIVE, "name": "Artistic License 2.0"},
    "Python-2.0": {"category": LicenseCategory.PERMISSIVE, "name": "Python License 2.0"},
    "Unlicense": {"category": LicenseCategory.PUBLIC_DOMAIN, "name": "The Unlicense"},
    "CC0-1.0": {"category": LicenseCategory.PUBLIC_DOMAIN, "name": "CC0 1.0 Universal"},
    "WTFPL": {"category": LicenseCategory.PUBLIC_DOMAIN, "name": "WTFPL"},
    # Weak copyleft
    "LGPL-2.0-only": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "LGPL 2.0"},
    "LGPL-2.0-or-later": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "LGPL 2.0+"},
    "LGPL-2.1-only": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "LGPL 2.1"},
    "LGPL-2.1-or-later": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "LGPL 2.1+"},
    "LGPL-3.0-only": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "LGPL 3.0"},
    "LGPL-3.0-or-later": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "LGPL 3.0+"},
    "MPL-2.0": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "Mozilla Public License 2.0"},
    "EPL-1.0": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "Eclipse Public License 1.0"},
    "EPL-2.0": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "Eclipse Public License 2.0"},
    "CDDL-1.0": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "CDDL 1.0"},
    "CDDL-1.1": {"category": LicenseCategory.WEAK_COPYLEFT, "name": "CDDL 1.1"},
    # Strong copyleft
    "GPL-2.0-only": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "GPL 2.0"},
    "GPL-2.0-or-later": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "GPL 2.0+"},
    "GPL-3.0-only": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "GPL 3.0"},
    "GPL-3.0-or-later": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "GPL 3.0+"},
    "AGPL-3.0-only": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "AGPL 3.0"},
    "AGPL-3.0-or-later": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "AGPL 3.0+"},
    "SSPL-1.0": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "SSPL 1.0"},
    "OSL-3.0": {"category": LicenseCategory.STRONG_COPYLEFT, "name": "Open Software License 3.0"},
    # Proprietary/Commercial
    "BUSL-1.1": {"category": LicenseCategory.PROPRIETARY, "name": "Business Source License 1.1"},
    "Elastic-2.0": {"category": LicenseCategory.PROPRIETARY, "name": "Elastic License 2.0"},
    "PROPRIETARY": {"category": LicenseCategory.PROPRIETARY, "name": "Proprietary License"},
    "COMMERCIAL": {"category": LicenseCategory.PROPRIETARY, "name": "Commercial License"},
}

# Common license aliases and variations
LICENSE_ALIASES: dict[str, str] = {
    # MIT variations
    "mit": "MIT",
    "mit license": "MIT",
    "the mit license": "MIT",
    "expat": "MIT",
    # Apache variations
    "apache": "Apache-2.0",
    "apache 2": "Apache-2.0",
    "apache 2.0": "Apache-2.0",
    "apache-2": "Apache-2.0",
    "apache license 2.0": "Apache-2.0",
    "asl 2.0": "Apache-2.0",
    # BSD variations
    "bsd": "BSD-3-Clause",
    "bsd 2": "BSD-2-Clause",
    "bsd 3": "BSD-3-Clause",
    "bsd-2": "BSD-2-Clause",
    "bsd-3": "BSD-3-Clause",
    "bsd 2-clause": "BSD-2-Clause",
    "bsd 3-clause": "BSD-3-Clause",
    "new bsd": "BSD-3-Clause",
    "simplified bsd": "BSD-2-Clause",
    # GPL variations
    "gpl": "GPL-3.0-only",
    "gpl2": "GPL-2.0-only",
    "gpl3": "GPL-3.0-only",
    "gpl-2": "GPL-2.0-only",
    "gpl-3": "GPL-3.0-only",
    "gplv2": "GPL-2.0-only",
    "gplv3": "GPL-3.0-only",
    "gnu gpl": "GPL-3.0-only",
    "gnu gpl v2": "GPL-2.0-only",
    "gnu gpl v3": "GPL-3.0-only",
    "gnu general public license": "GPL-3.0-only",
    # LGPL variations
    "lgpl": "LGPL-3.0-only",
    "lgpl2": "LGPL-2.0-only",
    "lgpl3": "LGPL-3.0-only",
    "lgplv2": "LGPL-2.0-only",
    "lgplv3": "LGPL-3.0-only",
    # ISC variations
    "isc": "ISC",
    "isc license": "ISC",
    # MPL variations
    "mpl": "MPL-2.0",
    "mpl 2.0": "MPL-2.0",
    "mpl-2": "MPL-2.0",
    "mozilla": "MPL-2.0",
    "mozilla public license": "MPL-2.0",
    # Public domain
    "public domain": "Unlicense",
    "cc0": "CC0-1.0",
    "cc0 1.0": "CC0-1.0",
    "unlicense": "Unlicense",
    "unlicensed": "Unlicense",
    # AGPL
    "agpl": "AGPL-3.0-only",
    "agpl3": "AGPL-3.0-only",
    "agplv3": "AGPL-3.0-only",
}


def normalize_license(license_str: str) -> str:
    """Normalize a license string to SPDX identifier.

    Args:
        license_str: Raw license string from manifest.

    Returns:
        Normalized SPDX identifier or original string if unknown.
    """
    if not license_str:
        return ""

    # Clean up the string
    cleaned = license_str.strip().lower()

    # Check aliases first
    if cleaned in LICENSE_ALIASES:
        return LICENSE_ALIASES[cleaned]

    # Check if it's already a valid SPDX identifier (case-insensitive)
    for spdx_id in SPDX_LICENSE_DATA:
        if cleaned == spdx_id.lower():
            return spdx_id

    # Handle SPDX expressions like "MIT OR Apache-2.0"
    if " or " in cleaned or " and " in cleaned:
        return license_str.strip()  # Return as-is for expression

    # Return original if not found
    return license_str.strip()


def get_license_category(spdx_id: str) -> LicenseCategory:
    """Get the category of a license.

    Args:
        spdx_id: SPDX license identifier.

    Returns:
        License category.
    """
    normalized = normalize_license(spdx_id)

    if normalized in SPDX_LICENSE_DATA:
        return SPDX_LICENSE_DATA[normalized]["category"]

    # Check for expressions
    lower = spdx_id.lower()
    if "gpl" in lower and "lgpl" not in lower:
        if "agpl" in lower:
            return LicenseCategory.STRONG_COPYLEFT
        return LicenseCategory.STRONG_COPYLEFT
    if "lgpl" in lower:
        return LicenseCategory.WEAK_COPYLEFT
    if "mpl" in lower:
        return LicenseCategory.WEAK_COPYLEFT

    return LicenseCategory.UNKNOWN


def get_license_name(spdx_id: str) -> str:
    """Get the human-readable name of a license.

    Args:
        spdx_id: SPDX license identifier.

    Returns:
        Human-readable license name.
    """
    normalized = normalize_license(spdx_id)

    if normalized in SPDX_LICENSE_DATA:
        return SPDX_LICENSE_DATA[normalized]["name"]

    return spdx_id


@dataclass
class LicenseFinding:
    """A license finding for a dependency."""

    dependency_name: str
    dependency_version: str
    ecosystem: str
    source_file: str
    license_id: str
    license_name: str
    category: LicenseCategory
    status: ComplianceStatus
    is_dev: bool = False
    confidence: float = 1.0  # 0-1, how confident we are in the detection
    error: str | None = None

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "dependency_name": self.dependency_name,
            "dependency_version": self.dependency_version,
            "ecosystem": self.ecosystem,
            "source_file": self.source_file,
            "license_id": self.license_id,
            "license_name": self.license_name,
            "category": self.category.value,
            "status": self.status.value,
            "is_dev": self.is_dev,
            "confidence": self.confidence,
            "error": self.error,
        }


@dataclass
class LicenseScanResult:
    """Result of a license compliance scan."""

    scan_id: str
    timestamp: str
    repository: str
    ref: str = ""
    total_dependencies: int = 0
    total_licenses_detected: int = 0

    # Counts by status
    permitted_count: int = 0
    restricted_count: int = 0
    incompatible_count: int = 0
    unknown_count: int = 0

    # Counts by category
    permissive_count: int = 0
    weak_copyleft_count: int = 0
    strong_copyleft_count: int = 0
    proprietary_count: int = 0
    public_domain_count: int = 0

    findings: list[LicenseFinding] = field(default_factory=list)
    manifests_scanned: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)

    # Unique licenses found
    unique_licenses: set[str] = field(default_factory=set)

    @property
    def passed(self) -> bool:
        """Check if scan passed (no incompatible licenses)."""
        return self.incompatible_count == 0

    @property
    def compliance_rate(self) -> float:
        """Percentage of dependencies with permitted licenses."""
        if self.total_dependencies == 0:
            return 100.0
        return (
            (self.total_dependencies - self.incompatible_count - self.unknown_count)
            / self.total_dependencies
        ) * 100

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "scan_id": self.scan_id,
            "timestamp": self.timestamp,
            "repository": self.repository,
            "ref": self.ref,
            "passed": self.passed,
            "compliance_rate": round(self.compliance_rate, 1),
            "total_dependencies": self.total_dependencies,
            "total_licenses_detected": self.total_licenses_detected,
            "status_counts": {
                "permitted": self.permitted_count,
                "restricted": self.restricted_count,
                "incompatible": self.incompatible_count,
                "unknown": self.unknown_count,
            },
            "category_counts": {
                "permissive": self.permissive_count,
                "weak_copyleft": self.weak_copyleft_count,
                "strong_copyleft": self.strong_copyleft_count,
                "proprietary": self.proprietary_count,
                "public_domain": self.public_domain_count,
            },
            "unique_licenses": sorted(self.unique_licenses),
            "findings": [f.to_dict() for f in self.findings],
            "manifests_scanned": self.manifests_scanned,
            "errors": self.errors,
        }


class LicensePolicy:
    """License compliance policy configuration."""

    def __init__(
        self,
        permitted_categories: list[LicenseCategory] | None = None,
        permitted_licenses: list[str] | None = None,
        denied_licenses: list[str] | None = None,
        allow_unknown: bool = False,
    ):
        """Initialize license policy.

        Args:
            permitted_categories: Categories allowed by default.
            permitted_licenses: Specific licenses always allowed.
            denied_licenses: Specific licenses always denied.
            allow_unknown: Whether to allow unknown licenses.
        """
        self.permitted_categories = permitted_categories or [
            LicenseCategory.PERMISSIVE,
            LicenseCategory.PUBLIC_DOMAIN,
        ]
        self.permitted_licenses = set(permitted_licenses or [])
        self.denied_licenses = set(denied_licenses or [])
        self.allow_unknown = allow_unknown

    def get_status(self, license_id: str, category: LicenseCategory) -> ComplianceStatus:
        """Determine compliance status for a license.

        Args:
            license_id: SPDX license identifier.
            category: License category.

        Returns:
            Compliance status.
        """
        normalized = normalize_license(license_id)

        # Check explicit deny list
        if normalized in self.denied_licenses:
            return ComplianceStatus.INCOMPATIBLE

        # Check explicit allow list
        if normalized in self.permitted_licenses:
            return ComplianceStatus.PERMITTED

        # Check category
        if category == LicenseCategory.UNKNOWN:
            return ComplianceStatus.UNKNOWN if self.allow_unknown else ComplianceStatus.RESTRICTED

        if category in self.permitted_categories:
            return ComplianceStatus.PERMITTED

        if category == LicenseCategory.STRONG_COPYLEFT:
            return ComplianceStatus.INCOMPATIBLE

        if category == LicenseCategory.PROPRIETARY:
            return ComplianceStatus.INCOMPATIBLE

        return ComplianceStatus.RESTRICTED


class LicenseScanner:
    """Scans dependencies for license compliance."""

    # Patterns to extract license from manifest files
    LICENSE_PATTERNS: dict[str, list[str]] = {
        "package.json": ["license"],
        "package-lock.json": ["license"],
        "composer.json": ["license"],
        "Cargo.toml": ["license", "license-file"],
        "pyproject.toml": ["license"],
    }

    def __init__(
        self,
        manifest_parser: ManifestParser | None = None,
        policy: LicensePolicy | None = None,
        scan_dev_dependencies: bool = False,
    ):
        """Initialize license scanner.

        Args:
            manifest_parser: Optional manifest parser instance.
            policy: License compliance policy.
            scan_dev_dependencies: Whether to scan dev dependencies.
        """
        self._parser = manifest_parser or ManifestParser()
        self._policy = policy or LicensePolicy()
        self._scan_dev = scan_dev_dependencies

    def scan_files(
        self,
        files: dict[str, str],
        repository: str = "",
        ref: str = "",
        scan_id: str | None = None,
    ) -> LicenseScanResult:
        """Scan files for license information.

        Args:
            files: Dictionary mapping filename to content.
            repository: Repository name.
            ref: Git ref being scanned.
            scan_id: Optional scan ID.

        Returns:
            LicenseScanResult with findings.
        """
        if scan_id is None:
            scan_id = self._generate_scan_id()

        result = LicenseScanResult(
            scan_id=scan_id,
            timestamp=datetime.now(UTC).isoformat(),
            repository=repository,
            ref=ref,
        )

        # Track seen dependencies to avoid duplicates
        seen_deps: set[tuple[str, str, str]] = set()

        for filename, content in files.items():
            base_name = Path(filename).name

            # Parse dependencies
            try:
                deps = self._parser.parse(content, filename)
                result.manifests_scanned.append(filename)
            except Exception as e:
                result.errors.append(f"Error parsing {filename}: {e}")
                continue

            for dep in deps:
                if dep.is_dev and not self._scan_dev:
                    continue

                key = (dep.ecosystem.value, dep.name, dep.version)
                if key in seen_deps:
                    continue
                seen_deps.add(key)

                result.total_dependencies += 1

                # Get license for dependency
                license_id = self._get_dependency_license(dep, content, base_name)
                if not license_id:
                    license_id = "UNKNOWN"

                normalized = normalize_license(license_id)
                category = get_license_category(normalized)
                license_name = get_license_name(normalized)
                status = self._policy.get_status(normalized, category)

                finding = LicenseFinding(
                    dependency_name=dep.name,
                    dependency_version=dep.version,
                    ecosystem=dep.ecosystem.value,
                    source_file=dep.source_file,
                    license_id=normalized,
                    license_name=license_name,
                    category=category,
                    status=status,
                    is_dev=dep.is_dev,
                    confidence=0.9 if license_id != "UNKNOWN" else 0.0,
                )

                result.findings.append(finding)
                result.unique_licenses.add(normalized)

                if license_id != "UNKNOWN":
                    result.total_licenses_detected += 1

                # Update counts
                self._update_counts(result, status, category)

        return result

    def scan_directory(
        self,
        directory: str | Path,
        repository: str = "",
        ref: str = "",
        scan_id: str | None = None,
    ) -> LicenseScanResult:
        """Scan a directory for license information.

        Args:
            directory: Path to directory.
            repository: Repository name.
            ref: Git ref.
            scan_id: Optional scan ID.

        Returns:
            LicenseScanResult with findings.
        """
        directory = Path(directory)
        files: dict[str, str] = {}

        # Common manifest files to look for
        manifest_files = [
            "package.json",
            "package-lock.json",
            "requirements.txt",
            "Pipfile",
            "pyproject.toml",
            "Cargo.toml",
            "Cargo.lock",
            "go.mod",
            "go.sum",
            "composer.json",
            "composer.lock",
            "Gemfile",
            "Gemfile.lock",
            "pom.xml",
        ]

        for manifest_name in manifest_files:
            manifest_path = directory / manifest_name
            if manifest_path.exists():
                try:
                    files[str(manifest_path)] = manifest_path.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {manifest_path}: {e}")

        return self.scan_files(
            files,
            repository=repository or str(directory),
            ref=ref,
            scan_id=scan_id,
        )

    def _extract_project_license(self, content: str, filename: str) -> str | None:
        """Extract the project's own license from a manifest.

        Args:
            content: File content.
            filename: Manifest filename.

        Returns:
            License string if found.
        """
        if filename == "package.json":
            try:
                data = json.loads(content)
                return data.get("license")
            except json.JSONDecodeError:
                pass

        elif filename == "Cargo.toml":
            match = re.search(r'^license\s*=\s*"([^"]+)"', content, re.MULTILINE)
            if match:
                return match.group(1)

        elif filename == "pyproject.toml":
            match = re.search(r'license\s*=\s*"([^"]+)"', content)
            if match:
                return match.group(1)
            match = re.search(r'license\s*=\s*\{text\s*=\s*"([^"]+)"', content)
            if match:
                return match.group(1)

        elif filename == "composer.json":
            try:
                data = json.loads(content)
                license_val = data.get("license")
                if isinstance(license_val, list):
                    return " OR ".join(license_val)
                return license_val
            except json.JSONDecodeError:
                pass

        return None

    def _get_dependency_license(self, dep: Dependency, content: str, filename: str) -> str | None:
        """Get license for a specific dependency.

        In a real implementation, this would query package registries.
        For now, we use known license mappings for common packages.

        Args:
            dep: The dependency.
            content: Manifest content.
            filename: Manifest filename.

        Returns:
            License identifier if found.
        """
        # Well-known package licenses (common examples)
        known_licenses: dict[str, dict[str, str]] = {
            # npm packages
            "react": {"npm": "MIT"},
            "react-dom": {"npm": "MIT"},
            "lodash": {"npm": "MIT"},
            "express": {"npm": "MIT"},
            "axios": {"npm": "MIT"},
            "typescript": {"npm": "Apache-2.0"},
            "webpack": {"npm": "MIT"},
            "eslint": {"npm": "MIT"},
            "jest": {"npm": "MIT"},
            "prettier": {"npm": "MIT"},
            "next": {"npm": "MIT"},
            "tailwindcss": {"npm": "MIT"},
            "@types/node": {"npm": "MIT"},
            "@types/react": {"npm": "MIT"},
            # Python packages
            "requests": {"pypi": "Apache-2.0"},
            "django": {"pypi": "BSD-3-Clause"},
            "flask": {"pypi": "BSD-3-Clause"},
            "numpy": {"pypi": "BSD-3-Clause"},
            "pandas": {"pypi": "BSD-3-Clause"},
            "pytest": {"pypi": "MIT"},
            "black": {"pypi": "MIT"},
            "boto3": {"pypi": "Apache-2.0"},
            "pydantic": {"pypi": "MIT"},
            "fastapi": {"pypi": "MIT"},
            # Rust packages
            "serde": {"cargo": "MIT OR Apache-2.0"},
            "tokio": {"cargo": "MIT"},
            "clap": {"cargo": "MIT OR Apache-2.0"},
            "reqwest": {"cargo": "MIT OR Apache-2.0"},
            # Go packages
            "github.com/gin-gonic/gin": {"go": "MIT"},
            "github.com/gorilla/mux": {"go": "BSD-3-Clause"},
        }

        ecosystem_key = dep.ecosystem.value.lower()
        if dep.name in known_licenses:
            licenses = known_licenses[dep.name]
            if ecosystem_key in licenses:
                return licenses[ecosystem_key]

        # For package-lock.json, try to extract from nested structure
        if filename == "package-lock.json" and dep.ecosystem == Ecosystem.NPM:
            try:
                data = json.loads(content)
                # v2/v3 lockfile format
                packages = data.get("packages", {})
                pkg_key = f"node_modules/{dep.name}"
                if pkg_key in packages:
                    return packages[pkg_key].get("license")
                # v1 lockfile format
                deps = data.get("dependencies", {})
                if dep.name in deps:
                    return deps[dep.name].get("license")
            except json.JSONDecodeError:
                pass

        return None

    def _update_counts(
        self, result: LicenseScanResult, status: ComplianceStatus, category: LicenseCategory
    ) -> None:
        """Update result counts based on status and category."""
        # Status counts
        if status == ComplianceStatus.PERMITTED:
            result.permitted_count += 1
        elif status == ComplianceStatus.RESTRICTED:
            result.restricted_count += 1
        elif status == ComplianceStatus.INCOMPATIBLE:
            result.incompatible_count += 1
        else:
            result.unknown_count += 1

        # Category counts
        if category == LicenseCategory.PERMISSIVE:
            result.permissive_count += 1
        elif category == LicenseCategory.WEAK_COPYLEFT:
            result.weak_copyleft_count += 1
        elif category == LicenseCategory.STRONG_COPYLEFT:
            result.strong_copyleft_count += 1
        elif category == LicenseCategory.PROPRIETARY:
            result.proprietary_count += 1
        elif category == LicenseCategory.PUBLIC_DOMAIN:
            result.public_domain_count += 1

    def _generate_scan_id(self) -> str:
        """Generate a unique scan ID."""
        import uuid

        return f"license_scan_{uuid.uuid4().hex[:12]}"


def scan_licenses(
    files: dict[str, str],
    repository: str = "",
    ref: str = "",
    include_dev: bool = False,
) -> LicenseScanResult:
    """Convenience function to scan files for licenses.

    Args:
        files: Dictionary mapping filename to content.
        repository: Repository name.
        ref: Git ref.
        include_dev: Whether to include dev dependencies.

    Returns:
        LicenseScanResult with findings.
    """
    scanner = LicenseScanner(scan_dev_dependencies=include_dev)
    return scanner.scan_files(files, repository=repository, ref=ref)
