import enum
import json
from datetime import datetime, timezone
from typing import List
from urllib.parse import quote

import attr
import html2text
import maya
from cvss import CVSS3
from django.utils.translation import gettext_lazy as _

from ..constants import PLATFORM_ICONS

ATTACHMENT_IMAGE_FORMATS = ["png", "jpg", "jpeg", "bmp", "svg", "gif"]


class PlatformEnum(enum.Enum):
    ANDROID = "Android"
    IOS = "iOS"
    WINDOWS = "Windows"


class PlatformIconEnum(enum.Enum):
    ANDROID = PLATFORM_ICONS["ANDROID"]
    IOS = PLATFORM_ICONS["IOS"]
    WINDOWS = PLATFORM_ICONS["WINDOWS"]


class RiskColorEnum(enum.Enum):
    CRITICAL = "#EF4836"
    HIGH = "#FF8C00"
    MEDIUM = "#F5D76E"
    LOW = "#2CC2F8"
    PASSED = "#80C081"
    UNTESTED = "#6B6B6B"


class RiskValueEnum(enum.Enum):
    CRITICAL = 4
    HIGH = 3
    MEDIUM = 2
    LOW = 1
    PASSED = 0
    UNTESTED = -1


class RiskLabelEnum(enum.Enum):
    CRITICAL = "Critical"
    HIGH = "High"
    MEDIUM = "Medium"
    LOW = "Low"
    PASSED = "Passed"
    UNTESTED = "Untested"


class AnalysisTypeEnum(enum.Enum):
    STATIC = "Static"
    DYNAMIC = "Dynamic"
    MANUAL = "Manual"
    API = "API"


class AttackVectorEnum(enum.Enum):
    N = "NETWORK"
    A = "ADJACENT_NETWORK"
    L = "LOCAL"
    P = "PHYSICAL"


class AttackComplexityEnum(enum.Enum):
    H = "HIGH"
    L = "LOW"


class PrivilegesRequiredEnum(enum.Enum):
    H = "HIGH"
    L = "LOW"
    N = "NONE"


class UserInteractionEnum(enum.Enum):
    N = "NONE"
    R = "REQUIRED"


class ScopeEnum(enum.Enum):
    U = "UNCHANGED"
    C = "CHANGED"


class ConfidentialityImpactEnum(enum.Enum):
    H = "HIGH"
    L = "LOW"
    N = "NONE"


class IntegrityImpactEnum(enum.Enum):
    H = "HIGH"
    L = "LOW"
    N = "NONE"


class AvailabilityImpactEnum(enum.Enum):
    H = "HIGH"
    L = "LOW"
    N = "NONE"


@attr.s
class Platform:
    ANDROID = PlatformEnum.ANDROID.value
    IOS = PlatformEnum.IOS.value
    WINDOWS = PlatformEnum.WINDOWS.value
    ICON_ANDROID = PlatformIconEnum.ANDROID.value
    ICON_IOS = PlatformIconEnum.IOS.value
    ICON_WINDOWS = PlatformIconEnum.WINDOWS.value
    name = attr.ib(type=str)
    icon = attr.ib(type=str)


@attr.s
class Application:
    name = attr.ib(type=str)
    icon = attr.ib(type=str)
    version = attr.ib(type=str)
    version_code = attr.ib(type=int)
    sha1 = attr.ib(type=str)
    md5 = attr.ib(type=str)


@attr.s
class Company:
    name = attr.ib(type=str, default="")
    logo = attr.ib(type=str, default="")
    hide = attr.ib(type=bool, default=False)


@attr.s
class Reference:
    id = attr.ib(type=int)
    name = attr.ib(type=str)
    url = attr.ib(type=str)


@attr.s
class CustomMetaData:
    key = attr.ib(type=str)
    val = attr.ib(type=str)


@attr.s
class Content:
    html = attr.ib(type=str)
    text = attr.ib(type=str, default="")
    markdown = attr.ib(type=str, default="")

    def html_to_text(self):
        return html2text.html2text(self.html)[:-2]


@attr.s
class Attachment:
    id = attr.ib(type=str)
    name = attr.ib(type=str)
    extension = attr.ib(type=str)
    url = attr.ib(type=str)


