"""SCM Analyzer — converts local git scan results into typed findings."""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

import yaml

from sentinel.analyzers.base import BaseAnalyzer
from sentinel.models.findings import (
    ComplianceMapping,
    Evidence,
    Finding,
    ScanResult,
    ScanStats,
    Severity,
)

# .gitignore patterns we expect to see for sensitive files
_REQUIRED_GITIGNORE = [
    (r"\.env", "*.env / .env"),
    (r"\.pem", "*.pem"),
    (r"\.key", "*.key"),
]


class ScmAnalyzer(BaseAnalyzer):
    module_name = "scm"

    def analyze(self, collected: dict[str, Any]) -> ScanResult:
        if not collected.get("is_git_repo"):
            return self._make_result(
                [Finding(
                    id="scm.not_git_repo",
                    title="Target is not a git repository",
                    description="The target path does not contain a .git directory. "
                                "SCM checks require a local git repository.",
                    severity=Severity.INFO,
                    module=self.module_name,
                    evidence=[Evidence(file=str(self.repo_path), context=".git not found")],
                    remediation="Initialise a git repository: git init",
                    compliance=ComplianceMapping(),
                )],
                ScanStats(rules_evaluated=1, findings_count=1),
                errors=collected.get("errors", []),
            )

        findings: list[Finding] = []
        hygiene = collected.get("hygiene_files", {})
        gitignore_patterns = collected.get("gitignore_patterns", [])
        commits = collected.get("commit_signing", [])
        tracked_sensitive = collected.get("tracked_sensitive_files", [])
        large_blobs = collected.get("large_blobs", [])

        # Hygiene file checks
        findings += self._check_hygiene(hygiene)

        # .gitignore coverage
        findings += self._check_gitignore(hygiene.get(".gitignore", False), gitignore_patterns)

        # Tracked sensitive files
        findings += self._check_tracked_sensitive(tracked_sensitive)

        # Commit signing
        findings += self._check_commit_signing(commits)

        # Large binary blobs
        findings += self._check_large_blobs(large_blobs)

        stats = ScanStats(
            files_scanned=len(commits),
            rules_evaluated=5,
            findings_count=len(findings),
        )
        return self._make_result(findings, stats, errors=collected.get("errors", []))

    def _check_hygiene(self, hygiene: dict[str, bool]) -> list[Finding]:
        findings: list[Finding] = []
        rules_file = Path(__file__).parent.parent / "rules" / "scm.yml"
        rules_data = yaml.safe_load(rules_file.read_text()) if rules_file.exists() else {}

        rule_map = {r["id"]: r for r in rules_data.get("rules", [])}

        checks = [
            ("CODEOWNERS", "no_codeowners", Severity.MEDIUM),
            ("LICENSE", "no_license_file", Severity.MEDIUM),
            ("README", "no_readme", Severity.INFO),
        ]
        for file_key, rule_id, severity in checks:
            if not hygiene.get(file_key, False):
                rule = rule_map.get(rule_id, {})
                findings.append(Finding(
                    id=f"scm.{rule_id}",
                    title=rule.get("title", f"{file_key} file is absent"),
                    description=str(rule.get("description", f"No {file_key} found.")).strip(),
                    severity=severity,
                    module=self.module_name,
                    evidence=[Evidence(file=file_key, context="File not found in repository")],
                    remediation=str(rule.get("remediation", f"Add a {file_key} file.")).strip(),
                    compliance=ComplianceMapping(
                        **{k: v for k, v in rule.get("compliance", {}).items()
                           if k in ("soc2", "cis", "owasp_cicd", "iso27001")}
                    ) if rule.get("compliance") else ComplianceMapping(),
                ))
        return findings

    def _check_gitignore(self, has_gitignore: bool, patterns: list[str]) -> list[Finding]:
        findings: list[Finding] = []
        if not has_gitignore:
            findings.append(Finding(
                id="scm.no_gitignore",
                title="No .gitignore file present",
                description=(
                    "No .gitignore file was found. Without it, sensitive files (.env, keys, "
                    "build artifacts) may be accidentally committed."
                ),
                severity=Severity.MEDIUM,
                module=self.module_name,
                evidence=[Evidence(file=".gitignore", context="File not found")],
                remediation=(
                    "Create a .gitignore appropriate for your stack. "
                    "Use gitignore.io for a comprehensive baseline."
                ),
                compliance=ComplianceMapping(
                    soc2=["CC6.1"],
                    iso27001=["A.9.4.3"],
                ),
            ))
            return findings

        patterns_text = "\n".join(patterns)
        missing: list[str] = []
        for pattern_regex, pattern_desc in _REQUIRED_GITIGNORE:
            if not re.search(pattern_regex, patterns_text):
                missing.append(pattern_desc)

        if missing:
            findings.append(Finding(
                id="scm.gitignore_missing_patterns",
                title=".gitignore missing patterns for sensitive file types",
                description=(
                    f"The .gitignore is missing entries for: {', '.join(missing)}. "
                    "Developers may accidentally commit credentials."
                ),
                severity=Severity.MEDIUM,
                module=self.module_name,
                evidence=[Evidence(file=".gitignore", context=f"Missing: {', '.join(missing)}")],
                remediation=(
                    "Add the following to .gitignore:\n"
                    "  .env\n  .env.*\n  *.pem\n  *.key\n  *.p12\n  *.pfx"
                ),
                compliance=ComplianceMapping(
                    soc2=["CC6.1"],
                    cis=["CIS-GitLab-2.3"],
                    iso27001=["A.9.4.3"],
                ),
            ))
        return findings

    def _check_tracked_sensitive(self, tracked: list[str]) -> list[Finding]:
        findings: list[Finding] = []
        for path_str in tracked:
            findings.append(Finding(
                id=f"scm.tracked_sensitive.{abs(hash(path_str)):05d}",
                title=f"Sensitive file tracked in git: {path_str}",
                description=(
                    f"The file `{path_str}` matches a sensitive file pattern and is tracked "
                    "in the git index. It may contain real credentials that are now in history."
                ),
                severity=Severity.HIGH,
                module=self.module_name,
                evidence=[Evidence(file=path_str, context="Tracked in git index")],
                remediation=(
                    f"1. Run: git rm --cached {path_str}\n"
                    f"2. Add to .gitignore\n"
                    "3. Rotate any credentials that may have been committed\n"
                    "4. Consider purging history with git-filter-repo"
                ),
                compliance=ComplianceMapping(
                    soc2=["CC6.1"],
                    cis=["CIS-GitLab-2.3"],
                    iso27001=["A.9.4.3"],
                ),
                tags=["sensitive-file"],
            ))
        return findings

    def _check_commit_signing(self, commits: list[dict[str, Any]]) -> list[Finding]:
        if not commits:
            return []
        total = len(commits)
        signed = sum(1 for c in commits if c.get("signed"))
        unsigned_pct = round((total - signed) / total * 100, 1)

        if unsigned_pct == 0:
            return []

        # Find examples of unsigned commits
        unsigned_examples = [
            c["sha"] for c in commits if not c.get("signed")
        ][:5]

        return [Finding(
            id="scm.unsigned_commits",
            title=f"{unsigned_pct}% of recent commits are unsigned",
            description=(
                f"{total - signed} of the last {total} commits lack a GPG/SSH signature. "
                "Unsigned commits cannot be attributed to a verified identity."
            ),
            severity=Severity.LOW,
            module=self.module_name,
            evidence=[Evidence(
                file=".git/logs",
                context=f"Unsigned commits (sample): {', '.join(unsigned_examples)}",
            )],
            remediation=(
                "Enable commit signing:\n"
                "  git config --global commit.gpgsign true\n"
                "Enforce in GitLab: Settings → Repository → Push rules → "
                "Reject unsigned commits."
            ),
            compliance=ComplianceMapping(
                soc2=["CC6.1", "CC8.1"],
                cis=["CIS-GitLab-1.3"],
                iso27001=["A.8.32"],
            ),
        )]

    def _check_large_blobs(self, blobs: list[dict[str, Any]]) -> list[Finding]:
        if not blobs:
            return []
        unique_paths = list({b["path"] for b in blobs})[:10]
        return [Finding(
            id="scm.large_binary_blobs",
            title=f"Large binary files detected in git history ({len(blobs)} instances)",
            description=(
                f"Found large binary blobs (>1MB) in git history: "
                f"{', '.join(unique_paths[:5])}{'...' if len(unique_paths) > 5 else ''}. "
                "Large binaries bloat the repository and may contain sensitive data."
            ),
            severity=Severity.MEDIUM,
            module=self.module_name,
            evidence=[Evidence(
                file=b["path"],
                context=f"{b['size_mb']}MB in commit {b['commit']}",
            ) for b in blobs[:3]],
            remediation=(
                "Use git-lfs for large binary assets. "
                "Remove them from history using git-filter-repo. "
                "Store build artifacts in GitLab Package Registry."
            ),
            compliance=ComplianceMapping(
                soc2=["CC8.1"],
                iso27001=["A.8.32"],
            ),
        )]
