# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
PHI Detection Middleware

Scan text for Protected Health Information (PHI) per HIPAA Safe Harbor
de-identification standard (18 identifier categories).

Auto-detect, redact, and classify PHI before data leaves the system.
All pattern matching is local — no data sent to external services.

Dependencies: None (regex-based, pure stdlib)
"""

import json
import logging
import re
from datetime import datetime
from pathlib import Path


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.utils import atomic_write_json, safe_load_json
except ImportError:
    atomic_write_json = None
    safe_load_json = None

logger = logging.getLogger(__name__)


def _get_data_file():
    """Get data file path (re-evaluates for tenant scoping)."""
    return _get_data_dir() / "phi_config.json"


DATA_FILE = _get_data_file()  # Default for backward compat

_DEFAULT_CONFIG = {
    "safe_mode": False,  # When True, block all outbound data containing PHI
    "auto_redact": False,  # When True, auto-redact PHI in exports
    "log_detections": True,  # Log PHI detections to audit trail
    "sensitivity": "standard",  # standard, strict, minimal
    "whitelist": [],  # Terms that should not be flagged (e.g. org name)
    "version": 1,
}


def _load_config():
    if safe_load_json:
        return safe_load_json(
            _get_data_file(), default=lambda: json.loads(json.dumps(_DEFAULT_CONFIG))
        )
    if _get_data_file().exists():
        try:
            with open(_get_data_file(), encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError) as e:
            logger.debug("JSON parse error suppressed: %s", e)
    return json.loads(json.dumps(_DEFAULT_CONFIG))


def _save_config(config):
    if atomic_write_json:
        atomic_write_json(_get_data_file(), config)
    else:
        _get_data_file().parent.mkdir(parents=True, exist_ok=True)
        with open(_get_data_file(), "w", encoding="utf-8") as f:
            json.dump(config, f, indent=2)


# === PHI Pattern Definitions (HIPAA Safe Harbor 18 identifiers) ===

# Compiled regex patterns for each PHI category
_PHI_PATTERNS = {
    "ssn": {
        "label": "Social Security Number",
        "hipaa_id": 7,
        "patterns": [
            re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
            re.compile(r"\b\d{9}\b(?=.*(?:ssn|social|security))", re.IGNORECASE),
        ],
        "severity": "critical",
    },
    "phone": {
        "label": "Phone Number",
        "hipaa_id": 4,
        "patterns": [
            re.compile(r"\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b"),
        ],
        "severity": "high",
    },
    "email": {
        "label": "Email Address",
        "hipaa_id": 6,
        "patterns": [
            re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"),
        ],
        "severity": "high",
    },
    "date_of_birth": {
        "label": "Date (birth/admission/discharge)",
        "hipaa_id": 3,
        "patterns": [
            re.compile(
                r"\b(?:DOB|date\s*of\s*birth|born|birthday)\s*[:\-]?\s*\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4}\b",
                re.IGNORECASE,
            ),
            re.compile(
                r"\b(?:DOB|date\s*of\s*birth)\s*[:\-]?\s*(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4}\b",
                re.IGNORECASE,
            ),
        ],
        "severity": "high",
    },
    "medical_record_number": {
        "label": "Medical Record Number",
        "hipaa_id": 8,
        "patterns": [
            re.compile(
                r"\b(?:MRN|medical\s*record|chart)\s*#?\s*[:\-]?\s*[A-Z0-9]{6,12}\b", re.IGNORECASE
            ),
        ],
        "severity": "critical",
    },
    "health_plan_id": {
        "label": "Health Plan Beneficiary Number",
        "hipaa_id": 9,
        "patterns": [
            re.compile(
                r"\b(?:health\s*plan|insurance|policy|member)\s*(?:id|number|#)\s*[:\-]?\s*[A-Z0-9]{6,15}\b",
                re.IGNORECASE,
            ),
        ],
        "severity": "critical",
    },
    "account_number": {
        "label": "Account Number",
        "hipaa_id": 10,
        "patterns": [
            re.compile(r"\b(?:account|acct)\s*#?\s*[:\-]?\s*\d{8,17}\b", re.IGNORECASE),
        ],
        "severity": "high",
    },
    "license_number": {
        "label": "Certificate/License Number",
        "hipaa_id": 11,
        "patterns": [
            re.compile(r"\b(?:license|DEA|NPI)\s*#?\s*[:\-]?\s*[A-Z0-9]{7,12}\b", re.IGNORECASE),
        ],
        "severity": "high",
    },
    "ip_address": {
        "label": "IP Address",
        "hipaa_id": 15,
        "patterns": [
            re.compile(
                r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b"
            ),
        ],
        "severity": "medium",
    },
    "url": {
        "label": "URL",
        "hipaa_id": 14,
        "patterns": [
            re.compile(r'https?://[^\s<>"]+(?:/[^\s<>"]*)?', re.IGNORECASE),
        ],
        "severity": "low",
    },
    "vin": {
        "label": "Vehicle Identifier (VIN)",
        "hipaa_id": 12,
        "patterns": [
            re.compile(r"\b(?:VIN)\s*[:\-]?\s*[A-HJ-NPR-Z0-9]{17}\b", re.IGNORECASE),
        ],
        "severity": "medium",
    },
    "geographic": {
        "label": "Geographic Data (ZIP code)",
        "hipaa_id": 2,
        "patterns": [
            re.compile(
                r"\b(?:zip|postal)\s*(?:code)?\s*[:\-]?\s*\d{5}(?:-\d{4})?\b", re.IGNORECASE
            ),
        ],
        "severity": "medium",
    },
}

# Strict mode adds name detection (high false-positive rate)
_STRICT_PATTERNS = {
    "person_name": {
        "label": "Person Name (potential)",
        "hipaa_id": 1,
        "patterns": [
            re.compile(
                r"\b(?:patient|client|member|resident)\s*(?:name)?\s*[:\-]?\s*[A-Z][a-z]+\s+[A-Z][a-z]+\b"
            ),
        ],
        "severity": "medium",
    },
}


def _get_active_patterns(sensitivity="standard"):
    """Get patterns based on sensitivity level."""
    patterns = dict(_PHI_PATTERNS)
    if sensitivity == "strict":
        patterns.update(_STRICT_PATTERNS)
    elif sensitivity == "minimal":
        # Only SSN, MRN, health plan ID
        patterns = {k: v for k, v in patterns.items() if v.get("severity") == "critical"}
    return patterns


def _scan_text(text, sensitivity="standard", whitelist=None):
    """Scan text for PHI, return list of detections."""
    whitelist = whitelist or []
    patterns = _get_active_patterns(sensitivity)
    detections = []

    for category, info in patterns.items():
        for pattern in info["patterns"]:
            for match in pattern.finditer(text):
                value = match.group()
                # Check whitelist
                if any(wl.lower() in value.lower() for wl in whitelist):
                    continue
                detections.append(
                    {
                        "category": category,
                        "label": info["label"],
                        "hipaa_id": info["hipaa_id"],
                        "severity": info["severity"],
                        "value": value,
                        "position": match.start(),
                        "length": len(value),
                    }
                )

    return detections


def _redact_text(text, detections):
    """Replace PHI matches with redaction markers."""
    # Sort by position descending to preserve indices
    sorted_dets = sorted(detections, key=lambda d: d["position"], reverse=True)
    redacted = text
    for det in sorted_dets:
        start = det["position"]
        end = start + det["length"]
        marker = f"[REDACTED-{det['label'].upper().replace(' ', '_')}]"
        redacted = redacted[:start] + marker + redacted[end:]
    return redacted


# === Tool Handlers ===


def scan_for_phi(data):
    """Scan text or file for PHI. Returns categories found and locations."""
    text = data.get("text", "").strip()
    filepath = data.get("filepath", "").strip()

    if filepath:
        path = Path(filepath).expanduser()
        if path.exists():
            try:
                text = path.read_text(encoding="utf-8", errors="replace")[:100000]
            except Exception as e:
                return f"Error reading file: {e}"
        else:
            return f"File not found: {filepath}"

    if not text:
        return "Please provide text or a filepath to scan."

    config = _load_config()
    detections = _scan_text(
        text, config.get("sensitivity", "standard"), config.get("whitelist", [])
    )

    if not detections:
        return "✅ No PHI detected in the provided content."

    # Aggregate by category
    by_cat = {}
    for d in detections:
        cat = d["category"]
        if cat not in by_cat:
            by_cat[cat] = {
                "label": d["label"],
                "severity": d["severity"],
                "hipaa_id": d["hipaa_id"],
                "count": 0,
                "samples": [],
            }
        by_cat[cat]["count"] += 1
        if len(by_cat[cat]["samples"]) < 3:
            # Partially redact the sample
            val = d["value"]
            if len(val) > 6:
                sample = val[:3] + "***" + val[-2:]
            else:
                sample = val[:2] + "***"
            by_cat[cat]["samples"].append(sample)

    lines = [
        f"⚠️ PHI Detected: {len(detections)} match(es) in {len(by_cat)} categor{'y' if len(by_cat) == 1 else 'ies'}:\n"
    ]
    for cat, info in sorted(by_cat.items(), key=lambda x: -x[1]["count"]):
        sev_emoji = (
            "🔴" if info["severity"] == "critical" else "🟠" if info["severity"] == "high" else "🟡"
        )
        lines.append(
            f"  {sev_emoji} {info['label']} (HIPAA #{info['hipaa_id']}): {info['count']} match(es)"
        )
        lines.append(f"     Samples: {', '.join(info['samples'])}")

    # Log detection to audit
    if config.get("log_detections"):
        try:
            from familiar.skills.audit.skill import log_event as audit_log

            audit_log(
                {
                    "action": "phi_detected",
                    "resource": filepath or "inline_text",
                    "detail": f"{len(detections)} PHI matches in {len(by_cat)} categories",
                    "severity": "warning",
                }
            )
        except Exception as e:
            logger.debug("Error suppressed: %s", e)
    return "\n".join(lines)


def redact_phi(data):
    """Redact PHI from text or file, replacing matches with [REDACTED] markers."""
    text = data.get("text", "").strip()
    filepath = data.get("filepath", "").strip()
    output_path = data.get("output_path", "").strip()

    if filepath:
        path = Path(filepath).expanduser()
        if path.exists():
            try:
                text = path.read_text(encoding="utf-8", errors="replace")
            except Exception as e:
                return f"Error reading file: {e}"
        else:
            return f"File not found: {filepath}"

    if not text:
        return "Please provide text or a filepath to redact."

    config = _load_config()
    detections = _scan_text(
        text, config.get("sensitivity", "standard"), config.get("whitelist", [])
    )

    if not detections:
        return "✅ No PHI found — no redaction needed."

    redacted = _redact_text(text, detections)

    if output_path:
        out = Path(output_path).expanduser()
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(redacted, encoding="utf-8")
        return f"✅ Redacted {len(detections)} PHI match(es). Saved: {out}"
    elif filepath:
        out = Path(filepath).expanduser()
        stem = out.stem
        redacted_path = out.parent / f"{stem}_redacted{out.suffix}"
        redacted_path.write_text(redacted, encoding="utf-8")
        return f"✅ Redacted {len(detections)} PHI match(es). Saved: {redacted_path}"

    # Return redacted text inline (truncated for display)
    preview = redacted[:2000] + "\n\n[truncated]" if len(redacted) > 2000 else redacted
    return f"✅ Redacted {len(detections)} PHI match(es):\n\n{preview}"


def phi_safe_mode(data):
    """Enable/disable PHI safe mode, configure sensitivity and whitelist."""
    action = data.get("action", "status").lower()
    config = _load_config()

    if action == "enable":
        config["safe_mode"] = True
        config["auto_redact"] = data.get("auto_redact", True)
        _save_config(config)
        return (
            "🔒 PHI Safe Mode ENABLED. Outbound data will be scanned and optionally auto-redacted."
        )

    if action == "disable":
        config["safe_mode"] = False
        _save_config(config)
        return "🔓 PHI Safe Mode disabled."

    if action == "set_sensitivity":
        level = data.get("sensitivity", "standard")
        if level not in ("minimal", "standard", "strict"):
            return "Sensitivity must be: minimal, standard, or strict"
        config["sensitivity"] = level
        _save_config(config)
        return f"✅ PHI sensitivity set to: {level}"

    if action == "add_whitelist":
        term = data.get("term", "").strip()
        if term and term not in config.get("whitelist", []):
            config.setdefault("whitelist", []).append(term)
            _save_config(config)
        return f"✅ Whitelist updated: {config.get('whitelist', [])}"

    if action == "remove_whitelist":
        term = data.get("term", "").strip()
        config["whitelist"] = [w for w in config.get("whitelist", []) if w != term]
        _save_config(config)
        return f"✅ Whitelist updated: {config.get('whitelist', [])}"

    # Status
    mode = "🔒 ENABLED" if config.get("safe_mode") else "🔓 DISABLED"
    lines = [
        f"PHI Safe Mode: {mode}",
        f"  Auto-redact: {'Yes' if config.get('auto_redact') else 'No'}",
        f"  Sensitivity: {config.get('sensitivity', 'standard')}",
        f"  Log detections: {'Yes' if config.get('log_detections') else 'No'}",
        f"  Whitelist: {config.get('whitelist', []) or '(empty)'}",
        "",
        f"  Categories scanned ({config.get('sensitivity', 'standard')} mode):",
    ]
    patterns = _get_active_patterns(config.get("sensitivity", "standard"))
    for cat, info in sorted(patterns.items()):
        lines.append(f"    #{info['hipaa_id']} {info['label']} [{info['severity']}]")
    return "\n".join(lines)


def classification_report(data):
    """Scan a data store or directory and classify all PHI found."""
    target = data.get("target", "").strip()
    if not target:
        target = str(Path.home() / ".familiar" / "data")

    target_path = Path(target).expanduser()
    if not target_path.exists():
        return f"Target not found: {target}"

    config = _load_config()
    sensitivity = config.get("sensitivity", "standard")
    whitelist = config.get("whitelist", [])

    results = {}
    total_detections = 0
    files_scanned = 0
    files_with_phi = 0

    if target_path.is_file():
        files = [target_path]
    else:
        files = list(target_path.rglob("*.json")) + list(target_path.rglob("*.jsonl"))
        files += list(target_path.rglob("*.csv")) + list(target_path.rglob("*.txt"))
        files += list(target_path.rglob("*.md"))

    for fpath in files[:100]:  # Cap at 100 files
        try:
            text = fpath.read_text(encoding="utf-8", errors="replace")[:50000]
        except Exception as e:
            logger.debug("Error suppressed: %s", e)
            continue
        files_scanned += 1
        detections = _scan_text(text, sensitivity, whitelist)
        if detections:
            files_with_phi += 1
            total_detections += len(detections)
            by_cat = {}
            for d in detections:
                by_cat[d["category"]] = by_cat.get(d["category"], 0) + 1
            results[str(fpath.relative_to(target_path) if target_path.is_dir() else fpath.name)] = {
                "detections": len(detections),
                "categories": by_cat,
                "max_severity": max(d["severity"] for d in detections),
            }

    if not results:
        return f"✅ No PHI detected across {files_scanned} files in {target}"

    lines = [
        "📋 PHI Classification Report",
        f"Target: {target}",
        f"Files scanned: {files_scanned}",
        f"Files with PHI: {files_with_phi}",
        f"Total detections: {total_detections}",
        f"Sensitivity: {sensitivity}",
        "",
        "FILES WITH PHI:",
    ]

    for fname, info in sorted(results.items(), key=lambda x: -x[1]["detections"]):
        sev = info["max_severity"]
        sev_emoji = "🔴" if sev == "critical" else "🟠" if sev == "high" else "🟡"
        cats = ", ".join(f"{k}({v})" for k, v in info["categories"].items())
        lines.append(f"  {sev_emoji} {fname}: {info['detections']} detections [{cats}]")

    # Save report
    report_dir = Path.home() / ".familiar" / "data" / "audit"
    report_dir.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    report_path = report_dir / f"phi_classification_{ts}.json"
    with open(report_path, "w", encoding="utf-8") as f:
        json.dump(
            {
                "target": target,
                "files_scanned": files_scanned,
                "files_with_phi": files_with_phi,
                "total_detections": total_detections,
                "results": results,
                "generated_at": datetime.now().isoformat(),
            },
            f,
            indent=2,
        )
    lines.append(f"\nReport saved: {report_path}")

    return "\n".join(lines)


# === Tool Definitions ===

TOOLS = [
    {
        "name": "scan_for_phi",
        "description": (
            "Scan text or a file for Protected Health Information (PHI) per HIPAA Safe Harbor's 18 identifier categories. "
            "All pattern matching runs locally with no data sent to external services. "
            "Returns detected categories, severity levels, HIPAA identifier numbers, and partially-redacted samples. "
            "Detections are optionally logged to the audit trail."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "Text to scan for PHI"},
                "filepath": {
                    "type": "string",
                    "description": "File path to scan (alternative to text)",
                },
            },
        },
        "handler": scan_for_phi,
        "category": "phi_detection",
    },
    {
        "name": "redact_phi",
        "description": (
            "Redact PHI from text or a file, replacing each match with a labeled [REDACTED-CATEGORY] marker. "
            "Supports inline text or file-based redaction with optional custom output path. "
            "When processing a file without an output path, creates a new file with '_redacted' suffix. "
            "Uses the same HIPAA Safe Harbor patterns and sensitivity level as scan_for_phi."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "Text to redact"},
                "filepath": {"type": "string", "description": "File to redact"},
                "output_path": {
                    "type": "string",
                    "description": "Output file path (default: {filename}_redacted)",
                },
            },
        },
        "handler": redact_phi,
        "category": "phi_detection",
    },
    {
        "name": "phi_safe_mode",
        "description": (
            "Enable or disable PHI safe mode, configure sensitivity level (minimal/standard/strict), and manage the whitelist. "
            "When safe mode is enabled, outbound data is scanned and optionally auto-redacted. "
            "Use 'status' to view current settings, 'add_whitelist' to suppress false positives for known terms. "
            "Sensitivity levels control which HIPAA identifier categories are scanned."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": [
                        "status",
                        "enable",
                        "disable",
                        "set_sensitivity",
                        "add_whitelist",
                        "remove_whitelist",
                    ],
                    "default": "status",
                    "description": "Action to perform on PHI safe mode settings",
                },
                "sensitivity": {
                    "type": "string",
                    "enum": ["minimal", "standard", "strict"],
                    "description": "Sensitivity level controlling which PHI categories are scanned",
                },
                "auto_redact": {
                    "type": "boolean",
                    "description": "Whether to automatically redact PHI in outbound data when safe mode is enabled",
                },
                "term": {"type": "string", "description": "Whitelist term to add/remove"},
            },
        },
        "handler": phi_safe_mode,
        "category": "phi_detection",
    },
    {
        "name": "classification_report",
        "description": (
            "Scan a data directory or single file and classify all PHI found across JSON, JSONL, CSV, TXT, and MD files. "
            "Generates a per-file severity report with detection counts and category breakdowns. "
            "Scans up to 100 files with 50KB per-file limit to keep processing time reasonable. "
            "Saves the full classification report as JSON in the audit directory."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "target": {
                    "type": "string",
                    "description": "Directory or file to scan (default: ~/.familiar/data/)",
                },
            },
        },
        "handler": classification_report,
        "category": "phi_detection",
    },
]