@attr.s
class CVSSv3:
    version = attr.ib(type=str)
    vector_string = attr.ib(type=str)
    base_score = attr.ib(type=str)
    base_severity = attr.ib(type=str)
    attack_vector = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in AttackVectorEnum]),
        default=AttackVectorEnum.P.value,
    )
    attack_complexity = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in AttackComplexityEnum]),
        default=AttackComplexityEnum.H.value,
    )
    privileges_required = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in PrivilegesRequiredEnum]),
        default=PrivilegesRequiredEnum.H.value,
    )
    user_interaction = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in UserInteractionEnum]),
        default=UserInteractionEnum.R.value,
    )
    scope = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in ScopeEnum]),
        default=ScopeEnum.U.value,
    )
    confidentiality_impact = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in ConfidentialityImpactEnum]),
        default=ConfidentialityImpactEnum.N.value,
    )
    integrity_impact = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in IntegrityImpactEnum]),
        default=IntegrityImpactEnum.N.value,
    )
    availability_impact = attr.ib(
        type=str,
        validator=attr.validators.in_([e.value for e in AvailabilityImpactEnum]),
        default=AvailabilityImpactEnum.N.value,
    )

    @classmethod
    def parse_vector(cls, vector_string: str) -> "CVSSv3":
        if not vector_string:
            return None
        cvssv3 = CVSS3(vector_string)
        (base_severity, temporal_severity, environmental_severity) = cvssv3.severities()
        return CVSSv3(
            version="3",
            vector_string=vector_string,
            base_score=str(cvssv3.base_score),
            base_severity=base_severity,
            attack_vector=(AttackVectorEnum[cvssv3.metrics.get("AV")].value),
            attack_complexity=(AttackComplexityEnum[cvssv3.metrics.get("AC")].value),
            privileges_required=(
                PrivilegesRequiredEnum[cvssv3.metrics.get("PR")].value
            ),
            user_interaction=(UserInteractionEnum[cvssv3.metrics.get("UI")].value),
            scope=(ScopeEnum[cvssv3.metrics.get("S")].value),
            confidentiality_impact=(
                ConfidentialityImpactEnum[cvssv3.metrics.get("C")].value
            ),
            integrity_impact=(IntegrityImpactEnum[cvssv3.metrics.get("I")].value),
            availability_impact=(AvailabilityImpactEnum[cvssv3.metrics.get("A")].value),
        )


@attr.s
class Risk:
    value = attr.ib(type=int, default=RiskValueEnum.UNTESTED.value)
    value_label = attr.ib(type=str, default=RiskLabelEnum.UNTESTED.value)
    value_color = attr.ib(type=str, default=RiskColorEnum.UNTESTED.value)
    computed_value = attr.ib(type=int, default=RiskValueEnum.UNTESTED.value)
    computed_value_label = attr.ib(type=str, default=RiskLabelEnum.UNTESTED.value)
    computed_value_color = attr.ib(type=str, default=RiskColorEnum.UNTESTED.value)
    is_overridden = attr.ib(type=bool, default=False)
    override_comment = attr.ib(type=str, default="")
    overridden_by = attr.ib(type=str, default="")
    overridden_date = attr.ib(type=str, default="")
    overridden_criteria = attr.ib(type=str, default="")
    overridden_criteria_value = attr.ib(type=str, default="")


@attr.s
class OWASP:
    code = attr.ib(type=str)
    year = attr.ib(type=int)
    title = attr.ib(type=str)


@attr.s
class CWE:
    code = attr.ib(type=str)
    url = attr.ib(type=str)


@attr.s
class ASVS:
    code = attr.ib(type=str)
    title = attr.ib(type=str)


@attr.s
class MSTG:
    code = attr.ib(type=str)
    title = attr.ib(type=str)


@attr.s
class MASVS:
    code = attr.ib(type=str)
    title = attr.ib(type=str)


@attr.s
class OWASPAPI2023:
    code = attr.ib(type=str)
    year = attr.ib(type=int)
    title = attr.ib(type=str)


@attr.s
class OWASPMOBILE2024:
    code = attr.ib(type=str)
    year = attr.ib(type=int)
    title = attr.ib(type=str)


@attr.s
class PCIDSS:
    code = attr.ib(type=str)
    title = attr.ib(type=str)
    description = attr.ib(type=str)


@attr.s
class PCIDSS4:
    code = attr.ib(type=str)
    title = attr.ib(type=str)
    description = attr.ib(type=str)


@attr.s
class HIPAAStandard:
    title = attr.ib(type=str)
    description = attr.ib(type=str)
    specifications = attr.ib(type=str)


@attr.s
class HIPAA:
    code = attr.ib(type=str)
    safeguard = attr.ib(type=str)
    title = attr.ib(type=str)
    standards = attr.ib(factory=list, type=List[HIPAAStandard])

    @classmethod
    def create_standard(
        cls, title: str, description: str, specifications: str
    ) -> HIPAAStandard:
        return HIPAAStandard(
            title=title, description=description, specifications=specifications
        )

    @classmethod
    def from_json(cls, data: dict):
        return cls(
            code=data.get("code"),
            safeguard=data.get("safeguard"),
            title=data.get("title"),
            standards=[
                cls.create_standard(**standard)
                for standard in data.get("standards", [])
            ],
        )

    def add_standard(self, standard: HIPAAStandard) -> List[HIPAAStandard]:
        self.standards.append(standard)
        return self.standards


