# pragma: no cover file.

"""
Experimental code to generate SPDX.

This code is excluded from code coverage calculation because the feature is
highly experimental and most of this code will be removed.
"""

import os.path
import re
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, Union

from semantic_version import Version
from spdx_tools.spdx3.model import (
    Element,
    Hash,
    HashAlgorithm,
    ProfileIdentifierType,
    Relationship,
    RelationshipType,
    SpdxDocument,
)
from spdx_tools.spdx3.model.positive_integer_range import PositiveIntegerRange
from spdx_tools.spdx3.model.software import (
    File,
    Package,
    Snippet,
    SoftwarePurpose,
)
from spdx_tools.spdx3.model.spdx_document import CreationInfo
from spdx_tools.spdx3.payload import Payload
from spdx_tools.spdx3.writer.console.relationship_writer import (
    write_relationship,
)
from spdx_tools.spdx3.writer.console.software.file_writer import write_file
from spdx_tools.spdx3.writer.console.software.package_writer import (
    write_package,
)
from spdx_tools.spdx3.writer.console.software.snippet_writer import (
    write_snippet,
)
from spdx_tools.spdx3.writer.console.spdx_document_writer import (
    write_spdx_document,
)
from spdx_tools.spdx3.writer.json_ld.json_ld_writer import write_payload

from strictdoc.backend.sdoc.models.document import SDocDocument
from strictdoc.backend.sdoc.models.node import SDocNode
from strictdoc.backend.sdoc.models.reference import (
    ParentReqReference,
)
from strictdoc.backend.sdoc.writer import SDWriter
from strictdoc.core.document_iterator import DocumentCachingIterator
from strictdoc.core.project_config import ProjectConfig
from strictdoc.core.traceability_index import TraceabilityIndex
from strictdoc.export.spdx.spdx_sdoc_container import SPDXSDocContainer
from strictdoc.export.spdx.spdx_to_sdoc_converter import SPDXToSDocConverter
from strictdoc.helpers.cast import assert_cast
from strictdoc.helpers.sha256 import get_sha256

RELATION_ID_HOW_TO = "SPDXRef-Relationship-How-to-form-ID?"


def create_relationship_summary(
    lhs: Element, rhs: Element, relation: str
) -> str:
    return f"{lhs.summary} --|{relation}|--> {rhs.summary}"


def get_spdx_ref(node: Union[SDocDocument, SDocNode, str]) -> str:
    if isinstance(node, str):
        return re.sub(r"[/\\ ]", "_", node)

    identifier: Optional[str] = None
    if node.mid_permanent:
        identifier = node.reserved_mid
    elif node.reserved_uid is not None:
        identifier = node.reserved_uid
    elif node.reserved_title is not None:
        identifier = re.sub(r"[ #-/\\]", "_", node.reserved_title)
    else:
        raise RuntimeError(
            f"Cannot create unique identifier for this node: {node}"
        )

    if isinstance(node, SDocDocument):
        return "SDocDocument-" + identifier
    if isinstance(node, SDocNode):
        return "SDocRequirement-" + identifier
    raise NotImplementedError


class SDocToSPDXConverter:
    @staticmethod
    def create_document(project_config: ProjectConfig) -> SpdxDocument:
        spec_version = Version("3.0.0")
        creation_info = CreationInfo(
            spec_version=spec_version,
            created=datetime.today(),
            created_by=[],
            profile=[ProfileIdentifierType.SOFTWARE],
            data_license="CC0 1.0",
            created_using=["SPDXRef-StrictDoc"],
            comment=f"SPDX 3.0 SBOM for {project_config.project_title}'s requirements.",
        )
        spdx_document = SpdxDocument(
            spdx_id="SPDXRef-DOCUMENT",
            name=f"Requirements for {project_config.project_title}",
            element=[],
            root_element=[],
            creation_info=creation_info,
            summary=f"SPDX Document for project {project_config.project_title}",
            description=None,
            comment=None,
        )
        return spdx_document

    @staticmethod
    def create_package(project_config: ProjectConfig) -> Package:
        return Package(
            spdx_id="SPDXRef-PACKAGE",
            name="Requirements package",
            summary=f"SPDX Package for project {project_config.project_title}",
            description=None,
            comment=None,
            verified_using=[
                Hash(
                    algorithm=HashAlgorithm.SHA256,
                    hash_value="TBD: What to calculate for a package?",
                )
            ],
            primary_purpose=SoftwarePurpose.BOM,
            homepage=None,
        )

    @staticmethod
    def create_document_to_file(
        document: SDocDocument, document_bytes: bytes
    ) -> File:
        assert document.meta is not None

        return File(
            spdx_id=f"SPDXRef-File-{get_spdx_ref(document)}",
            name=document.meta.document_filename,
            summary=f"SDPX File for document {document.title}",
            description=None,
            comment=None,
            verified_using=[
                Hash(
                    algorithm=HashAlgorithm.SHA256,
                    hash_value=get_sha256(document_bytes),
                )
            ],
            primary_purpose=SoftwarePurpose.DOCUMENTATION,
        )

    @staticmethod
    def convert_file_to_file(node_link_path: str, file_bytes: bytes) -> File:
        return File(
            spdx_id=f"SPDXRef-File-{get_spdx_ref(node_link_path)}",
            name=node_link_path,
            summary=f"SPDX File for source file {node_link_path}",
            description=None,
            comment=None,
            verified_using=[
                Hash(
                    algorithm=HashAlgorithm.SHA256,
                    hash_value=get_sha256(file_bytes),
                )
            ],
            primary_purpose=SoftwarePurpose.SOURCE,
        )

    @staticmethod
    def convert_requirement_to_snippet(
        requirement: SDocNode, document_bytes: bytes, spdx_file: File
    ) -> Snippet:
        snippet_sha256 = get_sha256(
            document_bytes[requirement.ng_byte_start : requirement.ng_byte_end]
        )
        assert requirement.reserved_uid is not None
        assert requirement.reserved_statement is not None

        return Snippet(
            spdx_id=requirement.reserved_uid,
            primary_purpose=SoftwarePurpose.DOCUMENTATION,
            name=f"Requirement '{requirement.reserved_title}'",
            summary=f"SPDX Snippet for requirement {requirement.reserved_uid}",
            description=requirement.reserved_statement.partition("\n")[0],
            comment=(
                "This snippet has been generated from a requirement "
                f"defined in a StrictDoc file: {spdx_file.name}."
            ),
            verified_using=[
                Hash(
                    algorithm=HashAlgorithm.SHA256,
                    hash_value=snippet_sha256,
                )
            ],
            byte_range=PositiveIntegerRange(
                requirement.ng_byte_start, requirement.ng_byte_end
            ),
            line_range=PositiveIntegerRange(
                requirement.ng_line_start, requirement.ng_line_end
            ),
        )


