from __future__ import annotations

import logging
import subprocess  # nosec
from pathlib import Path
from typing import Union, Dict, Any
import asyncio
from urllib.parse import quote_plus

import docker
import json
import os
import time

from checkov.common.bridgecrew.vulnerability_scanning.integrations.docker_image_scanning import \
    docker_image_scanning_integration
from checkov.common.bridgecrew.platform_integration import bc_integration
from checkov.common.util.file_utils import decompress_file_gzip_base64
from checkov.common.util.http_utils import request_wrapper
from checkov.common.bridgecrew.platform_key import bridgecrew_dir

TWISTCLI_FILE_NAME = 'twistcli'
DOCKER_IMAGE_SCAN_RESULT_FILE_NAME = 'docker-image-scan-results.json'
CHECKOV_SEC_IN_WEEK = 604800


def generate_image_name() -> str:
    return f'repository/image{str(time.time() * 1000)}'


def _get_docker_image_name(docker_image_id: str) -> str:
    try:
        docker_client = docker.from_env()
        image_name: str = docker_client.images.get(docker_image_id).attrs['RepoDigests'][0].split('@')[0]
        return image_name
    except Exception:
        logging.info("Failed to fetch image name.", exc_info=True)
        return generate_image_name()


def _get_dockerfile_content(dockerfile_path: Union[str, "os.PathLike[str]"]) -> str:
    try:
        with open(dockerfile_path) as f:
            return f.read()
    except FileNotFoundError:
        logging.error("Path to Dockerfile is invalid", exc_info=True)
        raise
    except Exception:
        logging.error("Failed to read Dockerfile content", exc_info=True)
        raise


class ImageScanner:
    def __init__(self) -> None:
        self.docker_image_name = ''
        self.dockerfile_content = ''
        self.twistcli_path = Path(bridgecrew_dir) / TWISTCLI_FILE_NAME

    def setup_scan(
            self,
            docker_image_id: str,
            dockerfile_path: Union[str, "os.PathLike[str]"],
            skip_extract_image_name: bool,
    ) -> None:
        if skip_extract_image_name:
            # Provide a default image name in case the image has not been tagged with a name
            self.docker_image_name = generate_image_name()
        else:
            self.docker_image_name = _get_docker_image_name(docker_image_id)
        self.dockerfile_content = _get_dockerfile_content(dockerfile_path)

        self.setup_twistcli()

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

    def run_image_scan(self, docker_image_id: str) -> Dict[str, Any]:
        command = f"./{self.twistcli_path} images scan --address {docker_image_scanning_integration.get_proxy_address()} --token {docker_image_scanning_integration.get_bc_api_key()} --details --output-file \"{DOCKER_IMAGE_SCAN_RESULT_FILE_NAME}\" {docker_image_id}"
        logging.debug(f"TwistCLI: {command}")
        try:
            subprocess.run([command], check=True, shell=True)  # nosec
        except Exception as exc:
            logging.error("Failed to scan image", exc_info=True)
            self.cleanup_scan()
            raise Exception(f"Failed to scan image {docker_image_id}") from exc
        self.cleanup_scan()
        logging.info(f'TwistCLI ran successfully on image {docker_image_id}')

        with open(DOCKER_IMAGE_SCAN_RESULT_FILE_NAME) as docker_image_scan_result_file:
            scan_result: dict[str, Any] = json.load(docker_image_scan_result_file)
        return scan_result

    def scan(self, docker_image_id: str, dockerfile_path: str, skip_extract_image_name: bool = False) -> int:
        try:
            self.setup_scan(docker_image_id, dockerfile_path, skip_extract_image_name)
            scan_result = self.run_image_scan(docker_image_id)
            exit_code = asyncio.run(
                docker_image_scanning_integration.report_results_async(
                    twistcli_scan_result=scan_result,
                    bc_platform_integration=bc_integration,
                    bc_api_key=docker_image_scanning_integration.get_bc_api_key(),
                    file_path=Path(dockerfile_path),
                    file_content=self.dockerfile_content,
                    docker_image_name=self.docker_image_name,
                )
            )

            logging.info('Docker image scanning results reported to the platform')
            return exit_code
        except Exception:
            logging.error("Failed to scan docker image", exc_info=True)
            return 1

    @staticmethod
    def get_scan_results_from_cache(image_id: str) -> Dict[str, Any]:
        image_id_encode = quote_plus(image_id)

        try:
            response = request_wrapper(
                "GET", f"{bc_integration.api_url}/api/v1/vulnerabilities/scan-results/{image_id_encode}",
                headers=bc_integration.get_default_headers("GET"), should_call_raise_for_status=True
            )
            response_json = response.json()
            output_type = response_json["outputType"]

            logging.info(f"{output_type} returned from cache")

            if output_type == "Error":
                logging.error(response_json["outputData"])
                return {}

            elif output_type == "Result":
                result: dict[str, Any] = json.loads(
                    decompress_file_gzip_base64(
                        response_json["outputData"]
                    )
                )
                return result

            logging.info(f"Got an empty result for image={image_id}")
        except Exception:
            logging.info("Failed to retrieve image cache result", exc_info=True)

        return {}

    def should_download(self) -> bool:
        if not self.twistcli_path.exists():
            return True
        last_modification = os.stat(self.twistcli_path)
        file_age = (time.time() - last_modification.st_mtime)
        return file_age >= int(os.getenv("CHECKOV_EXPIRATION_TIME_IN_SEC", CHECKOV_SEC_IN_WEEK))

    def setup_twistcli(self) -> None:
        try:
            if self.should_download():
                if not os.path.exists(bridgecrew_dir):
                    try:
                        os.makedirs(bridgecrew_dir)
                    except FileExistsError:
                        # In multi-processing, this might meet a race condition
                        pass
                self.cleanup_scan()
                docker_image_scanning_integration.download_twistcli(image_scanner.twistcli_path)
        except Exception:
            logging.error("Failed to setup docker image scanning", exc_info=True)
            raise


image_scanner = ImageScanner()
