"""
XCResult bundle parser.

Extracts structured data from xcresult bundles using xcresulttool.
"""

import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any


class XCResultParser:
    """
    Parse xcresult bundles to extract build/test data.

    Uses xcresulttool to extract structured JSON data from Apple's
    xcresult bundle format.
    """

    def __init__(self, xcresult_path: Path, stderr: str = ""):
        """
        Initialize parser.

        Args:
            xcresult_path: Path to xcresult bundle
            stderr: Optional stderr output for fallback parsing
        """
        self.xcresult_path = xcresult_path
        self.stderr = stderr

        if xcresult_path and not xcresult_path.exists():
            raise FileNotFoundError(f"XCResult bundle not found: {xcresult_path}")

    def get_build_results(self) -> dict | None:
        """
        Get build results as JSON.

        Returns:
            Parsed JSON dict or None on error
        """
        return self._run_xcresulttool(["get", "build-results"])

    def get_test_results(self) -> dict | None:
        """
        Get test results summary as JSON.

        Returns:
            Parsed JSON dict or None on error
        """
        return self._run_xcresulttool(["get", "test-results", "summary"])

    def get_failed_tests(self) -> list[dict]:
        """
        Get failed test details from xcresult bundle.

        Returns:
            List of dicts with test_name and failure_message.
            Returns [] if parsing fails or no failures found.
        """
        try:
            data = self._run_xcresulttool(["get", "test-results", "tests"])
            if not data:
                return []

            failed = []
            # The structure varies by Xcode version — walk recursively
            nodes = data if isinstance(data, list) else data.get("testNodes", [])
            self._collect_failed_tests(nodes, failed)
            return failed

        except Exception as e:
            print(f"Warning: Could not parse failed tests: {e}", file=sys.stderr)
            return []

    def _collect_failed_tests(self, nodes: list, failed: list[dict]) -> None:
        """Recursively collect failed test cases from test node tree."""
        if not isinstance(nodes, list):
            return

        for node in nodes:
            if not isinstance(node, dict):
                continue

            is_test_case = node.get("nodeType") == "Test Case"
            is_failed = node.get("result") == "Failed"

            if is_test_case and is_failed:
                failed.append(
                    {
                        "test_name": node.get("name", "Unknown"),
                        "failure_message": node.get("details", ""),
                    }
                )

            # Recurse into children
            children = node.get("children", [])
            self._collect_failed_tests(children, failed)

    def get_build_log(self) -> str | None:
        """
        Get build log as plain text.

        Returns:
            Build log string or None on error
        """
        result = self._run_xcresulttool(["get", "log", "--type", "build"], parse_json=False)
        return result if result else None

    def count_issues(self) -> tuple[int, int]:
        """
        Count errors and warnings from build results.

        Returns:
            Tuple of (error_count, warning_count)
        """
        error_count = 0
        warning_count = 0

        build_results = self.get_build_results()

        if build_results:
            try:
                # Try top-level errors/warnings first (newer xcresult format)
                if "errors" in build_results and isinstance(build_results.get("errors"), list):
                    error_count = len(build_results["errors"])
                if "warnings" in build_results and isinstance(build_results.get("warnings"), list):
                    warning_count = len(build_results["warnings"])

                # If not found, try legacy format: actions[0].buildResult.issues
                if error_count == 0 and warning_count == 0:
                    actions = build_results.get("actions", {}).get("_values", [])
                    if actions:
                        build_result = actions[0].get("buildResult", {})
                        issues = build_result.get("issues", {})

                        # Count errors
                        error_summaries = issues.get("errorSummaries", {}).get("_values", [])
                        error_count = len(error_summaries)

                        # Count warnings
                        warning_summaries = issues.get("warningSummaries", {}).get("_values", [])
                        warning_count = len(warning_summaries)

            except (KeyError, IndexError, TypeError) as e:
                print(f"Warning: Could not parse issue counts from xcresult: {e}", file=sys.stderr)

        # If no errors found in xcresult but stderr available, count stderr errors
        if error_count == 0 and self.stderr:
            stderr_errors = self._parse_stderr_errors()
            error_count = len(stderr_errors)

        return (error_count, warning_count)

    def get_errors(self) -> list[dict]:
        """
        Get detailed error information.

        Returns:
            List of error dicts with message, file, line info
        """
        build_results = self.get_build_results()
        errors = []

        # Try to get errors from xcresult
        if build_results:
            try:
                # Try top-level errors first (newer xcresult format)
                if "errors" in build_results and isinstance(build_results.get("errors"), list):
                    for error in build_results["errors"]:
                        errors.append(
                            {
                                "message": error.get("message", "Unknown error"),
                                "type": error.get("issueType", "error"),
                                "location": self._extract_location_from_url(error.get("sourceURL")),
                            }
                        )

                # If not found, try legacy format: actions[0].buildResult.issues
                if not errors:
                    actions = build_results.get("actions", {}).get("_values", [])
                    if actions:
                        build_result = actions[0].get("buildResult", {})
                        issues = build_result.get("issues", {})
                        error_summaries = issues.get("errorSummaries", {}).get("_values", [])

                        for error in error_summaries:
                            errors.append(
                                {
                                    "message": error.get("message", {}).get(
                                        "_value", "Unknown error"
                                    ),
                                    "type": error.get("issueType", {}).get("_value", "error"),
                                    "location": self._extract_location(error),
                                }
                            )

            except (KeyError, IndexError, TypeError) as e:
                print(f"Warning: Could not parse errors from xcresult: {e}", file=sys.stderr)

        # If no errors found in xcresult but stderr available, parse stderr
        if not errors and self.stderr:
            errors = self._parse_stderr_errors()

        return errors

    def get_warnings(self) -> list[dict]:
        """
        Get detailed warning information.

        Returns:
            List of warning dicts with message, file, line info
        """
        build_results = self.get_build_results()
        if not build_results:
            return []

        warnings = []

        try:
            # Try top-level warnings first (newer xcresult format)
            if "warnings" in build_results and isinstance(build_results.get("warnings"), list):
                for warning in build_results["warnings"]:
                    warnings.append(
                        {
                            "message": warning.get("message", "Unknown warning"),
                            "type": warning.get("issueType", "warning"),
                            "location": self._extract_location_from_url(warning.get("sourceURL")),
                        }
                    )

            # If not found, try legacy format: actions[0].buildResult.issues
            if not warnings:
                actions = build_results.get("actions", {}).get("_values", [])
                if not actions:
                    return []

                build_result = actions[0].get("buildResult", {})
                issues = build_result.get("issues", {})
                warning_summaries = issues.get("warningSummaries", {}).get("_values", [])

                for warning in warning_summaries:
                    warnings.append(
                        {
                            "message": warning.get("message", {}).get("_value", "Unknown warning"),
                            "type": warning.get("issueType", {}).get("_value", "warning"),
                            "location": self._extract_location(warning),
                        }
                    )

        except (KeyError, IndexError, TypeError) as e:
            print(f"Warning: Could not parse warnings: {e}", file=sys.stderr)

        return warnings

    def _extract_location(self, issue: dict) -> dict:
        """
        Extract file location from issue.

        Args:
            issue: Issue dict from xcresult

        Returns:
            Location dict with file, line, column
        """
        location = {"file": None, "line": None, "column": None}

        try:
            doc_location = issue.get("documentLocationInCreatingWorkspace", {})
            location["file"] = doc_location.get("url", {}).get("_value")
            location["line"] = doc_location.get("startingLineNumber", {}).get("_value")
            location["column"] = doc_location.get("startingColumnNumber", {}).get("_value")
        except (KeyError, TypeError):
            pass

        return location

    def _extract_location_from_url(self, source_url: str | None) -> dict:
        """
        Extract file location from sourceURL (newer xcresult format).

        Args:
            source_url: Source URL like "file:///path/to/file.swift#StartingLineNumber=134&..."

        Returns:
            Location dict with file, line, column
        """
        location = {"file": None, "line": None, "column": None}

        if not source_url:
            return location

        try:
            # Split URL and fragment
            if "#" in source_url:
                file_part, fragment = source_url.split("#", 1)

                # Extract file path
                location["file"] = file_part.replace("file://", "")

                # Parse fragment parameters
                params = {}
                for param in fragment.split("&"):
                    if "=" in param:
                        key, value = param.split("=", 1)
                        params[key] = value

                # Extract line and column
                location["line"] = (
                    int(params.get("StartingLineNumber", 0)) + 1
                    if "StartingLineNumber" in params
                    else None
                )
                location["column"] = (
                    int(params.get("StartingColumnNumber", 0)) + 1
                    if "StartingColumnNumber" in params
                    else None
                )
            else:
                # No fragment, just file path
                location["file"] = source_url.replace("file://", "")

        except (ValueError, AttributeError):
            pass

        return location

    def _run_xcresulttool(self, args: list[str], parse_json: bool = True) -> Any | None:
        """
        Run xcresulttool command.

        Args:
            args: Command arguments (after 'xcresulttool')
            parse_json: Whether to parse output as JSON

        Returns:
            Parsed JSON dict, plain text, or None on error
        """
        if not self.xcresult_path:
            return None

        cmd = ["xcrun", "xcresulttool"] + args + ["--path", str(self.xcresult_path)]

        try:
            result = subprocess.run(cmd, capture_output=True, text=True, check=True)

            if parse_json:
                return json.loads(result.stdout)
            return result.stdout

        except subprocess.CalledProcessError as e:
            print(f"Error running xcresulttool: {e}", file=sys.stderr)
            print(f"stderr: {e.stderr}", file=sys.stderr)
            return None
        except json.JSONDecodeError as e:
            print(f"Error parsing JSON from xcresulttool: {e}", file=sys.stderr)
            return None

    def _parse_stderr_errors(self) -> list[dict]:
        """
        Parse common errors from stderr output as fallback.

        Returns:
            List of error dicts parsed from stderr
        """
        errors = []

        if not self.stderr:
            return errors

        # Pattern 0: Swift/Clang compilation errors (e.g., "/path/file.swift:135:59: error: message")
        compilation_error_pattern = (
            r"^(?P<file>[^:]+):(?P<line>\d+):(?P<column>\d+):\s*error:\s*(?P<message>.+?)$"
        )
        for match in re.finditer(compilation_error_pattern, self.stderr, re.MULTILINE):
            errors.append(
                {
                    "message": match.group("message").strip(),
                    "type": "compilation",
                    "location": {
                        "file": match.group("file"),
                        "line": int(match.group("line")),
                        "column": int(match.group("column")),
                    },
                }
            )

        # Pattern 1: xcodebuild top-level errors (e.g., "xcodebuild: error: Unable to find...")
        xcodebuild_error_pattern = r"xcodebuild:\s*error:\s*(?P<message>.*?)(?:\n\n|\Z)"
        for match in re.finditer(xcodebuild_error_pattern, self.stderr, re.DOTALL):
            message = match.group("message").strip()
            # Clean up multi-line messages
            message = " ".join(line.strip() for line in message.split("\n") if line.strip())
            errors.append(
                {
                    "message": message,
                    "type": "build",
                    "location": {"file": None, "line": None, "column": None},
                }
            )

        # Pattern 2: Provisioning profile errors
        provisioning_pattern = r"error:.*?provisioning profile.*?(?:doesn't|does not|cannot).*?(?P<message>.*?)(?:\n|$)"
        for match in re.finditer(provisioning_pattern, self.stderr, re.IGNORECASE):
            errors.append(
                {
                    "message": f"Provisioning profile error: {match.group('message').strip()}",
                    "type": "provisioning",
                    "location": {"file": None, "line": None, "column": None},
                }
            )

        # Pattern 3: Code signing errors
        signing_pattern = r"error:.*?(?:code sign|signing).*?(?P<message>.*?)(?:\n|$)"
        for match in re.finditer(signing_pattern, self.stderr, re.IGNORECASE):
            errors.append(
                {
                    "message": f"Code signing error: {match.group('message').strip()}",
                    "type": "signing",
                    "location": {"file": None, "line": None, "column": None},
                }
            )

        # Pattern 4: Generic compilation errors (but not if already captured)
        if not errors:
            generic_error_pattern = r"^(?:\*\*\s)?(?:error|❌):\s*(?P<message>.*?)(?:\n|$)"
            for match in re.finditer(generic_error_pattern, self.stderr, re.MULTILINE):
                message = match.group("message").strip()
                errors.append(
                    {
                        "message": message,
                        "type": "build",
                        "location": {"file": None, "line": None, "column": None},
                    }
                )

        # Pattern 5: Specific "No profiles" error
        if "No profiles for" in self.stderr:
            no_profile_pattern = r"No profiles for '(?P<bundle_id>.*?)' were found"
            for match in re.finditer(no_profile_pattern, self.stderr):
                errors.append(
                    {
                        "message": f"No provisioning profile found for bundle ID '{match.group('bundle_id')}'",
                        "type": "provisioning",
                        "location": {"file": None, "line": None, "column": None},
                    }
                )

        return errors
