import hashlib
import re
from datetime import timedelta

# We try to import spaCy. If not available, we use a robust regex fallback.
try:
    import spacy
    nlp = spacy.load("en_core_web_sm")
except ImportError:
    nlp = None
    print("[Warning] spaCy or en_core_web_sm not installed. Fallback to regex-based NER sanitization.")

# Salt and config
from config import SALT, MIN_DATE_SHIFT, MAX_DATE_SHIFT

def get_deterministic_hash_int(source_id, salt=SALT):
    """
    Generates a secure, deterministic 64-bit positive integer from any source ID.
    Perfect for generating OMOP primary keys (e.g., person_id, visit_occurrence_id).
    """
    if source_id is None:
        return None
    
    # Secure hash
    hasher = hashlib.sha256(f"{source_id}{salt}".encode("utf-8"))
    hash_bytes = hasher.digest()
    
    # Extract first 8 bytes for 64-bit integer
    int_val = int.from_bytes(hash_bytes[:8], byteorder="big")
    
    # Postgres bigint max signed value is 9,223,372,036,854,775,807
    # Keep it positive and within range
    return int_val % 9223372036854775807

def get_patient_date_shift_days(patient_id, salt=SALT):
    """
    Computes a stable, deterministic number of days to shift all dates for a given patient.
    This guarantees that longitudinal intervals for a patient are perfectly preserved 
    across all tables (e.g. visits, labs, prescriptions).
    """
    if patient_id is None:
        return 0
    
    hash_int = get_deterministic_hash_int(patient_id, salt)
    range_size = MAX_DATE_SHIFT - MIN_DATE_SHIFT + 1
    
    # Deterministic shift in days
    shift_days = (hash_int % range_size) + MIN_DATE_SHIFT
    
    # To make it split between forward/backward shift, we can use the hash parity
    if hash_int % 2 == 0:
        return -shift_days
    else:
        return shift_days

def shift_date(date_val, shift_days):
    """
    Applies the date shift to a datetime object or string.
    """
    if date_val is None:
        return None
    
    # If string, parse it (supports common SQL formats)
    if isinstance(date_val, str):
        from dateutil import parser
        try:
            date_dt = parser.parse(date_val)
        except Exception:
            return date_val # Return unmodified if parsing fails
    else:
        date_dt = date_val
        
    shifted_dt = date_dt + timedelta(days=shift_days)
    
    # Return in same type as input
    if isinstance(date_val, str):
        return shifted_dt.strftime("%Y-%m-%d %H:%M:%S")
    return shifted_dt

def scrub_text(text, facility_name=None):
    """
    De-identifies unstructured clinical text (doctor/nurse notes) by redacting
    names, locations, contact info, emails, and dates using spaCy NER + regex.
    """
    if not text:
        return text
    
    # Pre-clean known facility names if provided
    if facility_name:
        text = re.sub(re.escape(facility_name), "[REDACTED_FACILITY]", text, flags=re.IGNORECASE)
    
    # 1. Regex replacements for standard identifiers
    # Email addresses
    text = re.sub(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", "[REDACTED_EMAIL]", text)
    
    # Phone numbers (various formats)
    text = re.sub(r"\+?[0-9\-\s\(\)]{8,20}", "[REDACTED_PHONE]", text)
    
    # System IDs, National IDs (e.g. NHIS in West Africa, format matches alphanumeric strings of length 8-15)
    # We redact standard ID patterns
    text = re.sub(r"\b[A-Z0-9]{8,15}\b", "[REDACTED_ID]", text)

    # 2. NLP Named Entity Recognition (NER) for Names and Locations
    if nlp is not None:
        doc = nlp(text)
        ents = sorted(doc.ents, key=lambda e: e.start_char, reverse=True)
        for ent in ents:
            if ent.label_ in ["PERSON", "ORG", "GPE", "LOC", "DATE"]:
                label_map = {
                    "PERSON": "[REDACTED_NAME]",
                    "ORG": "[REDACTED_ORGANIZATION]",
                    "GPE": "[REDACTED_LOCATION]",
                    "LOC": "[REDACTED_LOCATION]",
                    "DATE": "[REDACTED_DATE]"
                }
                label = label_map.get(ent.label_, "[REDACTED]")
                text = text[:ent.start_char] + label + text[ent.end_char:]
    else:
        # Fallback regex for common clinician notes names patterns (e.g., Dr. Smith, Nurse Jones, Mr. Koffi)
        text = re.sub(r"\b(Dr\.|Nurse|Mr\.|Mrs\.|Ms\.)\s+[A-Z][a-zA-Z]+", r"\1 [REDACTED_NAME]", text)
        
    return text