@attr.s
class GDPR:
    code = attr.ib(type=str)
    title = attr.ib(type=str)


@attr.s
class NISTSP80053:
    code = attr.ib(type=str)
    title = attr.ib(type=str)


@attr.s
class NISTSP800171:
    code = attr.ib(type=str)
    title = attr.ib(type=str)


@attr.s
class SAMA:
    code = attr.ib(type=str)
    title = attr.ib(type=str)
    description = attr.ib(type=str)


@attr.s
class Regulatory:
    owasp = attr.ib(factory=list, type=List[dict])
    cwe = attr.ib(factory=list, type=List[dict])
    asvs = attr.ib(factory=list, type=List[dict])
    mstg = attr.ib(factory=list, type=List[dict])
    masvs = attr.ib(factory=list, type=List[dict])
    owaspapi2023 = attr.ib(factory=list, type=List[dict])
    owaspmobile2024 = attr.ib(factory=list, type=List[dict])
    pcidss = attr.ib(factory=list, type=List[dict])
    pcidss4 = attr.ib(factory=list, type=List[dict])
    hipaa = attr.ib(factory=list, type=List[dict])
    gdpr = attr.ib(factory=list, type=List[dict])
    nistsp80053 = attr.ib(factory=list, type=List[dict])
    nistsp800171 = attr.ib(factory=list, type=List[dict])
    sama = attr.ib(factory=list, type=List[dict])

    @classmethod
    def from_json(cls, data):
        return cls(
            owasp=[OWASP(**owasp) for owasp in data.get("owasp", [])],
            cwe=[CWE(**cwe) for cwe in data.get("cwe", [])],
            asvs=[ASVS(**asvs) for asvs in data.get("asvs", [])],
            mstg=[MSTG(**mstg) for mstg in data.get("mstg", [])],
            masvs=[MASVS(**masvs) for masvs in data.get("masvs", [])],
            owaspapi2023=[
                OWASPAPI2023(**owaspapi2023)
                for owaspapi2023 in data.get("owaspapi2023", [])
            ],
            owaspmobile2024=[
                OWASPMOBILE2024(**owaspmobile2024)
                for owaspmobile2024 in data.get("owaspmobile2024", [])
            ],
            pcidss=[PCIDSS(**pcidss) for pcidss in data.get("pcidss", [])],
            pcidss4=[PCIDSS4(**pcidss4) for pcidss4 in data.get("pcidss4", [])],
            hipaa=[HIPAA.from_json(hipaa) for hipaa in data.get("hipaa", [])],
            gdpr=[GDPR(**gdpr) for gdpr in data.get("gdpr", [])],
            nistsp80053=[
                NISTSP80053(**nistsp80053)
                for nistsp80053 in data.get("nistsp80053", [])
            ],
            nistsp800171=[
                NISTSP800171(**nistsp800171)
                for nistsp800171 in data.get("nistsp800171", [])
            ],
            sama=[SAMA(**sama) for sama in data.get("sama", [])],
        )

    @classmethod
    def create_owasp(cls, code: str, year: int, title: str) -> OWASP:
        return OWASP(code=code, year=year, title=title)

    @classmethod
    def create_cwe(cls, code: str, url: str) -> CWE:
        return CWE(code=code, url=url)

    @classmethod
    def create_asvs(cls, code: str, title: str) -> ASVS:
        return ASVS(code=code, title=title)

    @classmethod
    def create_mstg(cls, code: str, title: str) -> MSTG:
        return MSTG(code=code, title=title)

    @classmethod
    def create_masvs(cls, code: str, title: str) -> "MASVS":
        return MASVS(code=code, title=title)

    @classmethod
    def create_owaspapi2023(cls, code: str, year: int, title: str) -> "OWASPAPI2023":
        return OWASPAPI2023(code=code, year=year, title=title)

    @classmethod
    def create_owaspmobile2024(
        cls, code: str, year: int, title: str
    ) -> "OWASPMOBILE2024":
        return OWASPMOBILE2024(code=code, year=year, title=title)

    @classmethod
    def create_pcidss(cls, code: str, title: str, description: str) -> PCIDSS:
        return PCIDSS(code=code, title=title, description=description)

    @classmethod
    def create_pcidss4(cls, code: str, title: str, description: str) -> PCIDSS4:
        return PCIDSS4(code=code, title=title, description=description)

    @classmethod
    def create_hipaa(
        cls,
        code: str,
        safeguard: str,
        title: str,
        standards: List[HIPAAStandard] = None,
    ) -> HIPAA:
        if standards is None:
            standards = []
        return HIPAA(
            code=code,
            safeguard=safeguard,
            title=title,
            standards=standards,
        )

    @classmethod
    def create_gdpr(cls, code: str, title: str) -> GDPR:
        return GDPR(code=code, title=title)

    @classmethod
    def create_nistsp80053(cls, code: str, title: str) -> NISTSP80053:
        return NISTSP80053(code=code, title=title)

    @classmethod
    def create_nistsp800171(cls, code: str, title: str) -> NISTSP800171:
        return NISTSP800171(code=code, title=title)

    @classmethod
    def create_sama(cls, code: str, title: str, description: str) -> SAMA:
        return SAMA(code=code, title=title, description=description)

    def add_owasp(self, owasp: OWASP) -> List[OWASP]:
        self.owasp.append(owasp)
        return self.owasp

    def add_cwe(self, cwe: CWE) -> List[MSTG]:
        self.cwe.append(cwe)
        return self.cwe

    def add_asvs(self, asvs: ASVS) -> List[ASVS]:
        self.asvs.append(asvs)
        return self.asvs

    def add_mstg(self, mstg: MSTG) -> List[MSTG]:
        self.mstg.append(mstg)
        return self.mstg

    def add_masvs(self, masvs: MASVS) -> "List[MASVS]":
        self.masvs.append(masvs)
        return self.masvs

    def add_owaspapi2023(self, owaspapi2023: OWASPAPI2023) -> "List[OWASPAPI2023]":
        self.owaspapi2023.append(owaspapi2023)
        return self.owaspapi2023

    def add_owaspmobile2024(
        self, owaspmobile2024: OWASPMOBILE2024
    ) -> "List[OWASPMOBILE2024]":
        self.owaspmobile2024.append(owaspmobile2024)
        return self.owaspmobile2024

    def add_pcidss(self, pcidss: PCIDSS) -> List[PCIDSS]:
        self.pcidss.append(pcidss)
        return self.pcidss

    def add_pcidss4(self, pcidss4: PCIDSS4) -> List[PCIDSS4]:
        self.pcidss4.append(pcidss4)
        return self.pcidss4

    def add_hipaa(self, hipaa: HIPAA) -> List[HIPAA]:
        self.hipaa.append(hipaa)
        return self.hipaa

    def add_gdpr(self, gdpr: GDPR) -> List[GDPR]:
        self.gdpr.append(gdpr)
        return self.gdpr

    def add_nistsp80053(self, nistsp80053: NISTSP80053) -> List[NISTSP80053]:
        self.nistsp80053.append(nistsp80053)
        return self.nistsp80053

    def add_nistsp800171(self, nistsp800171: NISTSP800171) -> List[NISTSP800171]:
        self.nistsp800171.append(nistsp800171)
        return self.nistsp800171

    def add_sama(self, sama: SAMA) -> List[SAMA]:
        self.sama.append(sama)
        return self.sama


