import asyncio
import json
import logging
import sys
from collections.abc import Iterable
from pathlib import Path
from typing import List, Tuple

from aiomultiprocess import Pool

from checkov.common.bridgecrew.platform_integration import bc_integration, BcPlatformIntegration
from checkov.common.bridgecrew.vulnerability_scanning.integrations.package_scanning import package_scanning_integration

SUPPORTED_PACKAGE_FILES = {
    "bower.json",
    "build.gradle",
    "build.gradle.kts",
    "go.sum",
    "gradle.properties",
    "METADATA",
    "npm-shrinkwrap.json",
    "package.json",
    "package-lock.json",
    "pom.xml",
    "requirements.txt",
}
TWISTCLI_FILE_NAME = 'twistcli'


class PackageScanner:
    def __init__(self) -> None:
        self.twistcli_path = Path(TWISTCLI_FILE_NAME)

    def setup_scan(self) -> None:
        try:
            if not self.twistcli_path.exists():
                package_scanning_integration.download_twistcli(self.twistcli_path)
        except Exception as e:
            logging.error(f"Failed to setup package scanning\n{e}")
            raise e

    def cleanup_scan(self) -> None:
        if self.twistcli_path.exists():
            self.twistcli_path.unlink()
        logging.info('twistcli file removed')

    async def run_scan(
        self,
        command: str,
        bc_platform_integration: BcPlatformIntegration,
        bc_api_key: str,
        input_path: Path,
        output_path: Path,
    ) -> int:
        logging.info(f"Start to scan package file {input_path}")
        process = await asyncio.create_subprocess_shell(command)

        exit_code = await process.wait()

        if exit_code:
            logging.error(f"Failed to scan package file {input_path}")
            return exit_code

        logging.info(f"Successfully scanned package file {input_path}")

        # read and delete the report file
        scan_result = json.loads(output_path.read_text())
        output_path.unlink()

        exit_code = await package_scanning_integration.report_results_async(
            twistcli_scan_result=scan_result,
            bc_platform_integration=bc_platform_integration,
            bc_api_key=bc_api_key,
            file_path=input_path,
        )

        return exit_code

    async def run(
        self,
        bc_platform_integration: BcPlatformIntegration,
        address: str,
        bc_api_key: str,
        input_output_paths: "Iterable[Tuple[Path, Path]]",
    ) -> List[int]:
        args = [
            (
                f"./{TWISTCLI_FILE_NAME} coderepo scan --address {address} --token {bc_api_key} --output-file \"{output_path.absolute()}\" \"{input_path.absolute()}\"",
                bc_platform_integration,
                bc_api_key,
                input_path,
                output_path,
            )
            for input_path, output_path in input_output_paths
        ]
        async with Pool() as pool:
            exit_codes = await pool.starmap(self.run_scan, args)

        return exit_codes

    def scan(self, code_repo_path: Path) -> None:
        try:
            # backwards compatibility, should be removed in a later stage
            code_repo_path = code_repo_path if isinstance(code_repo_path, Path) else Path(code_repo_path)

            self.setup_scan()

            if code_repo_path.is_file():
                input_output_paths = [(code_repo_path, code_repo_path.parent / f"{code_repo_path.stem}_result.json")]
            else:
                logging.info(f"Searching for package files in {code_repo_path}")

                input_output_paths = [
                    (file_path, file_path.parent / f"{file_path.stem}_result.json")
                    for file_path in code_repo_path.glob("**/*")
                    if file_path.name in SUPPORTED_PACKAGE_FILES
                ]

                logging.info(f"Found {len(input_output_paths)} package files")

            # can not be pickled
            bc_integration.s3_client = None
            bc_integration.http = None

            exit_codes = asyncio.run(
                self.run(
                    bc_platform_integration=bc_integration,
                    address=package_scanning_integration.get_proxy_address(),
                    bc_api_key=package_scanning_integration.get_bc_api_key(),
                    input_output_paths=input_output_paths,
                )
            )

            if any(code != 0 for code in exit_codes):
                sys.exit(1)

            logging.info('Package file scanning results reported to the platform')
            self.cleanup_scan()
        except Exception as e:
            logging.error(f"Failed to scan package files\n{e}")
            raise e
