#!/usr/bin/env python3

"""Meson Build System Introspection and Dependency Discovery Module.

This module parses Meson build system introspection files to extract project metadata,
declared dependencies, subprojects, and build targets. It reads structured JSON data
generated by Meson during the build process and enriches dependency information using
pkg-config queries. Additionally, it processes .wrap files to gather metadata about
bundled subproject libraries including their source URLs and version tags.

The module serves as the foundation for SBOM generation by providing the initial
dependency graph structure that is later expanded and normalised by the resolver module.
"""

import json
import re
from pathlib import Path

from pkgconfig import PcCache

# Regular expression pattern to extract version information from meson.build files.
# Matches: project('name', version: '1.2.3', ...)
# This pattern is case-insensitive and handles multi-line project declarations.
_PROJ_VER = re.compile(
    r"project\s*\([^)]*version\s*:\s*['\"]([^'\"]+)['\"][^)]*\)",
    re.IGNORECASE | re.DOTALL
)


def _parse_wrap(path: Path) -> dict:
    """Parse a Meson .wrap file to extract subproject metadata.

    Meson uses .wrap files to define external dependencies and subprojects.
    This function extracts key metadata including the subproject directory name,
    source URL, and version tag information.

    Args:
        path: Path to the .wrap file to parse.

    Returns:
        Dictionary containing parsed metadata with keys:
            - 'directory': Subproject directory name
            - 'source_url': URL where the subproject source can be downloaded
            - 'source_tag': Version tag or commit reference
            - 'version': Extracted from source_tag if available
            - 'url': Alias for source_url

    Note:
        Lines starting with '#' are treated as comments and ignored.
        Only lines containing '=' are processed as key-value pairs.
    """
    meta = {}
    if not path.exists():
        return meta

    # Parse each line in the .wrap file as a key-value pair
    for line in path.read_text().splitlines():
        line = line.strip()
        # Skip empty lines, comments, and lines without assignment operator
        if not line or line.startswith('#') or '=' not in line:
            continue
        key, value = [x.strip() for x in line.split('=', 1)]
        if key in ('directory', 'source_url', 'source_tag'):
            meta[key] = value

    # Create convenient aliases for common fields used in SPDX generation
    if 'source_tag' in meta:
        meta['version'] = meta['source_tag']
    if 'source_url' in meta:
        meta['url'] = meta['source_url']

    return meta


def _json_read(path: Path):
    """Safely read and parse a JSON file.

    Args:
        path: Path to the JSON file.

    Returns:
        Parsed JSON content as a Python object (dict/list), or None if the file
        cannot be read or parsed.
    """
    try:
        return json.loads(path.read_text(encoding='utf-8'))
    except (OSError, json.JSONDecodeError, UnicodeDecodeError):
        return None


