from typing import Any, Dict, List, Optional
import logging
import os
import re
from .patch_utils import findings_to_patch

logger = logging.getLogger(__name__)

# Word-boundary regex for PQC/crypto keyword matching to avoid false positives
# e.g. "des" should not match "describes", "sha" should not match "shape"
# Intentionally broad: better to catch a few general issues than miss crypto findings
_PQC_PATTERN = re.compile(
    r"\b("
    r"quantum|post-quantum|shor|grover"
    r"|crypto(?:graphy|graphic(?:ally)?)?"
    r"|cipher(?:text)?|decipher|aes|rsa|hash(?:ing)?|md5|sha[\d-]|des(?:ede)?"
    r"|key.?size|encrypt(?:ion|ed)?|decrypt"
    r"|tls|ssl|hmac|pbkdf|diffie.?hellman"
    r"|ecdsa|ecdh|dsa|blowfish|3des|triple.?des"
    r"|private.?key|public.?key|priv.?key|certificates?"
    r"|gcm|cbc|ecb|ctr"
    r"|chacha|poly1305|bcrypt|scrypt|argon2?"
    r"|x509|pkcs|nonce"
    r")\b",
    re.IGNORECASE,
)


def extract_fix_records(scan_json: Dict[str, Any], base_path: Optional[str] = None) -> List[Dict[str, Any]]:
    """Extract records from a scanner JSON result, with fix flag and Git patches.

    Extracts all findings and marks whether each is fixable via a "has_fix"
    boolean. Includes fix payload fields (fix_text, fixed_lines) when present.
    For fixable findings, generates Git unified diff patches.

    Args:
        scan_json: The scanner output JSON (top-level object with "results").
        base_path: Optional base directory path to resolve relative file paths.
                   If not provided, uses current working directory.

    Returns:
        List of dicts with keys:
            - tool_version, engine_kind
            - fingerprint, check_id, path
            - start_line, start_col, end_line, end_col
            - severity, message, is_ignored, validation_state
            - shortlink, rule_url, cwe, owasp, technology, vulnerability_class
            - fix_text, fixed_lines, original_line, has_fix
            - patch (Git unified diff, only present if has_fix is True and patch generation succeeds)

    Raises:
        ValueError: If scan_json is not a valid dictionary or has invalid structure
    """
    if not isinstance(scan_json, dict):
        raise ValueError("scan_json must be a dictionary")

    if not scan_json:
        logger.warning("Empty scan_json provided")
        return []

    try:
        results = scan_json.get("results", []) or []
        if not isinstance(results, list):
            raise ValueError("'results' field must be a list")

        tool_version = scan_json.get("version")
        # OpenGrep reports for engine may live in each finding; prefer top level if present
        engine_kind = scan_json.get("engine_kind")
    except Exception as e:
        logger.error(f"Error parsing scan_json structure: {e}")
        raise ValueError(f"Invalid scan_json structure: {e}")

    extracted: List[Dict[str, Any]] = []
    for i, r in enumerate(results):
        if not isinstance(r, dict):
            logger.warning(f"Skipping invalid result at index {i}: not a dictionary")
            continue

        try:
            extra = r.get("extra", {}) or {}
            metadata = extra.get("metadata", {}) or {}
            fix_text = extra.get("fix")
            fixed_lines = extra.get("fixed_lines") or []
            original_line = extra.get("lines") or r.get("lines")

            has_fix = bool(fix_text) or bool(fixed_lines)

            start = r.get("start", {}) or {}
            end = r.get("end", {}) or {}

            rule_url = None

            # Determine finding category: pqc or general
            # Uses word-boundary regex to avoid false positives (e.g. "des" in "describes")
            message_text = extra.get("message") or r.get("message") or ""
            raw_check_id = r.get("check_id") or ""
            rule_name = raw_check_id.rsplit(".", 1)[-1] if raw_check_id else ""

            if metadata.get("quantum_vulnerable") or _PQC_PATTERN.search(message_text) or _PQC_PATTERN.search(rule_name):
                category = "pqc"
            else:
                category = "general"

            record = {
                "tool_version": tool_version,
                "engine_kind": r.get("engine_kind") or engine_kind,
                "fingerprint": r.get("fingerprint"),
                "check_id": r.get("check_id"),
                "path": r.get("path"),
                "start_line": start.get("line"),
                "start_col": start.get("col"),
                "end_line": end.get("line"),
                "end_col": end.get("col"),
                "severity": extra.get("severity"),
                "message": extra.get("message"),
                "is_ignored": r.get("is_ignored"),
                "validation_state": r.get("validation_state"),
                "shortlink": metadata.get("shortlink"),
                "rule_url": rule_url,
                "cwe": metadata.get("cwe") or [],
                "owasp": metadata.get("owasp") or [],
                "technology": metadata.get("technology") or [],
                "vulnerability_class": metadata.get("vulnerability_class") or [],
                "fix_text": fix_text,
                "fixed_lines": fixed_lines,
                "original_line": original_line,
                "has_fix": has_fix,
                "category": category,
            }

            # Generate Git patch for fixable findings
            if has_fix and fixed_lines:
                try:
                    # Resolve file path
                    file_path = r.get("path")
                    if file_path:
                        if not os.path.isabs(file_path):
                            # Resolve relative path using base_path
                            resolved_path = os.path.join(base_path or os.getcwd(), file_path)
                        else:
                            resolved_path = file_path

                        # Generate the patch
                        patch = findings_to_patch(
                            file_path=resolved_path,
                            start_line=start.get("line"),
                            end_line=end.get("line"),
                            original_line=original_line,
                            fixed_lines=fixed_lines,
                        )

                        if patch:
                            record["patch"] = patch
                            logger.debug(f"Generated patch for {file_path}:{start.get('line')}")
                        else:
                            logger.warning(f"Failed to generate patch for {file_path}:{start.get('line')}")
                except Exception as e:
                    logger.warning(f"Error generating patch for result at index {i}: {e}")

            extracted.append(record)
        except Exception as e:
            logger.warning(f"Error processing result at index {i}: {e}")
            continue

    return extracted

