import json
import traceback
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple, Set, Any
from urllib.parse import urlparse
from pathlib import Path

from libs.osaf.osaf_core import OSAF_Core
from libs.spider_logger.logger import SpiderLog
from libs.unirepo import search


@dataclass
class URI:
    """Represents a Uniform Resource Identifier with validation."""
    original_uri: str
    scheme: str
    path: str

    def __init__(self, uri_str: str):
        self.original_uri = uri_str
        parsed = urlparse(uri_str)
        self.scheme = parsed.scheme
        self.path = parsed.path

    @property
    def is_local(self) -> bool:
        return self.scheme == 'file'

    def __str__(self) -> str:
        return self.original_uri


@dataclass
class BundleFile:
    """Represents a file within a package bundle."""
    filename: str
    hash: Optional[str] = None
    size: int = -1


class UPackageBundle:
    """Represents a specific version/bundle of a package."""

    def __init__(self, parent_package: 'UPackage', bundle_id: str):
        self.id = bundle_id
        self.parent_package = parent_package
        self.full_url = f'{parent_package.full_url}/bundles/{bundle_id}/'

        # Initialize all attributes
        self.display_name: Optional[str] = None
        self.description: Optional[str] = None
        self.tags: Dict[str, Any] = {}
        self.deps: List[str] = []
        self.files: List[BundleFile] = []
        self.ext: Optional[Dict[str, Any]] = None
        self.note: Optional[str] = None
        self.contained_name: Optional[str] = None

        parent_package.repo.unirepo.log.debug(f'Created UPackageBundle {bundle_id} for package {parent_package.id}')

    def load(self) -> None:
        """Load bundle metadata from bundle.json."""
        bundle_url = f"{self.full_url}/bundle.json"
        bundle_raw = self.parent_package.repo.unirepo.get_file_content(bundle_url)

        try:
            bundle_data = json.loads(bundle_raw)
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON in bundle {bundle_url}: {e}")

        # Required fields
        self.display_name = bundle_data['display_name']
        self.description = bundle_data['description']
        self.tags = bundle_data.get('tags', {})
        self.deps = bundle_data.get('deps', [])

        # Files list
        self.files = [
            BundleFile(
                filename=file_info['filepath'],
                hash=file_info.get('hash'),
                size=file_info.get('size', -1)
            ) for file_info in bundle_data.get('files', [])
        ]

        # Optional fields
        self.note = bundle_data.get('note')
        self.ext = bundle_data.get('ext')
        self.contained_name = bundle_data.get('contained_name', f'{self.parent_package.id}-{self.id}')