class SPDXGenerator:
    @staticmethod
    def export_tree(
        project_config: ProjectConfig,
        traceability_index: TraceabilityIndex,
        output_spdx_root: str,
    ) -> None:
        Path(output_spdx_root).mkdir(parents=True, exist_ok=True)

        sdoc_spdx_converter: SDocToSPDXConverter = SDocToSPDXConverter()
        spdx_container: SPDXSDocContainer = SPDXSDocContainer()

        #
        # SPDX Document and SPDX Package
        #
        spdx_document = sdoc_spdx_converter.create_document(project_config)
        spdx_container.document = spdx_document

        spdx_package = sdoc_spdx_converter.create_package(project_config)
        spdx_container.package = spdx_package

        spdx_document.root_element = [spdx_package.spdx_id]

        spdx_container.relationships.append(
            Relationship(
                spdx_id=RELATION_ID_HOW_TO,
                from_element=spdx_document.spdx_id,
                relationship_type=RelationshipType.CONTAINS,
                to=[spdx_package.spdx_id],
                name=None,
                summary=create_relationship_summary(
                    spdx_document,
                    spdx_package,
                    "CONTAINS",
                ),
                description=None,
                comment=None,
            )
        )

        lookup_uid_to_requirement_snippet: Dict[str, Snippet] = {}
        lookup_file_name_to_spdx_file: Dict[str, File] = {}

        for document_ in traceability_index.document_tree.document_list:
            assert document_.meta is not None
            with open(document_.meta.input_doc_full_path, "rb") as input_file_:
                document_bytes = input_file_.read()

            #
            # Create SPDX File from SDoc Document.
            #
            spdx_file = sdoc_spdx_converter.create_document_to_file(
                document_, document_bytes
            )
            spdx_container.files.append(spdx_file)
            spdx_container.relationships.append(
                Relationship(
                    spdx_id=RELATION_ID_HOW_TO,
                    from_element=spdx_package.spdx_id,
                    relationship_type=RelationshipType.CONTAINS,
                    to=[spdx_file.spdx_id],
                    summary=create_relationship_summary(
                        spdx_package,
                        spdx_file,
                        "CONTAINS",
                    ),
                )
            )
            spdx_container.map_spdx_ref_to_objects[spdx_file.spdx_id] = (
                spdx_file
            )

            document_iterator: DocumentCachingIterator = (
                traceability_index.get_document_iterator(document_)
            )

            for node, _ in document_iterator.all_content():
                if (
                    isinstance(node, SDocNode)
                    and node.node_type == "REQUIREMENT"
                ):
                    if node.reserved_uid is None:
                        continue

                    assert node.reserved_title is not None, (
                        "The current implementation only supports requirements with a title."
                    )

                    #
                    # Create SPDX Snippet from SDoc Requirement.
                    #
                    spdx_snippet: Snippet = (
                        sdoc_spdx_converter.convert_requirement_to_snippet(
                            node, document_bytes, spdx_file
                        )
                    )
                    lookup_uid_to_requirement_snippet[node.reserved_uid] = (
                        spdx_snippet
                    )
                    spdx_container.snippets.append(spdx_snippet)
                    spdx_container.map_spdx_ref_to_objects[
                        spdx_snippet.spdx_id
                    ] = spdx_snippet
                    spdx_container.map_spdx_snippets_to_files[
                        spdx_snippet.spdx_id
                    ] = spdx_file.spdx_id

                    spdx_container.relationships.append(
                        Relationship(
                            spdx_id=RELATION_ID_HOW_TO,
                            from_element=spdx_file.spdx_id,
                            relationship_type=RelationshipType.CONTAINS,
                            to=[spdx_snippet.spdx_id],
                            name=None,
                            summary=create_relationship_summary(
                                spdx_file,
                                spdx_snippet,
                                "CONTAINS",
                            ),
                            description=None,
                            comment=None,
                        )
                    )

                    node_links = traceability_index.get_requirement_file_links(
                        node
                    )
                    for node_link_path_, _range_markers in node_links:
                        if node_link_path_ in lookup_file_name_to_spdx_file:
                            continue

                        with open(node_link_path_, "rb") as node_link_file_:
                            file_bytes = node_link_file_.read()

                        source_spdx_file = (
                            sdoc_spdx_converter.convert_file_to_file(
                                node_link_path_, file_bytes
                            )
                        )
                        lookup_file_name_to_spdx_file[node_link_path_] = (
                            source_spdx_file
                        )
                        spdx_container.map_spdx_ref_to_objects[
                            source_spdx_file.spdx_id
                        ] = source_spdx_file

                        spdx_container.files.append(source_spdx_file)
                        spdx_container.relationships.append(
                            Relationship(
                                spdx_id=RELATION_ID_HOW_TO,
                                from_element=spdx_snippet.spdx_id,
                                relationship_type=RelationshipType.REQUIREMENT_FOR,
                                to=[source_spdx_file.spdx_id],
                                name=None,
                                summary=create_relationship_summary(
                                    spdx_snippet,
                                    source_spdx_file,
                                    "REQUIREMENT_FOR",
                                ),
                                description=None,
                                comment=None,
                            )
                        )

        #
        # Now iterate over requirement nodes again.
        #
        for document_ in traceability_index.document_tree.document_list:
            document_iterator = traceability_index.get_document_iterator(
                document_
            )
            for node, _ in document_iterator.all_content():
                if not isinstance(node, SDocNode):
                    continue

                if node.is_requirement():
                    if node.reserved_uid is None:
                        continue

                requirement: SDocNode = assert_cast(node, SDocNode)
                assert requirement.reserved_uid is not None
                requirement_snippet: Snippet = (
                    lookup_uid_to_requirement_snippet[requirement.reserved_uid]
                )

                for reference_ in requirement.relations:
                    if isinstance(reference_, ParentReqReference):
                        parent_requirement_snippet: Snippet = (
                            lookup_uid_to_requirement_snippet[
                                reference_.ref_uid
                            ]
                        )

                        spdx_container.relationships.append(
                            Relationship(
                                spdx_id=RELATION_ID_HOW_TO,
                                from_element=requirement_snippet.spdx_id,
                                relationship_type=RelationshipType.REQUIREMENT_FOR,
                                to=[parent_requirement_snippet.spdx_id],
                                name=None,
                                summary=create_relationship_summary(
                                    requirement_snippet,
                                    parent_requirement_snippet,
                                    "REQUIREMENT_FOR",
                                ),
                                description=None,
                                comment=None,
                            )
                        )

        #
        # Output SPDX to a file.
        #

        output_path = os.path.join(output_spdx_root, "output.spdx")
        with open(output_path, "w", encoding="utf8") as file:
            write_spdx_document(spdx_document, text_output=file)
            file.write("\n")

            write_package(spdx_package, text_output=file, heading=True)
            file.write("\n")

            for file_ in spdx_container.files:
                write_file(file_, text_output=file)
                file.write("\n")

            for snippet_ in spdx_container.snippets:
                write_snippet(snippet_, text_output=file)
                file.write("\n")

            for relationship_ in spdx_container.relationships:
                write_relationship(relationship_, text_output=file)
                file.write("\n")

        if True:
            payload: Payload = Payload()

            payload.add_element(spdx_document)
            payload.add_element(spdx_package)
            for spdx_file_ in spdx_container.files:
                payload.add_element(spdx_file_)
            for spdx_snippet_ in spdx_container.snippets:
                payload.add_element(spdx_snippet_)

            path_to_output_spdx_json = os.path.join(
                output_spdx_root, "output.spdx"
            )

            write_payload(payload, path_to_output_spdx_json)

        if True:
            sdoc_document = SPDXToSDocConverter.convert(spdx_container)

            sdoc_output: str = SDWriter(project_config).write(sdoc_document)

            sdoc_output_path = os.path.join(
                output_spdx_root, "output.spdx.sdoc"
            )
            with open(sdoc_output_path, "w", encoding="utf8") as output_file_:
                output_file_.write(sdoc_output)