@attr.s
class Tag:
    val = attr.ib(type=str)


@attr.s
class Finding:
    title = attr.ib(type=Content)
    description = attr.ib(type=Content)


@attr.s
class Analysis:
    id = attr.ib(type=int)
    title = attr.ib(type=Content)
    desc = attr.ib(type=Content)
    intro = attr.ib(type=Content)
    business_implication = attr.ib(type=Content)
    correct_implementation = attr.ib(type=Content)
    incorrect_implementation = attr.ib(type=Content)
    question = attr.ib(type=Content)
    success_message = attr.ib(type=Content)
    vulnerability_references = attr.ib(type=Content)
    risk = attr.ib(type=Risk, default=Risk())
    cvss_v3 = attr.ib(type=CVSSv3, default=None)
    regulatory = attr.ib(
        type=Regulatory,
        default=Regulatory(
            owasp=[],
            pcidss=[],
            pcidss4=[],
            hipaa=[],
            asvs=[],
            cwe=[],
            gdpr=[],
            mstg=[],
            masvs=[],
            owaspapi2023=[],
            owaspmobile2024=[],
            nistsp80053=[],
            nistsp800171=[],
            sama=[],
        ),
    )
    findings = attr.ib(factory=list, type=List[Content])
    tags = attr.ib(factory=list, type=List[Tag])
    attachments = attr.ib(factory=list, type=List[Attachment])

    @classmethod
    def from_json(cls, data):
        cvssv3 = data.get("cvss_v3")
        return cls(
            id=data.get("id"),
            title=Content(**data.get("title")),
            desc=Content(**data.get("desc")),
            intro=Content(**data.get("intro")),
            business_implication=Content(**data.get("business_implication")),
            correct_implementation=Content(**data.get("correct_implementation")),
            incorrect_implementation=Content(**data.get("incorrect_implementation")),
            question=Content(**data.get("question")),
            success_message=Content(**data.get("success_message")),
            vulnerability_references=Content(**data.get("vulnerability_references")),
            risk=Risk(**data.get("risk", {})),
            cvss_v3=CVSSv3(**cvssv3) if cvssv3 else None,
            regulatory=Regulatory.from_json(data.get("regulatory")),
            findings=[
                cls.create_finding(
                    title=Content(**finding.get("title")),
                    description=Content(**finding.get("description")),
                )
                for finding in data.get("findings", [])
            ],
            tags=[Tag(**tag) for tag in data.get("tags", [])],
            attachments=[
                Attachment(**attachment) for attachment in data.get("attachments", [])
            ],
        )

    @classmethod
    def create_regulatory(
        cls,
        owasp: List[dict] = None,
        cwe: List[dict] = None,
        asvs: List[dict] = None,
        mstg: List[dict] = None,
        masvs: List[dict] = None,
        pcidss: List[dict] = None,
        pcidss4: List[dict] = None,
        hipaa: List[dict] = None,
        gdpr: List[dict] = None,
        owaspapi2023: List[dict] = None,
        owaspmobile2024: List[dict] = None,
        nistsp80053: List[dict] = None,
        nistsp800171: List[dict] = None,
        sama: List[dict] = None,
    ) -> Regulatory:
        if sama is None:
            sama = []
        if nistsp80053 is None:
            nistsp80053 = []
        if nistsp800171 is None:
            nistsp800171 = []
        if owaspapi2023 is None:
            owaspapi2023 = []
        if owaspmobile2024 is None:
            owaspmobile2024 = []
        if gdpr is None:
            gdpr = []
        if hipaa is None:
            hipaa = []
        if pcidss is None:
            pcidss = []
        if pcidss4 is None:
            pcidss4 = []
        if masvs is None:
            masvs = []
        if mstg is None:
            mstg = []
        if asvs is None:
            asvs = []
        if cwe is None:
            cwe = []
        if owasp is None:
            owasp = []
        return Regulatory(
            owasp=owasp,
            cwe=cwe,
            asvs=asvs,
            mstg=mstg,
            masvs=masvs,
            owaspapi2023=owaspapi2023,
            owaspmobile2024=owaspmobile2024,
            pcidss=pcidss,
            pcidss4=pcidss4,
            hipaa=hipaa,
            gdpr=gdpr,
            nistsp80053=nistsp80053,
            nistsp800171=nistsp800171,
            sama=sama,
        )

    @classmethod
    def create_attachment(
        cls, id: str, name: str, extension: str, url: str
    ) -> Attachment:
        return Attachment(id=id, name=name, extension=extension, url=url)

    @classmethod
    def create_finding(cls, title: Content, description: Content) -> Finding:
        if title.html:
            title.html = cls.trim_unsupported_chars(title.html)
        if description.html:
            description.html = cls.trim_unsupported_chars(description.html)
        return Finding(title=title, description=description)

    @classmethod
    def create_tag(cls, val: str) -> Tag:
        return Tag(val=val)

    @classmethod
    def create_risk(
        cls,
        value: int,
        value_label: str,
        value_color: str,
        computed_value: int,
        computed_value_label: str,
        computed_value_color: str,
        is_overridden: bool,
        override_comment: str,
        overridden_by: str,
        overridden_date: str,
        overridden_criteria: str,
        overridden_criteria_value: str,
    ) -> Risk:
        return Risk(
            value=value,
            value_label=value_label,
            value_color=value_color,
            computed_value=computed_value,
            computed_value_label=computed_value_label,
            computed_value_color=computed_value_color,
            is_overridden=is_overridden,
            override_comment=override_comment,
            overridden_by=overridden_by,
            overridden_date=overridden_date,
            overridden_criteria=overridden_criteria,
            overridden_criteria_value=overridden_criteria_value,
        )

    def add_finding(self, finding: Finding) -> Finding:
        self.findings.append(finding)
        return self.findings

    def add_tag(self, tag: Tag) -> Tag:
        self.tags.append(tag)
        return self.tags

    def remove_tag(self, tag_val: str) -> Tag:
        for t in self.tags:
            if tag_val == t.val:
                self.tags.remove(t)
        return self.tags

    def add_attachment(self, attachment: Attachment) -> Attachment:
        if attachment.extension.lower() not in ATTACHMENT_IMAGE_FORMATS:
            return False
        self.attachments.append(attachment)
        return self.attachments

    @classmethod
    def trim_unsupported_chars(cls, string: str) -> str:
        """trim non-font characters"""
        s = string
        for char in ["\u001b", "\\u001b", "\x1b"]:  # ESCAPE char
            s = s.replace(char, "")
        return s


