from __future__ import annotations

import json
import os
import re
from typing import Dict, List

from bastion.models import Capability, Evidence


COMMON_PACKAGES = {
    "requests", "flask", "django", "numpy", "pandas", "cryptography",
    "boto3", "urllib3", "setuptools", "pip", "colorama", "beautifulsoup4",
    "pillow", "pyyaml", "scikit-learn", "tensorflow", "pytorch", "express",
    "lodash", "axios", "react", "webpack", "typescript", "eslint",
    "prettier", "jest", "mocha", "chalk", "commander", "inquirer",
}


def levenshtein_distance(s1: str, s2: str) -> int:
    """Calculate the Levenshtein distance between two strings."""
    if len(s1) < len(s2):
        return levenshtein_distance(s2, s1)
    
    if len(s2) == 0:
        return len(s1)
    
    previous_row = range(len(s2) + 1)
    for i, c1 in enumerate(s1):
        current_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            substitutions = previous_row[j] + (c1 != c2)
            current_row.append(min(insertions, deletions, substitutions))
        previous_row = current_row
    
    return previous_row[-1]


def is_typosquat(package_name: str) -> bool:
    """Check if a package name is a potential typosquat of common packages."""
    normalized = package_name.lower().replace('_', '-').replace('.', '-')
    
    for common_pkg in COMMON_PACKAGES:
        common_normalized = common_pkg.lower().replace('_', '-').replace('.', '-')
        if normalized == common_normalized:
            return False
        distance = levenshtein_distance(normalized, common_normalized)
        if distance <= 2:
            return True
    
    return False


