import json
import subprocess
import sys
import uuid
from datetime import datetime
from pathlib import Path

from loguru import logger

import surfactant.plugin
from surfactant.sbomtypes import SBOM, Software


def run_cve_bin_tool(input_file_path, shaHash, output_dir):
    cvebin_file_name = f"{shaHash}_{input_file_path.stem}.json"
    output_file_path = output_dir / cvebin_file_name

    cdxvex_file_name = f"{shaHash}_{input_file_path.stem}.cdxvex"
    vex_output_path = output_dir / cdxvex_file_name

    try:
        command = [
            "cve-bin-tool",
            "--offline",
            "--input-file",
            str(input_file_path),
            "--output",
            str(output_file_path),
            "--format",
            "json",
            "--vex",
            str(vex_output_path),
        ]
        result = subprocess.run(command, capture_output=True, text=True, check=False)

        # Check the exit status
        if result.returncode in (0, 1):
            return output_file_path  # Return path to the generated JSON file
    except subprocess.CalledProcessError as e:
        logger.error(
            f"Error running CVE-bin-tool: {e}\nOutput: {e.output}\nError: {e.stderr}",
            file=sys.stderr,
        )
    return None


def convert_cve_to_openvex(json_output_path, shaHash, output_dir):
    openvex_file_name = f"{json_output_path.stem}.vex"
    openvex_output = output_dir / openvex_file_name

    # Open and read the .json file
    try:
        with open(json_output_path, "r") as file:
            cve_data = json.load(file)
    except json.JSONDecodeError as e:
        logger.error(f"Error reading JSON file: {e}")
        return
    except IOError as e:
        logger.error(f"IO error when reading {json_output_path}: {e}")

    openvex_template = {
        "@context": "https://openvex.dev/ns/v0.2.0",
        "@id": f"urn:uuid:{uuid.uuid4()}",
        "author": "Surfactant plugin cvebintool2vex",
        "timestamp": datetime.now().isoformat(),
        "version": 1,
        "tooling": "Surfactant (https://github.com/LLNL/Surfactant)",
        "statements": [],
    }

    for entry in cve_data:
        # Convert CVE data to OpenVEX format
        statement = {
            "vulnerability": {"name": entry["cve_number"]},
            "products": [
                {"@id": f"cpe:2.3:a:{entry['vendor']}:{entry['product']}:{entry['version']}:::::"}
            ],
            "status": "under_investigation",
            "source": entry["source"],
            "cvss_version": entry["cvss_version"],
            "cvss_vector": entry["cvss_vector"],
            "severity": entry["severity"],
        }
        openvex_template["statements"].append(statement)

    # Save the OpenVEX output to a new file
    try:
        with open(openvex_output, "w") as outfile:
            json.dump(openvex_template, outfile, indent=4)
    except IOError as e:
        logger.error(f"IO error when writing {openvex_output}: {e}")


def process_input(input_path, shaHash, output_dir=None):
    input_path = Path(input_path)
    if output_dir is None:
        output_dir = Path.cwd()

    if input_path.is_dir():
        for input_file in input_path.glob("*.*"):
            if input_file.suffix.lower() not in [".bin", ".exe", ".jar"]:
                continue
            process_file(input_file, shaHash, output_dir)
    elif input_path.is_file():
        process_file(input_path, shaHash, output_dir)
    else:
        logger.info(f"Error: {input_path} is neither a file nor a directory.")


def process_file(input_file, shaHash, output_directory):
    try:
        json_output_path = run_cve_bin_tool(input_file, shaHash, output_directory)
        if json_output_path:
            convert_cve_to_openvex(json_output_path, shaHash, output_directory)
    except subprocess.CalledProcessError as e:
        logger.error(f"Error running CVE-bin-tool: {e}")
    except json.JSONDecodeError as e:
        logger.error(f"JSON decoding error in {input_file}: {e}")
    except IOError as e:
        logger.error(f"I/O error processing {input_file}: {e}")

    # Check if the expected JSON file was created and proceed if it exists
    cvebin_file_name = f"{shaHash}_{input_file.stem}.json"
    jsonfile = output_directory / cvebin_file_name
    if not jsonfile.exists():
        logger.warning(f"Expected JSON file does not exist: {jsonfile}")