def discover_meson(build_dir: Path, project_dir: Path) -> tuple[str, str, dict, list, list, dict]:
    """Discover and extract comprehensive project metadata from Meson build system.

    This function performs complete introspection of a Meson-based project by reading
    the structured JSON files generated during the build process. It extracts project
    information, dependencies, subprojects, build targets, and enriches this data with
    pkg-config metadata and .wrap file information.

    Args:
        build_dir: Path to the Meson build directory (typically 'build/').
        project_dir: Path to the project root directory containing subprojects/.

    Returns:
        A tuple containing six elements:
            - proj_name (str): Project name extracted from meson-info.json
            - proj_ver (str): Project version string
            - deps (dict): Dictionary mapping dependency names to their metadata
            - intro_targets (list): List of build target definitions from Meson
            - binaries (list): List of Path objects to built executable/library files
            - wraps (dict): Dictionary mapping .wrap file stems to their parsed metadata

    Raises:
        SystemExit: If the meson-info directory is missing from build_dir.

    Note:
        This function queries pkg-config for packages detected as 'pkg-config' type
        to enrich version and URL information. It also scans subproject directories
        to extract version information from meson.build files.
    """
    meson_info = build_dir / 'meson-info'
    if not meson_info.exists():
        raise SystemExit(f'{meson_info} missing.')

    # Retrieve base project data from meson-info.json
    # This file contains the project name, version, and build configuration details
    info = _json_read(meson_info / 'meson-info.json') or {}
    project_info = info.get('projectinfo', {})
    proj_name = project_info.get('descriptive_name') or project_info.get('name') or project_dir.resolve().name
    proj_ver = project_info.get('version') or 'UNKNOWN'

    # Retrieve dependencies and target info from Meson introspection files
    # intro-dependencies.json: Contains all dependencies declared in meson.build
    # intro-targets.json: Contains information about build targets (executables, libraries)
    intro_deps = _json_read(meson_info / 'intro-dependencies.json') or []
    intro_targets = _json_read(meson_info / 'intro-targets.json') or []

    # Store base project dependencies in a normalised dictionary structure
    # Each dependency entry contains name, type, and version information
    deps: dict[str, dict] = {}
    for dep in intro_deps:
        name = dep.get('name') or dep.get('depname') or dep.get('project_name')
        if not name:
            continue
        deps.setdefault(name, {
            'name': name,
            'type': dep.get('type') or dep.get('detection_method'),
            'version': dep.get('version') or dep.get('version_string'),
        })

    # Process subprojects and extract metadata from .wrap files
    # .wrap files define how to download and integrate external dependencies
    subprojects_dir = project_dir / 'subprojects'

    # Wrap files are not always used in a build. Statically analyzing
    # them can result in an SBOM falsely claiming that the build has a
    # certain dependency. So as a quick fix, let us reset the list of
    # wrap files to empty.

    # wrap_files = subprojects_dir.glob('*.wrap') if subprojects_dir.exists() else []
    # wraps = {wrap_file.stem: _parse_wrap(wrap_file) for wrap_file in wrap_files}
    wraps = {}

    # Scan subproject directories to extract version information from meson.build files
    # This provides version data for subprojects that may not be available elsewhere
    if subprojects_dir.exists():
        for child in subprojects_dir.iterdir():
            if not child.is_dir():
                continue
            name = child.name
            entry = deps.setdefault(name, {'name': name, 'type': 'subproject'})
            meson_build_file = child / 'meson.build'
            if meson_build_file.exists():
                # Extract version from project() declaration in meson.build
                version_match = _PROJ_VER.search(meson_build_file.read_text(errors='ignore'))
                if version_match and not entry.get('version'):
                    entry['version'] = version_match.group(1)

    # Update dependency entries with .wrap metadata (URL, version, etc.)
    # This ensures dependencies have complete metadata for SPDX generation
    for wrap_name, wrap_metadata in wraps.items():
        package_name = wrap_metadata.get('directory') or wrap_name
        package_entry = deps.setdefault(package_name, {'name': package_name, 'type': 'subproject'})
        package_entry.setdefault('version', wrap_metadata.get('version'))
        package_entry.setdefault('url', wrap_metadata.get('url'))

    # Query pkg-config cache to enrich dependencies detected via pkg-config
    # This adds version numbers, URLs, and .pc file locations to pkg-config dependencies
    cache = PcCache()
    for dep_name, dep_info in list(deps.items()):
        if 'pkg' in (dep_info.get('type') or '').lower():
            for info_key, info_value in cache.info(dep_name).items():
                if info_value and not dep_info.get(info_key):
                    dep_info[info_key] = info_value

    # Find and gather the full set of built binaries (executables and shared libraries)
    # These will be analysed later to discover runtime system library dependencies
    binaries = []
    for target in intro_targets or []:
        if target.get('type') in ('executable', 'shared library', 'shared module'):
            target_filename = target.get('filename')
            if not target_filename:
                continue

            # Handle both single filenames and lists of filenames
            filename_list = target_filename if isinstance(target_filename, list) else [target_filename]
            for filename in filename_list:
                file_path = build_dir / filename
                if file_path.exists():
                    binaries.append(file_path)

    return proj_name, proj_ver, deps, intro_targets, binaries, wraps
