import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional


@dataclass
class Property:

    name: str
    value: str

    def to_dict(self) -> Dict[str, str]:
        return {"name": self.name, "value": self.value}


def sort_properties_key(prop_dict: Dict[str, str]) -> tuple:
    return (prop_dict["name"], prop_dict["value"])


def sort_component_key(component: "Component") -> tuple:
    return (component.bom_ref, component.name)


def sort_service_key(service: "Service") -> str:
    return service.name


@dataclass
class Component:

    type: str  # application, service, library, data
    name: str
    version: str
    bom_ref: str
    properties: List[Property] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        result = {
            "type": self.type,
            "name": self.name,
            "version": self.version,
            "bom-ref": self.bom_ref,
        }
        if self.properties:
            prop_dicts = []
            for p in self.properties:
                prop_dicts.append(p.to_dict())
            result["properties"] = sorted(prop_dicts, key=sort_properties_key)
        return result

    def get_property(self, name: str) -> Optional[str]:
        for prop in self.properties:
            if prop.name == name:
                return prop.value
        return None

    def get_kind(self) -> Optional[str]:
        return self.get_property("brox.ai.asset.kind")


@dataclass
class Service:
    name: str
    properties: List[Property] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        result = {"name": self.name}
        if self.properties:
            prop_dicts = []
            for p in self.properties:
                prop_dicts.append(p.to_dict())
            result["properties"] = sorted(prop_dicts, key=sort_properties_key)
        return result


@dataclass
class Tool:
    vendor: str
    name: str
    version: str

    def to_dict(self) -> Dict[str, str]:
        return {"vendor": self.vendor, "name": self.name, "version": self.version}


@dataclass
class Metadata:
    timestamp: str
    tools: List[Tool]
    properties: List[Property] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        tool_dicts = []
        for t in self.tools:
            tool_dicts.append(t.to_dict())

        result = {
            "timestamp": self.timestamp,
            "tools": tool_dicts,
        }
        if self.properties:
            prop_dicts = []
            for p in self.properties:
                prop_dicts.append(p.to_dict())
            result["properties"] = sorted(prop_dicts, key=sort_properties_key)
        return result


@dataclass
class AIBom:
    metadata: Metadata
    components: List[Component] = field(default_factory=list)
    services: List[Service] = field(default_factory=list)
    bom_format: str = "CycloneDX"
    spec_version: str = "1.5"
    version: int = 1
    schema: str = "https://cyclonedx.org/schema/bom-1.5.schema.json"

    def to_dict(self) -> Dict[str, Any]:
        result = {
            "$schema": self.schema,
            "bomFormat": self.bom_format,
            "specVersion": self.spec_version,
            "version": self.version,
            "metadata": self.metadata.to_dict(),
        }

        if self.components:
            sorted_components = sorted(self.components, key=sort_component_key)
            component_dicts = []
            for c in sorted_components:
                component_dicts.append(c.to_dict())
            result["components"] = component_dicts

        if self.services:
            sorted_services = sorted(self.services, key=sort_service_key)
            service_dicts = []
            for s in sorted_services:
                service_dicts.append(s.to_dict())
            result["services"] = service_dicts

        return result

    def to_json(self, indent: int = 2) -> str:
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)

    def to_canonical_dict(self) -> Dict[str, Any]:
        result = {
            "bomFormat": self.bom_format,
            "specVersion": self.spec_version,
            "version": self.version,
        }

        tool_dicts = []
        for t in self.metadata.tools:
            tool_dicts.append(t.to_dict())

        canonical_metadata = {
            "tools": tool_dicts,
        }

        if self.metadata.properties:
            canonical_props = []
            for prop in self.metadata.properties:
                if prop.name not in ["brox.bom.hash", "brox.repo.root"]:
                    canonical_props.append(prop.to_dict())
            if canonical_props:
                canonical_metadata["properties"] = sorted(
                    canonical_props, key=sort_properties_key
                )

        result["metadata"] = canonical_metadata

        if self.components:
            sorted_components = sorted(self.components, key=sort_component_key)
            component_dicts = []
            for c in sorted_components:
                component_dicts.append(c.to_dict())
            result["components"] = component_dicts

        if self.services:
            sorted_services = sorted(self.services, key=sort_service_key)
            service_dicts = []
            for s in sorted_services:
                service_dicts.append(s.to_dict())
            result["services"] = service_dicts

        return result

    def calculate_hash(self) -> str:
        canonical = self.to_canonical_dict()
        content = json.dumps(canonical, separators=(',', ':'), sort_keys=True)
        return f"sha256:{hashlib.sha256(content.encode()).hexdigest()}"


