#!/usr/bin/env python3
"""Python regex validation utility for secure log parsing.

This script helps you validate a regular expression against real-world log samples
before using it in production. It checks:
- whether the pattern compiles
- which lines match or fail
- whether named groups extract expected fields
- whether suspicious patterns may be too permissive
- basic timing on sample inputs to spot obvious performance issues

Usage examples:
    python regex_validate.py --pattern '^(?P<ts>\S+)\s+(?P<level>INFO|WARN|ERROR)\s+(?P<service>[a-z0-9_-]+)\s+user=(?P<user>[A-Za-z0-9._-]+)\s+request_id=(?P<rid>[A-Fa-f0-9-]{8,36})$' \
        --sample-file samples.txt

    python regex_validate.py --pattern-file pattern.txt --sample-file samples.txt --mode fullmatch

The script is intentionally non-destructive. It only reads input and prints results.
"""

from __future__ import annotations

import argparse
import re
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional, Sequence, Tuple


@dataclass
class ValidationResult:
    line_no: int
    line: str
    matched: bool
    groups: Optional[dict]
    elapsed_ms: float
    note: str = ""


def read_text_from_file(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except FileNotFoundError:
        raise SystemExit(f"Input file not found: {path}")
    except OSError as exc:
        raise SystemExit(f"Unable to read file {path}: {exc}")


def load_pattern(args: argparse.Namespace) -> str:
    if args.pattern and args.pattern_file:
        raise SystemExit("Use either --pattern or --pattern-file, not both.")
    if not args.pattern and not args.pattern_file:
        raise SystemExit("Provide a regex via --pattern or --pattern-file.")

    if args.pattern_file:
        return read_text_from_file(Path(args.pattern_file)).strip()
    return args.pattern


def load_samples(args: argparse.Namespace) -> List[str]:
    samples: List[str] = []

    if args.sample_file:
        content = read_text_from_file(Path(args.sample_file))
        samples.extend(content.splitlines())

    if args.sample:
        samples.extend(args.sample)

    if not samples:
        raise SystemExit("Provide at least one sample line with --sample or --sample-file.")

    return samples


def compile_pattern(pattern_text: str, flags: int) -> re.Pattern:
    try:
        return re.compile(pattern_text, flags)
    except re.error as exc:
        raise SystemExit(f"Regex compilation failed: {exc}")


def match_line(pattern: re.Pattern, line: str, mode: str) -> Tuple[bool, Optional[re.Match]]:
    if mode == "fullmatch":
        m = pattern.fullmatch(line)
    elif mode == "match":
        m = pattern.match(line)
    else:
        m = pattern.search(line)
    return (m is not None), m


def validate_samples(pattern: re.Pattern, samples: Sequence[str], mode: str, max_len: int) -> List[ValidationResult]:
    results: List[ValidationResult] = []
    for idx, line in enumerate(samples, start=1):
        safe_line = line[:max_len]
        start = time.perf_counter()
        matched, m = match_line(pattern, safe_line, mode)
        elapsed_ms = (time.perf_counter() - start) * 1000.0
        groups = m.groupdict() if m else None
        note = ""
        if len(line) > max_len:
            note = f"truncated input for display/validation to {max_len} chars"
        results.append(
            ValidationResult(
                line_no=idx,
                line=line,
                matched=matched,
                groups=groups,
                elapsed_ms=elapsed_ms,
                note=note,
            )
        )
    return results


def summarize(results: Sequence[ValidationResult]) -> Tuple[int, int, float]:
    matches = sum(1 for r in results if r.matched)
    non_matches = len(results) - matches
    avg_ms = sum(r.elapsed_ms for r in results) / len(results)
    return matches, non_matches, avg_ms


def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Validate Python regex patterns for secure log parsing and data extraction."
    )
    parser.add_argument("--pattern", help="Regex pattern to validate.")
    parser.add_argument("--pattern-file", help="Path to a file containing the regex pattern.")
    parser.add_argument(
        "--sample",
        action="append",
        help="Sample log line to test. Can be provided multiple times.",
    )
    parser.add_argument(
        "--sample-file",
        help="Path to a file containing sample log lines, one per line.",
    )
    parser.add_argument(
        "--mode",
        choices=("fullmatch", "match", "search"),
        default="fullmatch",
        help="Matching mode. fullmatch is safest for validating complete log records.",
    )
    parser.add_argument(
        "--ignore-case",
        action="store_true",
        help="Compile the regex with re.IGNORECASE.",
    )
    parser.add_argument(
        "--multiline",
        action="store_true",
        help="Compile the regex with re.MULTILINE.",
    )
    parser.add_argument(
        "--dotall",
        action="store_true",
        help="Compile the regex with re.DOTALL.",
    )
    parser.add_argument(
        "--max-display-len",
        type=int,
        default=500,
        help="Maximum characters of each sample to process and display.",
    )
    parser.add_argument(
        "--warning-ms",
        type=float,
        default=25.0,
        help="Warn if a single sample takes longer than this many milliseconds.",
    )
    return parser.parse_args(argv)


def build_flags(args: argparse.Namespace) -> int:
    flags = 0
    if args.ignore_case:
        flags |= re.IGNORECASE
    if args.multiline:
        flags |= re.MULTILINE
    if args.dotall:
        flags |= re.DOTALL
    return flags


def main(argv: Optional[Sequence[str]] = None) -> int:
    args = parse_args(argv)

    if args.max_display_len <= 0:
        raise SystemExit("--max-display-len must be greater than 0.")
    if args.warning_ms < 0:
        raise SystemExit("--warning-ms must be 0 or greater.")

    pattern_text = load_pattern(args)
    samples = load_samples(args)
    pattern = compile_pattern(pattern_text, build_flags(args))

    print("Pattern compiled successfully.")
    print(f"Mode: {args.mode}")
    print(f"Named groups: {list(pattern.groupindex.keys()) if pattern.groupindex else []}")
    print()

    results = validate_samples(pattern, samples, args.mode, args.max_display_len)
    matches, non_matches, avg_ms = summarize(results)

    for result in results:
        status = "MATCH" if result.matched else "NO MATCH"
        warn = ""
        if result.elapsed_ms > args.warning_ms:
            warn = f" [slow: {result.elapsed_ms:.2f} ms]"
        print(f"Line {result.line_no}: {status}{warn}")
        print(f"  Input: {result.line[:args.max_display_len]}")
        if result.groups is not None:
            print(f"  Groups: {result.groups}")
        if result.note:
            print(f"  Note: {result.note}")
        print()

    print("Summary")
    print(f"  Total samples: {len(results)}")
    print(f"  Matches: {matches}")
    print(f"  Non-matches: {non_matches}")
    print(f"  Average time per sample: {avg_ms:.4f} ms")

    return 0


if __name__ == "__main__":
    sys.exit(main())