class DependencyAnalyzer:
    """Analyzes dependency files for supply-chain risks."""
    
    def analyze_plugin_dependencies(self, plugin_path: str, plugin_type: str) -> List[Capability]:
        """
        Analyze plugin dependencies for supply-chain risks.
        
        Args:
            plugin_path: Path to the plugin file or folder
            plugin_type: Either "file" or "folder"
        
        Returns:
            List of Capability objects with category="dependency_risk"
        """
        scan_dir = plugin_path
        
        if plugin_type == "file":
            scan_dir = os.path.dirname(plugin_path)
        
        all_evidence: List[Evidence] = []
        
        dependency_files = ["requirements.txt", "package.json", "setup.py", "pyproject.toml", "Pipfile"]
        
        for dep_file in dependency_files:
            file_path = os.path.join(scan_dir, dep_file)
            if os.path.isfile(file_path):
                evidence = self._analyze_file(file_path)
                all_evidence.extend(evidence)
        
        if not all_evidence:
            return []
        
        max_severity = self._calculate_max_severity(all_evidence)
        
        return [
            Capability(
                category="dependency_risk",
                description="Supply-chain risk detected in dependencies",
                evidence=all_evidence,
                severity=max_severity,
            )
        ]
    
    def _calculate_max_severity(self, evidence_list: List[Evidence]) -> str:
        """Calculate the maximum severity from evidence patterns."""
        severity_order = {"Low": 0, "Medium": 1, "High": 2, "Critical": 3}
        max_severity = "Low"
        
        pattern_severity_map = {
            "url_install_critical": "Critical",
            "index_url_critical": "Critical",
            "dependency_links_critical": "Critical",
            "http_dependency": "Critical",
            "git_ssh_install": "High",
            "editable_url_install": "High",
            "typosquat": "High",
            "git_url": "High",
            "file_protocol": "High",
            "url_in_install_requires": "High",
            "url_dependency_pyproject": "High",
            "url_build_system": "High",
            "install_script_postinstall": "High",
            "install_script_preinstall": "High",
            "wildcard_version": "Medium",
            "unpinned": "Low",
        }
        
        for evidence in evidence_list:
            for pattern, severity in pattern_severity_map.items():
                if pattern in evidence.pattern_name:
                    if severity_order.get(severity, 0) > severity_order.get(max_severity, 0):
                        max_severity = severity
                    break
        
        return max_severity
    
    def _analyze_file(self, file_path: str) -> List[Evidence]:
        """Analyze a single dependency file."""
        filename = os.path.basename(file_path)
        
        try:
            with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
                content = f.read()
                lines = content.splitlines()
        except Exception:
            return []
        
        evidence = []
        
        if filename == "requirements.txt" or filename == "Pipfile":
            evidence.extend(self._analyze_requirements(file_path, lines))
        elif filename == "package.json":
            evidence.extend(self._analyze_package_json(file_path, content))
        elif filename == "setup.py":
            evidence.extend(self._analyze_setup_py(file_path, lines))
        elif filename == "pyproject.toml":
            evidence.extend(self._analyze_pyproject_toml(file_path, lines))
        
        return evidence
    
    def _analyze_requirements(self, file_path: str, lines: List[str]) -> List[Evidence]:
        """Analyze requirements.txt or Pipfile for risks."""
        evidence = []
        
        for i, line in enumerate(lines, 1):
            stripped = line.strip()
            
            if not stripped or stripped.startswith('#'):
                continue
            
            if "--index-url" in stripped or "--extra-index-url" in stripped:
                if "pypi.org" not in stripped.lower() and "pypi.python.org" not in stripped.lower():
                    evidence.append(Evidence(
                        file=file_path,
                        line=i,
                        code_snippet=stripped[:200],
                        pattern_name="index_url_critical",
                    ))
                    continue
            
            if stripped.startswith("-e "):
                package_spec = stripped[3:].strip()
                if package_spec.startswith(("git+https://", "git+http://", "http://", "https://")):
                    evidence.append(Evidence(
                        file=file_path,
                        line=i,
                        code_snippet=stripped[:200],
                        pattern_name="editable_url_install",
                    ))
                    continue
            
            if stripped.startswith(("git+https://", "git+http://")):
                evidence.append(Evidence(
                    file=file_path,
                    line=i,
                    code_snippet=stripped[:200],
                    pattern_name="url_install_critical",
                ))
                continue
            
            if stripped.startswith("git+ssh://"):
                evidence.append(Evidence(
                    file=file_path,
                    line=i,
                    code_snippet=stripped[:200],
                    pattern_name="git_ssh_install",
                ))
                continue
            
            if re.match(r'^https?://', stripped):
                evidence.append(Evidence(
                    file=file_path,
                    line=i,
                    code_snippet=stripped[:200],
                    pattern_name="url_install_critical",
                ))
                continue
            
            package_match = re.match(r'^([a-zA-Z0-9_\-\.]+)(.*)$', stripped.split(';')[0])
            if package_match:
                package_name = package_match.group(1)
                spec_part = package_match.group(2) if package_match.group(2) else ""
                
                if is_typosquat(package_name):
                    evidence.append(Evidence(
                        file=file_path,
                        line=i,
                        code_snippet=stripped[:200],
                        pattern_name="typosquat",
                    ))
                    continue
                
                if not re.search(r'(==|>=|<=|~=|>|<|\[\w+\])', spec_part) and not spec_part.startswith(';'):
                    evidence.append(Evidence(
                        file=file_path,
                        line=i,
                        code_snippet=stripped[:200],
                        pattern_name="unpinned",
                    ))
        
        return evidence
    
    def _analyze_package_json(self, file_path: str, content: str) -> List[Evidence]:
        """Analyze package.json for risks."""
        evidence = []
        lines = content.splitlines()
        
        try:
            data = json.loads(content)
        except Exception:
            return evidence
        
        deps_to_check = {
            "dependencies": data.get("dependencies", {}),
            "devDependencies": data.get("devDependencies", {}),
        }
        
        for dep_type, deps in deps_to_check.items():
            if not isinstance(deps, dict):
                continue
            
            for pkg_name, version_spec in deps.items():
                if not isinstance(version_spec, str):
                    continue
                
                if version_spec.startswith("git+"):
                    line_num = self._find_line_number(lines, f'"{pkg_name}"')
                    evidence.append(Evidence(
                        file=file_path,
                        line=line_num,
                        code_snippet=f'{pkg_name}: {version_spec}'[:200],
                        pattern_name="git_url",
                    ))
                
                if version_spec.startswith("file:"):
                    line_num = self._find_line_number(lines, f'"{pkg_name}"')
                    evidence.append(Evidence(
                        file=file_path,
                        line=line_num,
                        code_snippet=f'{pkg_name}: {version_spec}'[:200],
                        pattern_name="file_protocol",
                    ))
                
                if version_spec.startswith("http://"):
                    line_num = self._find_line_number(lines, f'"{pkg_name}"')
                    evidence.append(Evidence(
                        file=file_path,
                        line=line_num,
                        code_snippet=f'{pkg_name}: {version_spec}'[:200],
                        pattern_name="http_dependency",
                    ))
                
                if version_spec == "*":
                    line_num = self._find_line_number(lines, f'"{pkg_name}"')
                    evidence.append(Evidence(
                        file=file_path,
                        line=line_num,
                        code_snippet=f'{pkg_name}: "{version_spec}"'[:200],
                        pattern_name="wildcard_version",
                    ))
        
        scripts = data.get("scripts", {})
        if isinstance(scripts, dict):
            for script_key in ("preinstall", "postinstall"):
                if script_key in scripts:
                    line_num = self._find_line_number(lines, f'"{script_key}"')
                    evidence.append(Evidence(
                        file=file_path,
                        line=line_num,
                        code_snippet=f'scripts.{script_key}: {scripts[script_key]}'[:200],
                        pattern_name=f"install_script_{script_key}",
                    ))
        
        return evidence
    
    def _analyze_setup_py(self, file_path: str, lines: List[str]) -> List[Evidence]:
        """Analyze setup.py for risks."""
        evidence = []
        content = "\n".join(lines)
        
        if "dependency_links" in content:
            for i, line in enumerate(lines, 1):
                if "dependency_links" in line:
                    evidence.append(Evidence(
                        file=file_path,
                        line=i,
                        code_snippet=line.strip()[:200],
                        pattern_name="dependency_links_critical",
                    ))
        
        if "install_requires" in content:
            in_install_requires = False
            bracket_depth = 0
            
            for i, line in enumerate(lines, 1):
                if "install_requires" in line:
                    in_install_requires = True
                
                if in_install_requires:
                    bracket_depth += line.count('[') + line.count('(')
                    bracket_depth -= line.count(']') + line.count(')')
                    
                    if re.search(r'(?:https?://|git\+)', line):
                        evidence.append(Evidence(
                            file=file_path,
                            line=i,
                            code_snippet=line.strip()[:200],
                            pattern_name="url_in_install_requires",
                        ))
                    
                    if bracket_depth == 0 and "[" in content[:content.find(line)] and "]" in line:
                        in_install_requires = False
        
        return evidence
    
    def _analyze_pyproject_toml(self, file_path: str, lines: List[str]) -> List[Evidence]:
        """Analyze pyproject.toml for risks."""
        evidence = []
        
        in_dependencies = False
        in_build_requires = False
        
        for i, line in enumerate(lines, 1):
            stripped = line.strip()
            
            if stripped.startswith("[project"):
                in_dependencies = True
            elif stripped.startswith("[build-system"):
                in_build_requires = True
            elif stripped.startswith("[") and not stripped.startswith("[project") and not stripped.startswith("[build-system"):
                in_dependencies = False
                in_build_requires = False
            
            if in_dependencies and stripped.startswith("dependencies"):
                for j in range(i, min(i + 50, len(lines))):
                    dep_line = lines[j].strip()
                    if dep_line.startswith("]"):
                        break
                    if re.search(r'(?:https?://|git\+)', dep_line):
                        evidence.append(Evidence(
                            file=file_path,
                            line=j + 1,
                            code_snippet=dep_line[:200],
                            pattern_name="url_dependency_pyproject",
                        ))
            
            if in_build_requires and stripped.startswith("requires"):
                for j in range(i, min(i + 50, len(lines))):
                    req_line = lines[j].strip()
                    if req_line.startswith("]"):
                        break
                    if re.search(r'(?:https?://|git\+)', req_line):
                        evidence.append(Evidence(
                            file=file_path,
                            line=j + 1,
                            code_snippet=req_line[:200],
                            pattern_name="url_build_system",
                        ))
        
        return evidence
    
    def _find_line_number(self, lines: List[str], search_str: str) -> int:
        """Find the line number containing a search string."""
        for i, line in enumerate(lines, 1):
            if search_str in line:
                return i
        return 1