@dataclass
class Capability:

    name: str  # filesystem.read, filesystem.write, exec.shell, network.egress, etc.
    scope: str  # path/domain/tool name
    asset: str  # bom-ref of the asset that grants this capability
    evidence: List[str] = field(default_factory=list)  # file:line pointers

    def to_record_string(self) -> str:
        return json.dumps({
            "cap": self.name,
            "scope": self.scope,
            "asset": self.asset,
            "evidence": self.evidence[:5]
        }, separators=(',', ':'), sort_keys=True)

    def __hash__(self):
        return hash((self.name, self.scope, self.asset))

    def __eq__(self, other):
        if not isinstance(other, Capability):
            return False
        return (
            self.name == other.name and self.scope == other.scope and self.asset == other.asset
        )


@dataclass
class AssetChange:
    id: str  # bom-ref
    kind: str
    change: Optional[str] = None


@dataclass
class CapabilityChange:
    capability: str
    scope: str
    asset: str
    evidence: List[str] = field(default_factory=list)
    from_scope: Optional[str] = None
    to_scope: Optional[str] = None


@dataclass
class Risk:
    level: str  # low, medium, high, critical
    reasons: List[str] = field(default_factory=list)


def create_default_risk() -> Risk:
    return Risk(level="low")


@dataclass
class CapabilityDiff:
    base_ref: str
    base_bom_hash: str
    head_ref: str
    head_bom_hash: str
    assets_added: List[AssetChange] = field(default_factory=list)
    assets_removed: List[AssetChange] = field(default_factory=list)
    assets_changed: List[AssetChange] = field(default_factory=list)
    capabilities_added: List[CapabilityChange] = field(default_factory=list)
    capabilities_removed: List[CapabilityChange] = field(default_factory=list)
    capabilities_widened: List[CapabilityChange] = field(default_factory=list)
    risk: Risk = field(default_factory=create_default_risk)

    def to_dict(self) -> Dict[str, Any]:
        added_assets = []
        for a in self.assets_added:
            added_assets.append({"id": a.id, "kind": a.kind})

        removed_assets = []
        for a in self.assets_removed:
            removed_assets.append({"id": a.id, "kind": a.kind})

        changed_assets = []
        for a in self.assets_changed:
            changed_assets.append({"id": a.id, "kind": a.kind, "change": a.change})

        added_capabilities = []
        for c in self.capabilities_added:
            added_capabilities.append({
                "capability": c.capability,
                "scope": c.scope,
                "asset": c.asset,
                "evidence": c.evidence,
            })

        removed_capabilities = []
        for c in self.capabilities_removed:
            removed_capabilities.append({
                "capability": c.capability,
                "scope": c.scope,
                "asset": c.asset,
                "evidence": c.evidence,
            })

        widened_capabilities = []
        for c in self.capabilities_widened:
            widened_capabilities.append({
                "capability": c.capability,
                "asset": c.asset,
                "from": c.from_scope,
                "to": c.to_scope,
                "evidence": c.evidence,
            })

        return {
            "base": {"ref": self.base_ref, "bom_hash": self.base_bom_hash},
            "head": {"ref": self.head_ref, "bom_hash": self.head_bom_hash},
            "assets": {
                "added": added_assets,
                "removed": removed_assets,
                "changed": changed_assets,
            },
            "capabilities": {
                "added": added_capabilities,
                "removed": removed_capabilities,
                "widened": widened_capabilities,
            },
            "risk": {"level": self.risk.level, "reasons": self.risk.reasons},
        }

    def to_json(self, indent: int = 2) -> str:
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)


def generate_bom_ref(kind: str, *parts: str) -> str:
    return f"{kind}:{':'.join(parts)}"


def hash_content(content: str) -> str:
    return hashlib.sha256(content.encode()).hexdigest()


def hash_content_short(content: str, length: int = 12) -> str:
    return hash_content(content)[:length]


def create_aibom(repo_root: str = ".") -> AIBom:
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    metadata = Metadata(
        timestamp=timestamp,
        tools=[Tool(vendor="brox", name="brox", version="0.1.0")],
        properties=[
            Property(name="brox.repo.root", value=repo_root),
            Property(name="brox.scan.deterministic", value="true"),
        ],
    )

    components = [
        Component(
            type="application",
            name="repo",
            version="0.0.0",
            bom_ref="repo:root",
            properties=[Property(name="brox.ai.asset.kind", value="repo")],
        )
    ]

    return AIBom(metadata=metadata, components=components)