@attr.s
class AssessmentType:
    TYPE_STATIC = 1
    TYPE_DYNAMIC = 2
    TYPE_API = 3
    TYPE_MANUAL = 4

    TYPE_DISPLAY_MAP = {
        TYPE_STATIC: _("Static"),
        TYPE_DYNAMIC: _("Dynamic"),
        TYPE_API: _("API"),
        TYPE_MANUAL: _("Manual"),
    }

    assessment_type = attr.ib(type=int)
    is_included = attr.ib(type=bool, default=False)
    is_done = attr.ib(type=bool, default=False)
    can_include = attr.ib(type=bool, default=False)

    @property
    def display(self) -> str:
        return self.TYPE_DISPLAY_MAP.get(self.assessment_type, "")


@attr.s
class Report:
    COLOR_CRITICAL = RiskColorEnum.CRITICAL.value
    COLOR_HIGH = RiskColorEnum.HIGH.value
    COLOR_MEDIUM = RiskColorEnum.MEDIUM.value
    COLOR_LOW = RiskColorEnum.LOW.value
    COLOR_PASSED = RiskColorEnum.PASSED.value
    COLOR_UNTESTED = RiskColorEnum.UNTESTED.value
    RISK_COLOR = {
        "critical": COLOR_CRITICAL,
        "high": COLOR_HIGH,
        "medium": COLOR_MEDIUM,
        "low": COLOR_LOW,
        "passed": COLOR_PASSED,
        "untested": COLOR_UNTESTED,
    }
    organization_id = attr.ib(type=int)
    project_id = attr.ib(type=int)
    package_name = attr.ib(type=str)
    platform = attr.ib(type=Platform)
    application = attr.ib(type=Application)
    appknox_file_id = attr.ib(type=int, default=None)
    prepared_for = attr.ib(type=Company, default=Company(name=""))
    prepared_by = attr.ib(
        type=Company,
        default=Company(name="Appknox", logo="https://appknox.com/in/img/logo.png"),
    )
    powered_by = attr.ib(type=Company, default=Company(hide=True))
    created_on = attr.ib(type=datetime, default=maya.now().iso8601())
    show_copyright = attr.ib(type=bool, default=True)
    is_partnered = attr.ib(type=bool, default=False)
    rating = attr.ib(type=int, default=0)
    assessment_types = attr.ib(factory=list, type=List[AssessmentType])
    hide_untested_analyses = attr.ib(type=bool, default=False)
    references = attr.ib(factory=list, type=List[Reference])
    custom_meta_data = attr.ib(factory=list, type=List[CustomMetaData])
    analyses = attr.ib(factory=list, type=List[Analysis])

    def to_dict(self) -> dict:
        return attr.asdict(self)

    def to_json(self) -> dict:
        return json.loads(
            json.dumps(self.to_dict(), indent=4, sort_keys=True, default=str)
        )

    @classmethod
    def from_json(cls, data):
        return cls(
            organization_id=data.get("organization_id"),
            project_id=data.get("project_id"),
            package_name=data.get("package_name"),
            platform=Platform(**data.get("platform")),
            application=Application(**data.get("application")),
            appknox_file_id=data.get("appknox_file_id"),
            prepared_for=Company(**data.get("prepared_for")),
            prepared_by=Company(**data.get("prepared_by")),
            powered_by=Company(**data.get("powered_by")),
            created_on=cls.create_created_on(data.get("created_on")),
            show_copyright=data.get("show_copyright"),
            is_partnered=data.get("is_partnered"),
            rating=data.get("rating"),
            assessment_types=[
                AssessmentType(**assess_type)
                for assess_type in data.get("assessment_types", [])
            ],
            hide_untested_analyses=data.get(
                "hide_untested_analyses",
                cls.__attrs_attrs__.hide_untested_analyses.default,
            ),
            references=[
                Reference(**reference) for reference in data.get("references", [])
            ],
            custom_meta_data=[
                CustomMetaData(**cmd) for cmd in data.get("custom_meta_data", [])
            ],
            analyses=[
                Analysis.from_json(analysis) for analysis in data.get("analyses", [])
            ],
        )

    def add_analysis(self, analysis: Analysis) -> Analysis:
        self.analyses.append(analysis)

    def add_reference(self, reference: Reference) -> Reference:
        self.references.append(reference)

    def add_custom_metadata(self, customdata: CustomMetaData) -> CustomMetaData:
        self.custom_meta_data.append(customdata)

    @classmethod
    def create_platform(cls, name: str, icon: str) -> Platform:
        return Platform(name=name, icon=icon)

    @classmethod
    def create_application(
        cls, name: str, icon: str, version: str, version_code: str, sha1: str, md5: str
    ) -> Application:
        return Application(
            name=name,
            icon=icon,
            version=version,
            version_code=version_code,
            sha1=sha1,
            md5=md5,
        )

    @classmethod
    def create_company(cls, name, logo, hide) -> Company:
        return Company(name=name, logo=logo, hide=hide)

    @classmethod
    def create_created_on(cls, isodate: str = None) -> datetime:
        if isodate is None:
            return timezone.now()
        return datetime.fromisoformat(isodate).replace(tzinfo=timezone.utc)

    @classmethod
    def create_reference(cls, id: int, name: str, url: str) -> Reference:
        return Reference(id=id, name=name, url=url)

    @classmethod
    def create_custommetadata(cls, key: str, val: str) -> CustomMetaData:
        return CustomMetaData(key=key, val=val)

    @classmethod
    def create_analysis(
        cls,
        id,
        title: Content,
        intro: Content,
        desc: Content,
        risk: Risk,
        cvss_v3: CVSSv3,
        business_implication: Content,
        correct_implementation: Content,
        incorrect_implementation: Content,
        question: Content,
        success_message: Content,
        regulatory: Regulatory,
        vulnerability_references: Content,
        findings: List[Content] = None,
        tags: List[Tag] = None,
        attachments: List[Attachment] = None,
    ) -> Analysis:
        if attachments is None:
            attachments = []
        if tags is None:
            tags = []
        if findings is None:
            findings = []
        return Analysis(
            id=id,
            title=title,
            intro=intro,
            desc=desc,
            risk=risk,
            cvss_v3=cvss_v3,
            business_implication=business_implication,
            correct_implementation=correct_implementation,
            incorrect_implementation=incorrect_implementation,
            question=question,
            success_message=success_message,
            regulatory=regulatory,
            vulnerability_references=vulnerability_references,
            findings=findings,
            tags=tags,
            attachments=attachments,
        )

    @classmethod
    def create_content(
        cls, text: str = "", html: str = "", markdown: str = ""
    ) -> Content:
        return Content(text=text, html=html, markdown=markdown)

    @classmethod
    def create_risk(cls, **kwargs) -> Risk:
        return Analysis.create_risk(**kwargs)

    @classmethod
    def create_regulatory(cls, **kwargs) -> Regulatory:
        return Analysis.create_regulatory(**kwargs)

    @classmethod
    def parse_cvssv3_vector(cls, **kwargs) -> CVSSv3:
        return CVSSv3.parse_vector(**kwargs)

    @classmethod
    def get_risk_color(cls, risk: str) -> str:
        return cls.RISK_COLOR.get(risk.lower(), "")

    def _count_severity(self, label: str) -> int:
        count = 0
        for a in self.analyses:
            if a.risk and (a.risk.computed_value_label.lower() == label):
                count += 1
        return count

    def _count_severity_overridden(self, risk_value_enum: RiskValueEnum) -> str:
        """
        Count the number of analyses with an overridden risk matching the given value.

        This function iterates over the list of analyses and increments a counter for
        each analysis that meets the following criteria:

        1. The analysis has a risk that is not passed by system (risk.value >0) .
        2. The risk level for the analysis has been overridden (a.risk.is_overridden).
        3. The computed value of the overridden risk level matches the provided
            value ( a.risk.computed_value == value)

        Args:
            value (RiskValueEnum):The value to match against the computed value value of
            the overridden risk level. This is typically a int representation of the
                risk severity level (e.g., RiskValueEnum.CRITICAL,RiskValueEnum.LOW
                ,RiskValueEnum.MEDIUM ,RiskValueEnum.HIGH, etc ).

        Returns:
            int: The count of analyses with an overridden risk matching the given value.
        """
        count = 0
        for a in self.analyses:
            if (
                a.risk
                and a.risk.value > 0
                and a.risk.is_overridden
                and (a.risk.computed_value == risk_value_enum.value)
            ):
                count += 1
        return count

    def _percent_severity(self, count: int) -> float:
        if not len(self.analyses):
            return 0.0
        return round((count / len(self.analyses)) * 100, 2)

    @property
    def critical_count(self) -> int:
        return self._count_severity("critical")

    @property
    def high_count(self) -> int:
        return self._count_severity("high")

    @property
    def medium_count(self) -> int:
        return self._count_severity("medium")

    @property
    def low_count(self) -> int:
        return self._count_severity("low")

    @property
    def passed_count(self) -> int:
        return self._count_severity("passed")

    @property
    def passed_overridden_count(self) -> int:
        return self._count_severity_overridden(RiskValueEnum.PASSED)

    @property
    def untested_count(self) -> int:
        return self._count_severity("untested")

    @property
    def critical_percent(self) -> float:
        return self._percent_severity(self.critical_count)

    @property
    def high_percent(self) -> float:
        return self._percent_severity(self.high_count)

    @property
    def medium_percent(self) -> float:
        return self._percent_severity(self.medium_count)

    @property
    def low_percent(self) -> float:
        return self._percent_severity(self.low_count)

    @property
    def passed_percent(self) -> float:
        return self._percent_severity(self.passed_count)

    @property
    def untested_percent(self) -> float:
        return self._percent_severity(self.untested_count)

    @property
    def svg_chart(self) -> str:
        tpl = (
            '<circle cx="21" cy="21" r="15.91549430918954" stroke-width="8" '
            'stroke="{}" stroke-dasharray="{} {}" stroke-dashoffset="-{}" '
            'fill="transparent"></circle>'
        )
        sector = ""
        offset = 0
        p = self.critical_percent
        sector += tpl.format(self.COLOR_CRITICAL, p, round(100 - p, 2), offset)
        offset += p
        p = self.high_percent
        sector += tpl.format(self.COLOR_HIGH, p, round(100 - p, 2), offset)
        offset += p
        p = self.medium_percent
        sector += tpl.format(self.COLOR_MEDIUM, p, round(100 - p, 2), offset)
        offset += p
        p = self.low_percent
        sector += tpl.format(self.COLOR_LOW, p, round(100 - p, 2), offset)
        offset += p
        p = self.passed_percent
        sector += tpl.format(self.COLOR_PASSED, p, round(100 - p, 2), offset)
        offset += p
        p = self.untested_percent
        sector += tpl.format(self.COLOR_UNTESTED, p, round(100 - p, 2), offset)
        return (
            "data:image/svg+xml;utf8,"
            '<svg xmlns="http://www.w3.org/2000/svg" '
            'xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" '
            f'width="216px" height="216px" viewBox="0 0 42 42">{quote(sector)}</svg>'
        )

    @property
    def custom_meta_names(self) -> List[str]:
        return [
            cmd.val
            for cmd in self.custom_meta_data
            if cmd.key == "name" and cmd.val != ""
        ]

    @property
    def _scan_types_visible(self):
        allowed_scan_types = [
            assessment_type.assessment_type
            for assessment_type in self.assessment_types
            if assessment_type.is_included
        ]
        scan_types = set()
        if AssessmentType.TYPE_STATIC in allowed_scan_types:
            scan_types.add(AnalysisTypeEnum.STATIC.value.lower())
        if AssessmentType.TYPE_DYNAMIC in allowed_scan_types:
            scan_types.add(AnalysisTypeEnum.DYNAMIC.value.lower())
        if AssessmentType.TYPE_API in allowed_scan_types:
            scan_types.add(AnalysisTypeEnum.API.value.lower())
        if AssessmentType.TYPE_MANUAL in allowed_scan_types:
            scan_types.add(AnalysisTypeEnum.MANUAL.value.lower())
        return scan_types

    def _is_visible_scan_type(self, analysis):
        tags = {t.val.lower() for t in analysis.tags}
        include_tags = tags.intersection(self._scan_types_visible)
        if not include_tags:
            return False
        if include_tags:
            for t in analysis.tags:
                if t.val.lower() not in include_tags:
                    analysis.tags.remove(t)
        return analysis

    @property
    def viewable_analyses(self) -> List[Analysis]:
        return [a for a in self.analyses if self._is_visible_scan_type(a)]

    @classmethod
    def create_assessment_type(
        cls,
        assessment_type: int,
        is_included: bool = False,
        is_done: bool = False,
        can_include: bool = False,
    ) -> "AssessmentType":
        return AssessmentType(
            assessment_type=assessment_type,
            is_included=is_included,
            is_done=is_done,
            can_include=can_include,
        )

    @property
    def is_manual_scan_included(self) -> bool:
        for assessment_type in self.assessment_types:
            if (
                assessment_type.assessment_type == AssessmentType.TYPE_MANUAL
                and assessment_type.is_included
            ):
                return True
        return False