def delete_extra_files(*file_paths):
    for file_path in file_paths:
        try:
            if file_path.exists():
                file_path.unlink()
        except PermissionError as e:
            logger.error(f"Permission error deleting {file_path}: {e}")

        except OSError as e:
            logger.error(f"OS error deleting {file_path}: {e}")


@surfactant.plugin.hookimpl(specname="extract_file_info")
def cvebintool2vex(sbom: SBOM, software: Software, filename: str, filetype: str):
    """
    :param sbom(SBOM): The SBOM that the software entry/file is being added to. Can be used to add observations or analysis data.
    :param software(Software): The software entry associated with the file to extract information from.
    :param filename (str): The full path to the file to extract information from.
    :param filetype (str): File type information based on magic bytes.
    """
    # Only parsing executable files
    if filetype not in ["ELF", "PE"]:
        pass

    shaHash = str(software.sha256)
    filename = Path(filename)
    output_dir = Path.cwd()

    existing_json_path = output_dir / f"{shaHash}_additional_metadata.json"
    if existing_json_path.exists():
        with open(existing_json_path, "r") as file:
            data = json.load(file)
    else:
        data = {
            "sha256hash": shaHash,
            "filename": [filename.name],
            "openvex": [],
            "cyclonedx-vex": [],
            "cve-bin-tool": [],
        }

    # Assuming JSON, CDXVEX, and VEX files are processed here
    process_input(filename, shaHash, output_dir)
    # and you have the output files: .json, .cdxvex, .vex

    # Integrate .cdxvex and .vex file contents
    cdxvex_file_path = output_dir / f"{shaHash}_{filename.stem}.cdxvex"
    vex_file_path = output_dir / f"{shaHash}_{filename.stem}.vex"
    json_file_path = output_dir / f"{shaHash}_{filename.stem}.json"

    if cdxvex_file_path.exists() and vex_file_path.exists() and json_file_path.exists():
        # For .cdxvex and .vex files, if they contain JSON, parse them as such; otherwise, read as text
        try:
            with open(cdxvex_file_path, "r") as file:
                cdxvex_data = json.load(file)  # Assuming .cdxvex file is in JSON format
            data["cyclonedx-vex"].append(cdxvex_data)
        except json.JSONDecodeError:
            with open(cdxvex_file_path, "r") as file:
                cdxvex_data = file.read()  # Fallback if not JSON
            data["cyclonedx-vex"].append(cdxvex_data)

        try:
            with open(vex_file_path, "r") as file:
                vex_data = json.load(file)  # Assuming .vex file is in JSON format
            data["openvex"].append(vex_data)
        except json.JSONDecodeError:
            with open(vex_file_path, "r") as file:
                vex_data = file.read()  # Fallback if not JSON
            data["openvex"].append(vex_data)

        with open(json_file_path, "r") as file:
            json_data = json.load(file)
        data["cve-bin-tool"].append(json_data)

    # Attempt to save the updated data
    try:
        with open(existing_json_path, "w") as file:
            json.dump(data, file, indent=4)
            logger.info(f"Updated data saved to {existing_json_path}")
    except IOError as e:
        logger.error(f"IO error when writing {existing_json_path}: {e}")

    # Clean up extra files
    delete_extra_files(cdxvex_file_path, vex_file_path, json_file_path)


@surfactant.plugin.hookimpl
def update_db():
    # Example update logic
    try:
        subprocess.check_call(["cve-bin-tool", "--update", "now", "."])
        return "Database updated successfully."
    except subprocess.CalledProcessError as e:
        return f"Failed to update database: {e}"