class UniRepo:
    """Unified repository manager for handling package sources and searches."""

    def __init__(self, logger: SpiderLog, osa: OSAF_Core, conf: Dict[str, Any], sources: Dict[str, Any]):
        self.log = logger
        self.osa = osa
        self.conf = conf
        self.raw_sources = sources

        # Query configuration
        self.query_shortcuts = {
            "v": "version",
            "a": "arch",
            "c": "cert",
            "n": "name",
            "s": "source"
        }
        self.arch_aliases = conf.get("arch_aliases", {})

        # Data storage
        self.managed_sources: List[Source] = []
        self.data_base: List[Source] = []
        self.extra_filters: Dict[str, Any] = {}
        self._cache_made = False

        self.log.debug(f"Arch aliases: {self.arch_aliases}")
        self._load_sources(sources)

    def _load_sources(self, sources: Dict[str, Any]) -> None:
        """Load and validate repository sources."""
        for source_config in sources.get('sources', []):
            try:
                uri = URI(source_config['url'])
                enabled = source_config.get('enabled', False)

                self.log.debug(f"Processing source: {source_config}, enabled: {enabled}")

                if enabled:
                    source = Source(self, uri)
                    source.read_repos()
                    self.managed_sources.append(source)
                    self.log.info(f"Added source: {uri}")
                else:
                    self.log.warning(f"Skipping disabled source: {uri}")

            except Exception as e:
                self.log.error(f"Failed to load source {source_config}: {e}")
                self.log.debug(traceback.format_exc())

    def get_file_content(self, uri: str) -> str:
        """Retrieve content from a URI."""
        uri_obj = URI(uri)
        content = ""

        try:
            if uri_obj.scheme in ("http", "https"):
                code, content = self.osa.web.getText(uri_obj.original_uri)
                if code != 200:
                    raise ValueError(f"HTTP error {code} for {uri}")

            elif uri_obj.scheme == "file":
                file_path = Path(uri_obj.path)
                if not self.osa.files.exists(str(file_path)):
                    raise FileNotFoundError(f"File not found: {file_path}")
                content = self.osa.files.read_str(str(file_path))

            else:
                raise ValueError(f"Unsupported scheme: {uri_obj.scheme}")

        except Exception as e:
            self.log.error(f"Failed to get {uri}: {e}")
            raise

        return content

    def make_cache(self) -> None:
        """Build the package cache from all sources."""
        if self._cache_made:
            return

        self.log.info("Building UniRepo cache...")
        self.data_base = self.managed_sources.copy()
        self._cache_made = True

    def parse_query(self, query_string: str) -> Dict[str, Any]:
        """Parse a query string into search filters."""
        if not query_string:
            return {}

        parts = query_string.split("?", 1)
        filters = {'name': parts[0], '__unclean': []}

        if len(parts) > 1:
            for tag in parts[1].split("+"):
                if not tag:
                    continue

                key_value = tag.split("=", 1)
                if len(key_value) == 1:
                    key = self.query_shortcuts.get(key_value[0], key_value[0])
                    filters[key] = '__no_value'
                elif len(key_value) == 2:
                    key = self.query_shortcuts.get(key_value[0], key_value[0])
                    filters[key] = key_value[1]
                else:
                    filters['__unclean'].append(tag)
                    self.log.warning(f"Invalid query part: {tag}")

        return filters

    def search(self, pkg_query: str, strict_names: bool = True) -> Tuple[List[UPackageBundle], List[Tuple]]:
        """Search for packages matching the query."""
        self.make_cache()

        filters = self.parse_query(pkg_query)
        filters.update(self.extra_filters)

        search_results = []
        non_strict_results = []

        for source in self.data_base:
            for repo in source.repos:
                for package in repo.packages:
                    for bundle in package.versions:
                        if self._bundle_matches_filters(filters, bundle):
                            search_results.append(bundle)

                        # Calculate similarity for non-strict matching
                        similarity = search.levenstein(bundle.parent_package.id, filters.get('name', ''))
                        non_strict_results.append((bundle, similarity))

        self.log.debug(f"Search '{pkg_query}' found {len(search_results)} results")
        return search_results, non_strict_results

    def _bundle_matches_filters(self, filters: Dict[str, Any], bundle: UPackageBundle) -> bool:
        """Check if a bundle matches all filters."""
        bundle_tags = {
            'name': bundle.parent_package.id,
            'bundle-id': bundle.id,
            'source': bundle.parent_package.repo.source.id,
            'repo': bundle.parent_package.repo.id,
            **bundle.tags
        }

        for key, filter_value in filters.items():
            if key.startswith('__'):  # Skip internal keys
                continue

            bundle_value = bundle_tags.get(key)
            if not self._values_match(bundle_value, filter_value):
                return False

        return True

    def _values_match(self, bundle_value: Any, filter_value: Any) -> bool:
        """Check if bundle value matches filter value."""
        if filter_value == 'any':
            return True

        if bundle_value is None:
            return False

        if isinstance(bundle_value, list):
            return any(
                str(item).lower() == str(filter_value).lower()
                for item in bundle_value
            )
        else:
            return str(bundle_value).lower() == str(filter_value).lower()

    def download_bundle(self, bundle: UPackageBundle, target_dir: str) -> None:
        """Download all files in a bundle to target directory."""
        self.log.info(f"Downloading bundle {bundle.full_url} to {target_dir}")

        for bundle_file in bundle.files:
            file_url = f"{bundle.full_url}/{bundle_file.filename}"

            if bundle.parent_package.repo.source.uri.is_local:
                # Local file copy
                source_path = Path(bundle.parent_package.repo.source.uri.path) / bundle_file.filename
                self.osa.files.cp(str(source_path), f"{target_dir}/{bundle_file.filename}")
            else:
                # Remote download
                self.osa.web.getFileInDir(file_url, target_dir, bundle_file.filename)


class Source:
    """Represents a source of repositories."""

    def __init__(self, unirepo: UniRepo, uri: URI):
        self.unirepo = unirepo
        self.uri = uri
        self.full_url = uri.original_uri
        self.type = 'remote-http' if uri.scheme in ('http', 'https') else 'local'
        self.repos: List[Repo] = []

        self._load_metadata()

    def _load_metadata(self) -> None:
        """Load source metadata from source.json."""
        content = self.unirepo.get_file_content(f"{self.full_url}/source.json")
        data = json.loads(content)

        self.id = data['id']
        if not self.id:
            raise ValueError("Source ID cannot be empty")

        # Filter enabled repos
        enabled_repos = [
            repo_config['id'] for repo_config in data.get('repos', [])
            if repo_config.get('enabled', False)
        ]

        self.unirepo.log.debug(f"Source {self.id} has {len(enabled_repos)} enabled repos")
        self.repos = [Repo(self, repo_id) for repo_id in enabled_repos]


class Repo:
    """Represents a repository within a source."""

    def __init__(self, source: Source, repo_id: str):
        self.source = source
        self.id = repo_id
        self.full_url = f"{source.full_url}/{repo_id}/"
        self.packages: List[UPackage] = []

        self._load_packages()

    def _load_packages(self) -> None:
        """Load packages from repo index."""
        try:
            index_content = self.source.unirepo.get_file_content(f"{self.full_url}/index.json")
            index_data = json.loads(index_content)

            for package_id in index_data.get('packages', []):
                try:
                    package = UPackage(self, package_id)
                    package.load()
                    self.packages.append(package)
                except Exception as e:
                    self.source.unirepo.log.error(f"Failed to load package {package_id}: {e}")

        except Exception as e:
            self.source.unirepo.log.error(f"Failed to load repo {self.id}: {e}")


class UPackage:
    """Represents a package within a repository."""

    def __init__(self, repo: Repo, package_id: str):
        self.repo = repo
        self.id = package_id
        self.full_url = f"{repo.full_url}/{package_id}/"
        self.versions: List[UPackageBundle] = []

    def load(self) -> None:
        """Load package metadata and all versions."""
        try:
            package_content = self.repo.source.unirepo.get_file_content(f"{self.full_url}/package.json")
            package_data = json.loads(package_content)

            for version_id in package_data.get('bundles', []):
                bundle = UPackageBundle(self, version_id)
                bundle.load()
                self.versions.append(bundle)

        except Exception as e:
            self.repo.source.unirepo.log.error(f"Failed to load package {self.id}: {e}")
            raise