from flask import Flask, render_template, request, redirect, url_for, Response, send_file, session, flash
import datetime, uuid, json, os
import csv
import io
import hashlib

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "txra-change-this-in-production-2025")

# ── Techxyra Auth ──────────────────────────────────────────────────────────────
# Default credentials (change via TECHXYRA_ADMIN_USER / TECHXYRA_ADMIN_PASS env vars)
ADMIN_USERNAME = os.environ.get("TECHXYRA_ADMIN_USER", "admin")
ADMIN_PASSWORD = os.environ.get("TECHXYRA_ADMIN_PASS", "techxyra2025")

def _hash(pw):
    return hashlib.sha256(pw.encode()).hexdigest()

# Stored as hashed; on first run we just compare plain if hash not set
ADMIN_PASSWORD_HASH = os.environ.get("TECHXYRA_ADMIN_HASH", _hash(ADMIN_PASSWORD))

def check_credentials(username, password):
    return (username == ADMIN_USERNAME and _hash(password) == ADMIN_PASSWORD_HASH)

def login_required(f):
    from functools import wraps
    @wraps(f)
    def decorated(*args, **kwargs):
        if not session.get("logged_in"):
            return redirect(url_for("login"))
        return f(*args, **kwargs)
    return decorated

@app.before_request
def require_login():
    allowed_endpoints = {"login", "logout", "static"}
    if request.endpoint not in allowed_endpoints and not session.get("logged_in"):
        return redirect(url_for("login"))
# ──────────────────────────────────────────────────────────────────────────────

# File paths
MACHINES_FILE = "machines.json"
USERS_FILE = "users.json"
REPAIRS_FILE = "repairs.json"
UPGRADES_FILE = "upgrades.json"
SERVERS_FILE = "servers.json"
ACTIVITY_LOG_FILE = "activity_log.json"
REALLOCATION_LOG_FILE = "reallocation_log.json"
PURCHASES_FILE = "purchases.json"
ASSETS_FILE = "reserve_assets.json"
COST_SETTINGS_FILE = "cost_settings.json"
BRANCHES_FILE = "branches.json"

# System recommendations (static content)
SYSTEM_RECOMMENDATIONS = [
    {
        "title": "HDD → SSD",
        "details": [
            "Faster boot and load times",
            "Smoother overall performance"
        ]
    },
    {
        "title": "RAM Upgrade (≥8GB)",
        "details": [
            "Windows 10 minimum requirement is 8GB",
            "More RAM = better multitasking"
        ]
    },
    {
        "title": "Cooling & Maintenance",
        "details": [
            "Fresh thermal paste keeps CPU/GPU cool",
            "Improved cooling prevents overheating & throttling",
            "Regular servicing extends hardware life"
        ]
    }
]

# Load data from JSON files
def load_data(file):
    if os.path.exists(file):
        with open(file, "r") as f:
            return json.load(f)
    return []

def save_data(file, data):
    with open(file, "w") as f:
        json.dump(data, f, indent=4)

machines = load_data(MACHINES_FILE)
users = load_data(USERS_FILE)
repairs = load_data(REPAIRS_FILE)
upgrades = load_data(UPGRADES_FILE)
servers = load_data(SERVERS_FILE)
activity_log = load_data(ACTIVITY_LOG_FILE)
reallocation_log = load_data(REALLOCATION_LOG_FILE)
purchases = load_data(PURCHASES_FILE)
reserve_assets = load_data(ASSETS_FILE)
cost_settings = load_data(COST_SETTINGS_FILE)
branches = load_data(BRANCHES_FILE)

# Initialize cost settings defaults if empty
if not isinstance(cost_settings, dict) or not cost_settings:
    cost_settings = {
        "service_cost": 2000,
        "mouse_cost": 500,
        "upgrade_default_cost": 0,
        "repair_default_cost": 0
    }
    save_data(COST_SETTINGS_FILE, cost_settings)

def normalize_branch_name(name):
    if not name:
        return ""
    return " ".join(str(name).strip().split())

LOCATION_ALIASES = {
    "hq": "HQ Nairobi",
    "head office": "HQ Nairobi",
    "head-office": "HQ Nairobi"
}

def normalize_location_name(name):
    normalized = normalize_branch_name(name)
    if not normalized:
        return ""
    alias = LOCATION_ALIASES.get(normalized.lower())
    return alias or normalized

def get_user_name_counts():
    counts = {}
    for u in users:
        name = u.get("user_name", "")
        if not name:
            continue
        counts[name] = counts.get(name, 0) + 1
    return counts

def normalize_branch_records(branch_data):
    normalized = []
    if not isinstance(branch_data, list):
        return normalized

    if branch_data and isinstance(branch_data[0], str):
        for name in branch_data:
            norm_name = normalize_location_name(name)
            if not norm_name:
                continue
            normalized.append({
                "branch_id": str(uuid.uuid4())[:8],
                "name": norm_name,
                "status": "Active",
                "created_at": datetime.datetime.now().strftime("%Y-%m-%d"),
                "retired_at": ""
            })
        return normalized

    for branch in branch_data:
        if not isinstance(branch, dict):
            continue
        name = normalize_location_name(branch.get("name", ""))
        if not name:
            continue
        normalized.append({
            "branch_id": branch.get("branch_id") or str(uuid.uuid4())[:8],
            "name": name,
            "status": branch.get("status", "Active"),
            "created_at": branch.get("created_at", ""),
            "retired_at": branch.get("retired_at", "")
        })
    return normalized

branches = normalize_branch_records(branches)

def merge_branch_records(records):
    merged = {}
    for branch in records:
        name = normalize_location_name(branch.get("name", ""))
        if not name:
            continue
        status = branch.get("status", "Active")
        existing = merged.get(name.lower())
        if not existing:
            merged[name.lower()] = dict(branch, name=name)
            continue
        if existing.get("status") != "Active" and status == "Active":
            existing["status"] = "Active"
            existing["retired_at"] = ""
        if not existing.get("created_at") and branch.get("created_at"):
            existing["created_at"] = branch.get("created_at")
    return list(merged.values())

branches = merge_branch_records(branches)

def ensure_branch_exists(branch_name):
    name = normalize_branch_name(branch_name)
    if not name:
        return False
    if any(b.get("name", "").lower() == name.lower() for b in branches):
        return False
    branches.append({
        "branch_id": str(uuid.uuid4())[:8],
        "name": name,
        "status": "Active",
        "created_at": datetime.datetime.now().strftime("%Y-%m-%d"),
        "retired_at": ""
    })
    return True

if not branches:
    for default_branch in ["HQ Nairobi", "Nakuru", "Eldoret"]:
        ensure_branch_exists(default_branch)

RESERVE_TARGETS = ["Reserve", "Unassigned", ""]

def resolve_user_assignment(assigned_value, location_hint=""):
    if assigned_value in RESERVE_TARGETS:
        return "Reserve", ""

    user_by_code = next((u for u in users if u.get("user_code") == assigned_value), None)
    if user_by_code:
        return user_by_code.get("user_name", assigned_value), user_by_code.get("user_code", "")

    name_matches = [u for u in users if u.get("user_name") == assigned_value]
    if len(name_matches) == 1:
        user = name_matches[0]
        return user.get("user_name", assigned_value), user.get("user_code", "")
    if len(name_matches) > 1 and location_hint:
        user = next((u for u in name_matches if u.get("user_location") == location_hint), None)
        if user:
            return user.get("user_name", assigned_value), user.get("user_code", "")

    return assigned_value, ""

def normalize_locations_in_records():
    users_changed = False
    machines_changed = False
    repairs_changed = False
    servers_changed = False
    assets_changed = False
    reallocations_changed = False

    for u in users:
        current = u.get("user_location", "")
        normalized = normalize_location_name(current)
        if normalized and normalized != current:
            u["user_location"] = normalized
            users_changed = True

    for m in machines:
        current = m.get("machine_location", "")
        normalized = normalize_location_name(current)
        if normalized and normalized != current:
            m["machine_location"] = normalized
            machines_changed = True

    for r in repairs:
        current = r.get("machine_location", "")
        normalized = normalize_location_name(current)
        if normalized and normalized != current:
            r["machine_location"] = normalized
            repairs_changed = True

    for s in servers:
        current = s.get("location", "")
        normalized = normalize_location_name(current)
        if normalized and normalized != current:
            s["location"] = normalized
            servers_changed = True

    for a in reserve_assets:
        current = a.get("location", "")
        normalized = normalize_location_name(current)
        if normalized and normalized != current:
            a["location"] = normalized
            assets_changed = True

    for r in reallocation_log:
        current = r.get("location", "")
        normalized = normalize_location_name(current)
        if normalized and normalized != current:
            r["location"] = normalized
            reallocations_changed = True

    return {
        "users": users_changed,
        "machines": machines_changed,
        "repairs": repairs_changed,
        "servers": servers_changed,
        "assets": assets_changed,
        "reallocations": reallocations_changed
    }

def normalize_machine_assignments():
    changed = False
    for m in machines:
        assigned_value = m.get("assigned_to", "")
        assigned_code = m.get("assigned_to_code", "")
        if assigned_value in RESERVE_TARGETS:
            if assigned_code:
                m["assigned_to_code"] = ""
                changed = True
            continue

        if assigned_code and any(u.get("user_code") == assigned_code for u in users):
            continue

        assigned_name, resolved_code = resolve_user_assignment(
            assigned_value,
            m.get("machine_location", "")
        )
        if resolved_code:
            m["assigned_to"] = assigned_name
            m["assigned_to_code"] = resolved_code
            changed = True
    return changed

location_changes = normalize_locations_in_records()
assignments_changed = normalize_machine_assignments()

for location in set(
    [m.get("machine_location", "") for m in machines]
    + [u.get("user_location", "") for u in users]
    + [s.get("location", "") for s in servers]
):
    ensure_branch_exists(location)

branches = merge_branch_records(branches)

if location_changes.get("users"):
    save_data(USERS_FILE, users)
if location_changes.get("machines"):
    save_data(MACHINES_FILE, machines)
elif assignments_changed:
    save_data(MACHINES_FILE, machines)
if location_changes.get("repairs"):
    save_data(REPAIRS_FILE, repairs)
if location_changes.get("servers"):
    save_data(SERVERS_FILE, servers)
if location_changes.get("assets"):
    save_data(ASSETS_FILE, reserve_assets)
if location_changes.get("reallocations"):
    save_data(REALLOCATION_LOG_FILE, reallocation_log)

save_data(BRANCHES_FILE, branches)

def get_branch_lists():
    active = sorted(
        [b for b in branches if b.get("status", "Active") == "Active"],
        key=lambda b: b.get("name", "")
    )
    all_branches = sorted(branches, key=lambda b: b.get("name", ""))
    return active, all_branches

# Migration: Ensure all users have user_level field
for user in users:
    if "user_level" not in user:
        user["user_level"] = user.get("department", "")
save_data(USERS_FILE, users)

# Validation constants
ALLOWED_PROCESSOR_BRANDS = ["Intel", "AMD", "Apple"]
PROCESSOR_MODELS = {
    "Intel": ["Celeron", "Pentium", "Core 2 Duo", "Core i3", "Core i5", "Core i7", "Core i9", "Xeon"],
    "AMD": ["Athlon", "Ryzen 3", "Ryzen 5", "Ryzen 7", "Ryzen 9", "Threadripper"],
    "Apple": ["M1", "M2", "M3", "M3 Pro", "M3 Max", "M3 Ultra"]
}

def parse_processor_fields(processor_value):
    raw = (processor_value or "").strip()
    if not raw:
        return "", ""

    for brand in ALLOWED_PROCESSOR_BRANDS:
        if raw.lower().startswith(brand.lower() + " "):
            model = raw[len(brand):].strip()
            if model:
                return brand, model

    if raw.startswith("Core") or raw in ["Celeron", "Pentium", "Core 2 Duo", "Xeon"]:
        return "Intel", raw
    if raw.startswith("Ryzen") or raw in ["Athlon", "Threadripper"]:
        return "AMD", raw
    if raw.startswith("M"):
        return "Apple", raw

    return "Intel", raw
ALLOWED_GENERATIONS = ["2nd Gen", "3rd Gen"] + [f"{i}th Gen" for i in range(4, 16)] + ["Apple Silicon"]
ALLOWED_RAM = ["0GB", "1GB", "2GB", "3GB", "4GB", "6GB", "8GB", "16GB", "32GB", "64GB", "128GB"]
ALLOWED_STORAGE = ["HDD", "2.5 SSD", "NVMe", "No Drive"]
ALLOWED_CPU_TEMP = ["30-50 Normal", "50-70 Alarming", "70-90 Urgent"]
ALLOWED_BACKUP_STATUS = ["Yes", "No"]
ALLOWED_UPS_STATUS = ["Yes", "No"]
ALLOWED_STATUS = ["Active", "Retired"]
ALLOWED_DEVICE_TYPES = ["Desktop", "Laptop"]
ALLOWED_MONITOR_SIZES = [
    "No Monitor",
    "17 Square",
    "19 Wide",
    "20 Wide",
    "22 Wide",
    "24 Wide",
    "24 Edge to Edge",
    "27 Wide",
]
ALLOWED_SCREEN_SIZES = ["17", "15.6", "14", "13.3"]
ALLOWED_BATTERY_HEALTH = ["Excellent", "Good", "Fair", "Poor"]

# Migration: Ensure all machines have required fields
for machine in machines:
    if "status" not in machine:
        machine["status"] = "Active"
    # Only default device_type if missing/empty
    if "device_type" not in machine or not machine.get("device_type"):
        machine["device_type"] = "Desktop"
    if not machine.get("processor_brand") or not machine.get("processor_model"):
        brand, model = parse_processor_fields(machine.get("processor", ""))
        if brand and model:
            machine["processor_brand"] = brand
            machine["processor_model"] = model
save_data(MACHINES_FILE, machines)

# Generate unique codes
def generate_machine_code():
    return "MHA-" + str(uuid.uuid4())[:8]

def generate_user_code():
    return "USR-" + str(uuid.uuid4())[:6]

def generate_server_code():
    return "SRV-" + str(uuid.uuid4())[:8]

def log_activity(action_type, entity_type, entity_id, details="", user="System", state_data=None):
    """Log all system activities - append-only, cannot be edited"""
    global activity_log
    log_entry = {
        "log_id": str(uuid.uuid4())[:12],
        "timestamp": datetime.datetime.now().isoformat(),
        "action": action_type,  # ADD, EDIT, DELETE, VIEW, LOGIN, LOGOUT, RECALCULATE
        "entity_type": entity_type,  # MACHINE, USER, REPAIR, UPGRADE, SERVER
        "entity_id": entity_id,
        "details": details,
        "user": user,
        "state_data": state_data  # Store state snapshot for critical operations
    }
    activity_log.append(log_entry)
    save_data(ACTIVITY_LOG_FILE, activity_log)

def replay_to_date(target_date):
    """Replay activity log to reconstruct machine state on a specific date"""
    target_datetime = datetime.datetime.strptime(target_date, "%Y-%m-%d")
    target_datetime = target_datetime.replace(hour=23, minute=59, second=59)
    
    # Start with empty state
    historical_machines = {}  # Use dict for easier lookup
    historical_repairs = []
    historical_upgrades = []
    
    # Replay all logged actions up to target date
    for log in activity_log:
        try:
            log_time = datetime.datetime.fromisoformat(log.get("timestamp", ""))
        except:
            continue
            
        if log_time > target_datetime:
            break
        
        action = log.get("action")
        entity_type = log.get("entity_type")
        entity_id = log.get("entity_id")
        state_data = log.get("state_data")
        
        if entity_type == "MACHINE":
            if action == "ADD" and state_data:
                # Add machine with captured state
                historical_machines[entity_id] = dict(state_data)
            elif action == "EDIT" and state_data:
                # Update machine with captured state
                historical_machines[entity_id] = dict(state_data)
            elif action == "DELETE":
                # Remove from historical state
                if entity_id in historical_machines:
                    del historical_machines[entity_id]
        
        elif entity_type == "REPAIR":
            if action == "ADD" and state_data:
                historical_repairs.append(dict(state_data))
                # Find repair date and affected machine
                machine_code = state_data.get("machine_code")
                if machine_code in historical_machines:
                    repair_date = state_data.get("repair_date")
                    if state_data.get("includes_service") == "yes":
                        historical_machines[machine_code]["last_service_repaste"] = repair_date
        
        elif entity_type == "UPGRADE":
            if action in ["ADD", "ADD_BATCH", "ADD_BULK"] and state_data:
                historical_upgrades.append(dict(state_data))
                # Apply upgrade to machine
                machine_code = state_data.get("machine_code")
                if machine_code in historical_machines:
                    if state_data.get("ram_upgraded") == "Yes":
                        historical_machines[machine_code]["ram"] = state_data.get("new_ram")
                    if state_data.get("storage_upgraded") == "Yes":
                        historical_machines[machine_code]["storage_type"] = state_data.get("new_storage")
                    if state_data.get("includes_service") == "yes":
                        historical_machines[machine_code]["last_service_repaste"] = state_data.get("upgrade_date")
    
    # Convert dict back to list
    historical_machines_list = list(historical_machines.values())
    
    # Recalculate health scores and categorization for historical state
    for m in historical_machines_list:
        calculate_health_score(m)
        categorize_machine(m, historical_repairs)
        auto_recommend(m)
    
    return historical_machines_list, historical_repairs, historical_upgrades

def get_available_dates():
    """Get list of dates that have logged activities"""
    dates = set()
    for log in activity_log:
        timestamp = log.get("timestamp", "")
        if timestamp:
            try:
                date_only = timestamp.split("T")[0]
                dates.add(date_only)
            except:
                pass
    return sorted(dates, reverse=True)

def calculate_comparison(current_machines, historical_machines):
    """Compare current vs historical machine health scores"""
    current_scores = {"0-25": 0, "26-50": 0, "51-75": 0, "76-100": 0, "total": 0}
    historical_scores = {"0-25": 0, "26-50": 0, "51-75": 0, "76-100": 0, "total": 0}
    
    for m in current_machines:
        score = m.get("health_score", 0)
        current_scores["total"] += 1
        if score <= 25:
            current_scores["0-25"] += 1
        elif score <= 50:
            current_scores["26-50"] += 1
        elif score <= 75:
            current_scores["51-75"] += 1
        else:
            current_scores["76-100"] += 1
    
    for m in historical_machines:
        score = m.get("health_score", 0)
        historical_scores["total"] += 1
        if score <= 25:
            historical_scores["0-25"] += 1
        elif score <= 50:
            historical_scores["26-50"] += 1
        elif score <= 75:
            historical_scores["51-75"] += 1
        else:
            historical_scores["76-100"] += 1
    
    changes = {}
    for key in ["0-25", "26-50", "51-75", "76-100"]:
        changes[key] = current_scores[key] - historical_scores[key]
    
    return {
        "current": current_scores,
        "historical": historical_scores,
        "changes": changes
    }

def extract_generation_number(generation_str):
    """Extract the numeric generation from ordinal string (e.g., '1st', '2nd', '3rd', '4th')"""
    import re
    match = re.search(r'(\d+)', generation_str)
    return int(match.group(1)) if match else 0

def is_apple_machine(machine):
    brand = (machine.get("processor_brand") or "").strip()
    generation = (machine.get("generation") or "").strip()
    processor = (machine.get("processor") or "").strip()
    return (
        brand == "Apple"
        or generation == "Apple Silicon"
        or processor.lower().startswith("apple")
    )

def parse_money(value, default=0.0):
    try:
        if value is None:
            return float(default)
        cleaned = str(value).replace(",", "").strip()
        if cleaned == "":
            return float(default)
        return float(cleaned)
    except:
        return float(default)

def calculate_health_score(machine):
    """
    Calculate comprehensive health score (0-100 points)
    Health = Current condition of the machine (maintenance, backup, specs)
    
    Service/Maintenance (35 pts): Serviced in last 4 months=35, else=0
    RAM (30 pts): 4GB=10, 8GB=25, 16GB+=30
    Storage (20 pts): HDD=5, SSD=15, NVMe=20
    Backup (15 pts): Yes=15, No=0
    
    Target: Serviced + 8GB RAM + Backup = 35 + 25 + 15 = 75+ points (Healthy threshold at 70+)
    """
    score = 0
    
    # Service/Repaste score (35 points) - MOST IMPORTANT
    # Check if last_service_repaste is within last 4 months (120 days)
    service_score = 0
    last_service = machine.get("last_service_repaste")
    if last_service:
        try:
            service_date = datetime.datetime.strptime(last_service, "%Y-%m-%d")
            days_since_service = (datetime.datetime.now() - service_date).days
            if days_since_service <= 120:  # Within 4 months
                service_score = 35
        except:
            service_score = 0
    score += service_score
    
    # RAM score (30 points)
    try:
        ram = int(machine.get("ram", "4GB").replace("GB", "").split()[0])
        if ram >= 16:
            ram_score = 30
        elif ram >= 8:
            ram_score = 25
        else:
            ram_score = 10
    except:
        ram_score = 10
    score += ram_score
    
    # Storage score (20 points)
    storage = machine.get("storage_type", "HDD")
    if storage in ["No Drive", "", None]:
        storage_score = 0
    elif storage == "NVMe":
        storage_score = 20
    elif storage in ["2.5 SSD", "SSD"]:
        storage_score = 15
    else:
        storage_score = 5
    score += storage_score
    
    # Backup score (15 points)
    backup_status = machine.get("backup_status", "No")
    backup_score = 15 if backup_status == "Yes" else 0
    score += backup_score
    
    machine["health_score"] = score
    return machine

def has_machine_been_touched(machine_code):
    """Check if machine has been serviced or upgraded by technicians"""
    # Check repairs
    machine_has_repairs = any(r["machine_code"] == machine_code for r in repairs)
    # Check upgrades
    machine_has_upgrades = any(u["machine_code"] == machine_code for u in upgrades)
    return machine_has_repairs or machine_has_upgrades

def categorize_machine(machine, repairs):
    # Calculate health score first
    machine = calculate_health_score(machine)
    score = machine.get("health_score", 0)
    
    # Determine health status based on score
    if score >= 70:
        health = "Healthy"
    elif score >= 40:
        health = "Alarming"
    else:
        health = "Critical"
    
    gen = extract_generation_number(machine.get("generation", ""))
    try:
        ram = int(machine["ram"].replace("GB","").split()[0])
    except:
        ram = 0
    storage = machine["storage_type"]

    if is_apple_machine(machine):
        zone_lifecycle = "Apple Silicon"
        lifecycle = "Apple Silicon"
    else:
        # Zone-based categorization (detailed generations)
        if gen <= 5:
            zone_lifecycle = "Legacy (1st-5th Gen)"  # Older models
        elif 6 <= gen <= 10:
            zone_lifecycle = "Mid-Range (6th-10th Gen)"  # Mid-range
        elif 11 <= gen <= 13:
            zone_lifecycle = "Modern (11th-13th Gen)"  # Modern with hybrid cores
        else:
            zone_lifecycle = "Latest (14th-15th Gen)"  # Latest, AI-ready

        if gen <= 2:
            lifecycle = "Legacy"
        elif gen >= 8 and ram >= 8 and storage in ["2.5 SSD", "NVMe"]:
            lifecycle = "Active"
        elif 5 <= gen <= 7:
            lifecycle = "Aging"
        else:
            lifecycle = "Outphased"

    backup_status = machine["backup_status"]
    backup = "Safe" if backup_status == "Yes" else "Single Point Failure"

    count = sum(1 for r in repairs if r["machine_code"] == machine["machine_code"])
    if count <= 1:
        repair_risk = "Low Risk"
    elif count <= 3:
        repair_risk = "Medium Risk"
    else:
        repair_risk = "High Risk"

    machine["lifecycle"] = lifecycle
    machine["zone_lifecycle"] = zone_lifecycle
    machine["health"] = health
    machine["backup_category"] = backup
    machine["repair_risk"] = repair_risk
    return machine

def auto_recommend(machine):
    """Auto-generate upgrade and service recommendations separately"""
    gen = extract_generation_number(machine.get("generation", ""))
    
    # SEPARATE: Upgrade recommendations
    upgrade_recommendations = []
    upgrade_recommendations_detailed = []
    
    # SEPARATE: Service recommendations
    service_recommendations = []
    service_recommendations_detailed = []
    
    replacement = "No"

    # ===== SERVICE RECOMMENDATIONS =====
    # Check if serviced within last 4 months (120 days)
    needs_service = False
    last_service = machine.get("last_service_repaste")
    if last_service:
        try:
            service_date = datetime.datetime.strptime(last_service, "%Y-%m-%d")
            days_since_service = (datetime.datetime.now() - service_date).days
            if days_since_service > 120:  # More than 4 months
                needs_service = True
        except:
            needs_service = True
    else:
        needs_service = True  # No service record
    
    # Also check CPU temperature - if urgent, needs service regardless
    cpu_temp = machine.get("cpu_temp", "")
    if "Urgent" in cpu_temp or "70-90" in cpu_temp:
        needs_service = True
    
    if needs_service:
        service_recommendations.append("🔧 Service & Thermal Paste")
        service_recommendations_detailed.append({
            "title": "🔧 Service & Thermal Paste",
            "details": [
                "Fresh thermal paste keeps CPU/GPU cool",
                "Improved cooling prevents overheating & throttling",
                "Regular servicing extends hardware life"
            ]
        })

    # Laptop-specific service recommendations
    if machine.get("device_type") == "Laptop":
        battery_health = machine.get("battery_health", "")
        if battery_health in ["Fair", "Poor"]:
            service_recommendations.append("🔋 Battery Replacement")
            service_recommendations_detailed.append({
                "title": "🔋 Battery Replacement",
                "details": [
                    "Battery health is below optimal",
                    "Replace to improve uptime and reliability"
                ]
            })

    # ===== HARDWARE UPGRADE RECOMMENDATIONS =====
    # HDD to SSD upgrade check
    if machine.get("storage_type") == "HDD":
        upgrade_recommendations.append("💾 HDD → SSD Upgrade")
        upgrade_recommendations_detailed.append({
            "title": "💾 HDD → SSD",
            "details": [
                "Faster boot and load times",
                "Smoother overall performance"
            ]
        })

    # RAM upgrade check (all device types)
    try:
        ram = int(machine.get("ram", "0GB").replace("GB", "").split()[0])
        if ram < 8:
            upgrade_recommendations.append("🧠 RAM Upgrade (≥8GB)")
            upgrade_recommendations_detailed.append({
                "title": "🧠 RAM Upgrade (≥8GB)",
                "details": [
                    "Windows 10 minimum requirement is 8GB",
                    "More RAM = better multitasking"
                ]
            })
    except:
        pass

    # Store both separately
    machine["recommended_upgrades"] = " | ".join(upgrade_recommendations) if upgrade_recommendations else "✅ No upgrades needed"
    machine["recommended_service"] = " | ".join(service_recommendations) if service_recommendations else "✅ Recently serviced"
    machine["recommendations_detailed"] = upgrade_recommendations_detailed
    machine["service_recommendations_detailed"] = service_recommendations_detailed

    # Check for replacement
    if not is_apple_machine(machine):
        if gen <= 4 and machine.get("health") in ["Alarming", "Critical"]:
            replacement = "Yes - Consider replacement"
        elif machine.get("repair_risk") == "High Risk":
            replacement = "Yes - High repair cost, consider replacement"
    elif machine.get("repair_risk") == "High Risk":
        replacement = "Yes - High repair cost, consider replacement"

    machine["replacement"] = replacement
    return machine

def calculate_server_score(server):
    """
    Calculate comprehensive server protection score (0-100 points)
    RAID Redundancy (30 pts): Yes=30, No=0
    Onsite Server (20 pts): Yes=20, No=0
    Offsite Backup (25 pts): Yes=25, No=0
    Backup Frequency (15 pts): Daily=15, Weekly=10, Fortnightly=7, Monthly=3, None=0
    Client PC Integration (10 pts): All=10, Partial=5, None=0
    """
    score = 0
    recommendations = []
    warnings = []
    
    # RAID Redundancy (30 points)
    if server.get("raid_redundancy") == "Yes":
        score += 30
    else:
        recommendations.append({
            "icon": "💿",
            "title": "Implement RAID Redundancy",
            "priority": "CRITICAL",
            "details": [
                "Data written simultaneously to two drives",
                "If one drive fails, operations continue without downtime",
                "Prevents catastrophic data loss from hardware failure"
            ]
        })
        warnings.append("CRITICAL: No drive redundancy - single drive failure will cause complete data loss")
    
    # Onsite Server (20 points)
    if server.get("onsite_server") == "Yes":
        score += 20
    else:
        recommendations.append({
            "icon": "🖥️",
            "title": "Deploy Onsite Server",
            "priority": "HIGH",
            "details": [
                "Centralized storage for company data and applications",
                "Host critical software (QuickBooks, payroll systems)",
                "Faster access speeds compared to cloud-only solutions",
                "Better control over sensitive client data"
            ]
        })
        warnings.append("HIGH: No central server - data scattered across individual PCs")
    
    # Offsite Backup (25 points)
    if server.get("offsite_backup") == "Yes":
        score += 25
    else:
        recommendations.append({
            "icon": "☁️",
            "title": "Setup Offsite Backup System",
            "priority": "CRITICAL",
            "details": [
                "Protects against fire, theft, or physical destruction",
                "Ensures business continuity in disaster scenarios",
                "Compliance requirement for law firms and professional services",
                "Should mirror onsite data automatically"
            ]
        })
        warnings.append("CRITICAL: No offsite backup - fire/theft would destroy ALL data permanently")
    
    # Backup Frequency (15 points)
    backup_freq = server.get("backup_frequency", "None")
    freq_scores = {"Daily": 15, "Weekly": 10, "Fortnightly": 7, "Monthly": 3, "None": 0}
    score += freq_scores.get(backup_freq, 0)
    if backup_freq in ["None", "Monthly"]:
        recommendations.append({
            "icon": "⏰",
            "title": "Increase Backup Frequency",
            "priority": "HIGH",
            "details": [
                "Daily backups recommended for active law firm operations",
                "Minimize potential data loss window",
                "Automated overnight backups ensure no manual oversight needed"
            ]
        })
        if backup_freq == "None":
            warnings.append("CRITICAL: No backup schedule - losing months/years of work in one incident")
    elif backup_freq in ["Fortnightly", "Monthly"]:
        warnings.append("MEDIUM: Backup frequency too low - up to 2-4 weeks of data could be lost")
    
    # Client PC Integration (10 points)
    pc_integration = server.get("client_pc_integration", "None")
    integration_scores = {"All PCs": 10, "Partial": 5, "None": 0}
    score += integration_scores.get(pc_integration, 0)
    if pc_integration != "All PCs":
        recommendations.append({
            "icon": "🔗",
            "title": "Integrate All Client PCs with Server",
            "priority": "HIGH",
            "details": [
                "Eliminate single points of failure from individual PCs",
                "Ensure all staff data backed up centrally",
                "Enable file sharing and collaboration",
                "Simplify backup management"
            ]
        })
        if pc_integration == "None":
            warnings.append("HIGH: All client data at risk - no PCs connected to backup infrastructure")
        else:
            warnings.append("MEDIUM: Some PCs not backed up - partial single point of failure risk")
    
    # Security measures bonus (additional assessment, not scored but generates recommendations)
    if server.get("encryption_enabled") != "Yes":
        recommendations.append({
            "icon": "🔒",
            "title": "Enable Data Encryption",
            "priority": "HIGH",
            "details": [
                "Protect against data breaches if drives are stolen",
                "Compliance requirement for sensitive client information",
                "Encrypt both onsite and offsite storage"
            ]
        })
        warnings.append("HIGH: Unencrypted data vulnerable to theft")
    
    if server.get("access_control") != "Strong":
        recommendations.append({
            "icon": "🔐",
            "title": "Implement Strong Access Controls",
            "priority": "MEDIUM",
            "details": [
                "Multi-factor authentication for server access",
                "Role-based permissions for different staff levels",
                "Regular password policy enforcement",
                "Protects against unauthorized access and insider threats"
            ]
        })
    
    # Determine risk level based on score
    if score >= 80:
        risk_level = "LOW RISK"
        risk_color = "green"
        risk_message = "Excellent data protection posture. Continue monitoring and maintain current standards."
    elif score >= 60:
        risk_level = "MEDIUM RISK"
        risk_color = "orange"
        risk_message = "Adequate protection but vulnerabilities exist. Address high-priority recommendations promptly."
    elif score >= 40:
        risk_level = "HIGH RISK"
        risk_color = "red"
        risk_message = "Significant data loss risks present. Immediate action required on critical items."
    else:
        risk_level = "CRITICAL RISK"
        risk_color = "darkred"
        risk_message = "Severe exposure to data loss. Business continuity threatened. Emergency implementation needed."
    
    server["protection_score"] = score
    server["risk_level"] = risk_level
    server["risk_color"] = risk_color
    server["risk_message"] = risk_message
    server["recommendations"] = recommendations
    server["warnings"] = warnings
    
    return server

# Refresh recommendations on startup
if machines:
    for m in machines:
        if "device_type" not in m:
            m["device_type"] = "Laptop"
        categorize_machine(m, repairs)
        auto_recommend(m)
    save_data(MACHINES_FILE, machines)

@app.route("/recalculate_scores")
def recalculate_scores():
    """Recalculate health scores, lifecycle, and recommendations for all machines"""
    global machines
    for m in machines:
        calculate_health_score(m)
        categorize_machine(m, repairs)
        auto_recommend(m)  # Refresh recommendations
    save_data(MACHINES_FILE, machines)
    log_activity("RECALCULATE", "SYSTEM", "ALL_MACHINES", f"Recalculated health scores for {len(machines)} machines")
    return redirect(url_for("index"))
    return redirect(url_for("index"))

def get_oldest_active_generation(machines_list):
    """Get the actual generation string of the oldest (lowest number) active machine in the institution"""
    active_machines = [m for m in machines_list if m.get("status") == "Active"]
    gen_numbers = {}  # Map: generation_number -> generation_string
    
    for m in active_machines:
        if is_apple_machine(m):
            continue
        try:
            gen_str = m.get("generation", "")
            gen_num = extract_generation_number(gen_str)
            gen_numbers[gen_num] = gen_str  # Store mapping of number to string
        except:
            pass
    
    if gen_numbers:
        oldest_num = min(gen_numbers.keys())
        return gen_numbers[oldest_num]  # Return the actual generation string
    return None

# ── Login / Logout routes ──────────────────────────────────────────────────────
@app.route("/login", methods=["GET", "POST"])
def login():
    if session.get("logged_in"):
        return redirect(url_for("index"))
    if request.method == "POST":
        username = request.form.get("username", "").strip()
        password = request.form.get("password", "")
        if check_credentials(username, password):
            session["logged_in"] = True
            session["username"] = username
            return redirect(url_for("index"))
        flash("Invalid username or password. Please try again.", "error")
    return render_template("login.html")

@app.route("/logout")
def logout():
    session.clear()
    flash("You've been signed out successfully.", "success")
    return redirect(url_for("login"))
# ──────────────────────────────────────────────────────────────────────────────

@app.route("/")
@login_required
def index():
    sorted_users = sorted(users, key=lambda u: u["user_name"])
    
    # Check if there's a date filter - use time-travel to replay
    selected_date = request.args.get("view_date")
    machines_to_display = machines
    historical_info = None
    comparison_data = None
    
    if selected_date:
        # Replay activity log to reconstruct state on that date
        historical_machines, historical_repairs, historical_upgrades = replay_to_date(selected_date)
        if historical_machines:
            machines_to_display = historical_machines
            historical_info = {
                "date": selected_date,
                "machine_count": len(historical_machines),
                "repairs_count": len(historical_repairs),
                "upgrades_count": len(historical_upgrades)
            }
            # Calculate comparison with current state
            comparison_data = calculate_comparison(machines, machines_to_display)
    
    # Calculate generation counts
    gen_counts = {}
    for m in machines_to_display:
        gen = m.get("generation", "Unknown")
        gen_counts[gen] = gen_counts.get(gen, 0) + 1
    # Sort by generation number for better visualization
    gen_counts = dict(sorted(gen_counts.items(), key=lambda x: (x[0] == 'Unknown', x[0])))
    
    # Calculate zone counts (new detailed zones)
    legacy_zone = 0
    midrange_zone = 0
    modern_zone = 0
    latest_zone = 0
    
    for m in machines_to_display:
        if is_apple_machine(m):
            continue
        gen = extract_generation_number(m.get("generation", ""))
        if gen <= 5:
            legacy_zone += 1
        elif 6 <= gen <= 10:
            midrange_zone += 1
        elif 11 <= gen <= 13:
            modern_zone += 1
        else:
            latest_zone += 1
    
    zone_counts = {
        "legacy": legacy_zone,
        "midrange": midrange_zone,
        "modern": modern_zone,
        "latest": latest_zone
    }
    
    # Calculate health score distribution
    score_ranges = {
        "0-25": 0,
        "26-50": 0,
        "51-75": 0,
        "76-100": 0
    }
    
    for m in machines_to_display:
        score = m.get("health_score", 0)
        if score <= 25:
            score_ranges["0-25"] += 1
        elif score <= 50:
            score_ranges["26-50"] += 1
        elif score <= 75:
            score_ranges["51-75"] += 1
        else:
            score_ranges["76-100"] += 1
    
    # Get 5 critical machines with lowest health scores
    critical_machines = sorted(machines_to_display, key=lambda m: m.get("health_score", 0))[:5]
    
    # Get aging generation across institution (oldest generation among active machines)
    oldest_active_generation = get_oldest_active_generation(machines_to_display)
    
    # Get available dates from activity log
    available_dates = get_available_dates()

    active_branches, all_branches = get_branch_lists()
    
    return render_template(
        "index.html",
        machines=machines_to_display,
        repairs=repairs,
        users=sorted_users,
        gen_counts=gen_counts,
        oldest_active_generation=oldest_active_generation,
        zone_counts=zone_counts,
        score_ranges=score_ranges,
        critical_machines=critical_machines,
        available_dates=available_dates,
        selected_date=selected_date,
        historical_info=historical_info,
        comparison_data=comparison_data,
        branches=active_branches,
        branches_all=all_branches
    )

@app.route("/add_user", methods=["POST"])
def add_user():
    user = {
        "user_code": generate_user_code(),
        "user_name": request.form["user_name"],
        "user_level": request.form.get("user_level", ""),
        "user_location": request.form["user_location"],
        "status": "Active"
    }
    users.append(user)
    save_data(USERS_FILE, users)
    log_activity("ADD", "USER", user["user_code"], f"Added user: {user['user_name']} at {user['user_location']}")
    return redirect(url_for("users_page"))

def validate_machine_form(form):
    device_type = form.get("device_type", "").strip()
    if device_type not in ALLOWED_DEVICE_TYPES:
        return False, "Invalid device type."

    processor_brand = form.get("processor_brand", "").strip()
    processor_model = form.get("processor_model", "").strip()
    if processor_brand not in ALLOWED_PROCESSOR_BRANDS:
        return False, "Invalid processor brand selection."
    if processor_model not in PROCESSOR_MODELS.get(processor_brand, []):
        return False, "Invalid processor model selection."
    generation = (form.get("generation") or "").strip()
    if processor_brand == "Apple":
        if generation and generation != "Apple Silicon":
            return False, "Invalid generation selection for Apple."
    else:
        if generation not in ALLOWED_GENERATIONS or generation == "Apple Silicon":
            return False, "Invalid generation selection."
    if form.get("ram") not in ALLOWED_RAM:
        return False, "Invalid RAM selection."
    if form.get("storage_type") not in ALLOWED_STORAGE:
        return False, "Invalid storage selection."
    if form.get("cpu_temp") not in ALLOWED_CPU_TEMP:
        return False, "Invalid CPU temperature selection."
    if form.get("backup_status") not in ALLOWED_BACKUP_STATUS:
        return False, "Invalid backup status selection."

    ups_status = form.get("ups_status")
    if ups_status and ups_status not in ALLOWED_UPS_STATUS:
        return False, "Invalid UPS status selection."

    if form.get("status", "") not in ALLOWED_STATUS:
        return False, "Invalid machine status selection."

    if device_type == "Desktop":
        monitor_size = form.get("monitor_size", "").strip()
        if monitor_size not in ALLOWED_MONITOR_SIZES:
            return False, "Invalid monitor size selection."
    else:
        screen_size = form.get("screen_size", "").strip()
        battery_health = form.get("battery_health", "").strip()
        if screen_size not in ALLOWED_SCREEN_SIZES:
            return False, "Invalid screen size selection."
        if battery_health not in ALLOWED_BATTERY_HEALTH:
            return False, "Invalid battery health selection."

    return True, ""

def apply_machine_form(machine, form, assigned_to=None, is_new=False):
    peripherals_selected = form.getlist("peripherals")
    peripherals_str = ", ".join(peripherals_selected)

    processor_brand = form.get("processor_brand", "").strip()
    processor_model = form.get("processor_model", "").strip()
    processor_full = f"{processor_brand} {processor_model}".strip()
    generation = (form.get("generation") or "").strip()
    if processor_brand == "Apple" and not generation:
        generation = "Apple Silicon"

    machine["computer_name"] = form["computer_name"]
    machine["processor"] = processor_full
    machine["processor_brand"] = processor_brand
    machine["processor_model"] = processor_model
    machine["generation"] = generation
    machine["ram"] = form["ram"]
    machine["device_type"] = form.get("device_type", machine.get("device_type", "Laptop"))
    machine["storage_type"] = form["storage_type"]
    machine["cpu_temp"] = form["cpu_temp"]
    machine["peripherals"] = peripherals_str
    machine["monitor_size"] = form.get("monitor_size", "") if machine["device_type"] == "Desktop" else ""
    machine["screen_size"] = form.get("screen_size", "") if machine["device_type"] == "Laptop" else ""
    machine["battery_health"] = form.get("battery_health", "") if machine["device_type"] == "Laptop" else ""
    machine["backup_status"] = form["backup_status"]
    machine["ups_status"] = form.get("ups_status", "No")
    machine["machine_location"] = form.get("machine_location", "")
    assigned_value = assigned_to if assigned_to is not None else form["assigned_to"]
    assigned_name, assigned_code = resolve_user_assignment(assigned_value, form.get("machine_location", ""))
    machine["assigned_to"] = assigned_name
    machine["assigned_to_code"] = assigned_code
    machine["status"] = form.get("status", "Active")

    service_date = form.get("last_service_repaste")
    if service_date is not None:
        service_date = service_date.strip()
        machine["last_service_repaste"] = service_date
        machine["service_done"] = True if service_date else False

    last_checked = form.get("last_checked")
    if last_checked:
        machine["last_checked"] = last_checked
        machine["next_service"] = (
            datetime.datetime.strptime(last_checked, "%Y-%m-%d") + datetime.timedelta(days=120)
        ).strftime("%Y-%m-%d")

    if is_new:
        if "last_service_repaste" not in form:
            machine["last_service_repaste"] = ""
            machine["service_done"] = False
        machine["recommended_upgrades"] = ""
        machine["replacement"] = ""
    else:
        machine["recommended_upgrades"] = form.get("recommended_upgrades", machine.get("recommended_upgrades", ""))
        machine["replacement"] = form.get("replacement", machine.get("replacement", ""))

    return machine

@app.route("/add_machine", methods=["POST"])
def add_machine():
    assigned_to_value = request.form["assigned_to"]
    assigned_name, assigned_code = resolve_user_assignment(
        assigned_to_value,
        request.form.get("machine_location", "")
    )

    # Prevent multiple assignments (by user code)
    if assigned_name not in RESERVE_TARGETS:
        if not assigned_code:
            return "Error: User not found for assignment.", 400
        for m in machines:
            if m.get("assigned_to_code") == assigned_code:
                return "Error: This user already has a machine assigned.", 400

    is_valid, error_message = validate_machine_form(request.form)
    if not is_valid:
        return f"Error: {error_message}", 400
    if not request.form.get("last_checked"):
        return "Error: Last checked date is required.", 400

    machine_code = generate_machine_code()
    machine = {"machine_code": machine_code}
    machine = apply_machine_form(machine, request.form, assigned_to=assigned_to_value, is_new=True)
    machine = categorize_machine(machine, repairs)
    machine = auto_recommend(machine)
    machines.append(machine)
    save_data(MACHINES_FILE, machines)
    log_activity("ADD", "MACHINE", machine_code, f"Added machine: {machine['computer_name']} assigned to {assigned_name}", state_data=dict(machine))
    tab = "laptops" if machine.get("device_type") == "Laptop" else "desktops"
    return redirect(url_for("machines_page", tab=tab))

@app.route("/add_repair", methods=["POST"])
def add_repair():
    machine_code = request.form["machine_code"]
    repair_date = request.form["repair_date"]
    signature = request.form.get("signature", "").strip()
    if not signature:
        return "Error: Signature is required.", 400
    battery_replaced = request.form.get("battery_replaced") == "yes"
    repair_cost = request.form.get("repair_cost", "")
    
    repair = {
        "machine_code": machine_code,
        "computer_name": next((m["computer_name"] for m in machines if m["machine_code"] == machine_code), ""),
        "assigned_to": next((m["assigned_to"] for m in machines if m["machine_code"] == machine_code), ""),
        "machine_location": next((m["machine_location"] for m in machines if m["machine_code"] == machine_code), ""),
        "repair_date": repair_date,
        "issue": request.form["issue"],
        "action_taken": request.form["action_taken"],
        "technician": request.form["technician"],
        "signature": signature,
        "battery_replaced": "Yes" if battery_replaced else "No",
        "repair_cost": repair_cost
    }
    repairs.append(repair)
    save_data(REPAIRS_FILE, repairs)

    log_activity("ADD", "REPAIR", machine_code, f"Added repair for {repair['computer_name']}: {repair['issue']}", state_data=dict(repair))

    # Update machine with verification data
    for m in machines:
        if m["machine_code"] == machine_code:
            # If service & thermal repaste was included, update the machine's last_service_repaste
            if request.form.get("includes_service") == "yes":
                m["last_service_repaste"] = repair_date
                m["service_done"] = True

            # If battery replaced for laptops, update battery health
            if battery_replaced and m.get("device_type") == "Laptop":
                m["battery_health"] = "Excellent"
            
            # Update backup status based on verification
            backup_verified = request.form.get("backup_verified")
            if backup_verified:
                m["backup_status"] = backup_verified
                if backup_verified == "Yes" and not m.get("backup_type"):
                    m["backup_type"] = "Cloud Backup (OneDrive/Google Drive/etc.)"
            
            # CRITICAL: Recalculate health score, lifecycle, AND recommendations after repair
            calculate_health_score(m)
            categorize_machine(m, repairs)
            auto_recommend(m)  # Refresh recommendations
            break
    
    save_data(MACHINES_FILE, machines)
    return redirect(url_for("repairs_page"))

@app.route("/edit_user/<user_code>", methods=["GET", "POST"])
def edit_user(user_code):
    user = next((u for u in users if u["user_code"] == user_code), None)
    if not user:
        return "User not found", 404

    if request.method == "POST":
        user["user_name"] = request.form["user_name"]
        user["user_level"] = request.form.get("user_level", "")
        user["user_location"] = request.form["user_location"]
        user["status"] = request.form["status"]
        save_data(USERS_FILE, users)
        log_activity("EDIT", "USER", user_code, f"Edited user: {user['user_name']}")
        return redirect(url_for("users_page"))

    _, all_branches = get_branch_lists()
    return render_template("edit_user.html", user=user, branches=all_branches)

@app.route("/edit_machine/<machine_code>", methods=["GET", "POST"])
def edit_machine(machine_code):
    machine = next((m for m in machines if m["machine_code"] == machine_code), None)
    if not machine:
        return "Machine not found", 404

    if request.method == "POST":
        is_valid, error_message = validate_machine_form(request.form)
        if not is_valid:
            return f"Error: {error_message}", 400

        assigned_value = request.form["assigned_to"]
        assigned_name, assigned_code = resolve_user_assignment(
            assigned_value,
            request.form.get("machine_location", "")
        )
        if assigned_name not in RESERVE_TARGETS:
            if not assigned_code:
                return "Error: User not found for assignment.", 400
            for m in machines:
                if m.get("machine_code") != machine_code and m.get("assigned_to_code") == assigned_code:
                    return "Error: This user already has a machine assigned.", 400

        machine = apply_machine_form(machine, request.form, assigned_to=assigned_value, is_new=False)

        # CRITICAL: Recalculate health score, lifecycle, AND recommendations after edits
        calculate_health_score(machine)
        categorize_machine(machine, repairs)
        auto_recommend(machine)  # Refresh recommendations based on new specs
        
        save_data(MACHINES_FILE, machines)
        log_activity("EDIT", "MACHINE", machine_code, f"Edited machine: {machine['computer_name']}", state_data=dict(machine))
        return redirect(url_for("machines_page"))

    _, all_branches = get_branch_lists()
    name_counts = get_user_name_counts()
    return render_template(
        "edit_machine.html",
        machine=machine,
        users=users,
        branches=all_branches,
        name_counts=name_counts
    )

@app.route("/delete_user/<user_code>", methods=["POST"])
def delete_user(user_code):
    global users
    user = next((u for u in users if u["user_code"] == user_code), None)
    user_name = user["user_name"] if user else "Unknown"
    users = [u for u in users if u["user_code"] != user_code]
    save_data(USERS_FILE, users)
    log_activity("DELETE", "USER", user_code, f"Deleted user: {user_name}")
    return redirect(url_for("users_page"))

@app.route("/delete_machine/<machine_code>", methods=["POST"])
def delete_machine(machine_code):
    global machines, repairs
    machine = next((m for m in machines if m["machine_code"] == machine_code), None)
    machine_name = machine["computer_name"] if machine else "Unknown"
    machines = [m for m in machines if m["machine_code"] != machine_code]
    repairs = [r for r in repairs if r["machine_code"] != machine_code]
    save_data(MACHINES_FILE, machines)
    save_data(REPAIRS_FILE, repairs)
    log_activity("DELETE", "MACHINE", machine_code, f"Deleted machine: {machine_name}")
    return redirect(url_for("machines_page"))

@app.route("/delete_repair/<int:repair_index>", methods=["POST"])
def delete_repair(repair_index):
    global repairs
    if 0 <= repair_index < len(repairs):
        repair = repairs[repair_index]
        repair_id = repair.get("machine_code", f"Index-{repair_index}")
        machine_code = repair.get("machine_code")
        
        repairs.pop(repair_index)
        save_data(REPAIRS_FILE, repairs)
        log_activity("DELETE", "REPAIR", repair_id, f"Deleted repair for {repair.get('computer_name', 'Unknown')}")
        
        # Recalculate machine recommendations after repair deletion
        if machine_code:
            for m in machines:
                if m["machine_code"] == machine_code:
                    calculate_health_score(m)
                    categorize_machine(m, repairs)
                    auto_recommend(m)
                    break
            save_data(MACHINES_FILE, machines)
    
    return redirect(url_for("repairs_page"))

def normalize_yes_no(value, default="No"):
    if value is None:
        return default
    val = str(value).strip().lower()
    if val in ["yes", "y", "true", "1", "✓"]:
        return "Yes"
    if val in ["no", "n", "false", "0", "x"]:
        return "No"
    return default

def normalize_yes_no_lower(value, default="no"):
    if value is None:
        return default
    val = str(value).strip().lower()
    if val in ["yes", "y", "true", "1", "✓"]:
        return "yes"
    if val in ["no", "n", "false", "0", "x"]:
        return "no"
    return default

def normalize_storage(value):
    if not value:
        return ""
    val = str(value).strip().lower()
    if val in ["hdd", "hard drive", "harddisk"]:
        return "HDD"
    if val in ["ssd", "2.5 ssd", "2.5" , "2.5ssd", "2.5-inch ssd", "sata ssd"]:
        return "2.5 SSD"
    if val in ["nvme", "m.2", "m2"]:
        return "NVMe"
    if val in ["no drive", "missing", "none", "no storage"]:
        return "No Drive"
    return str(value).strip()

def parse_ram_value(value):
    if not value:
        return ""
    val = str(value).strip().upper().replace(" ", "")
    if val.isdigit():
        return f"{val}GB"
    if val.endswith("GB"):
        return val
    return str(value).strip()

def calculate_new_ram_from_added(original_ram, ram_added):
    try:
        original = int(str(original_ram).replace("GB", "").split()[0])
    except:
        original = 0
    try:
        added = int(str(ram_added).replace("GB", "").split()[0])
    except:
        added = 0
    if added <= 0:
        return ""
    return f"{original + added}GB"

def build_upgrade_record(machine, machine_code, upgrade_date, ram_upgraded, ram_added, new_ram,
                         storage_upgraded, new_storage, includes_service, upgrade_cost,
                         technician, notes, backup_verified, signature):
    original_ram = machine.get("ram", "0GB")
    original_storage = machine.get("storage_type", "")
    ram_upgraded = normalize_yes_no(ram_upgraded, "No")
    storage_upgraded = normalize_yes_no(storage_upgraded, "No")
    includes_service = normalize_yes_no_lower(includes_service, "no")
    backup_verified = normalize_yes_no(backup_verified, "") if backup_verified else ""

    ram_added_val = str(ram_added).strip() if ram_added is not None else ""
    new_ram_val = parse_ram_value(new_ram)
    if ram_upgraded == "Yes" and not new_ram_val and ram_added_val:
        new_ram_val = calculate_new_ram_from_added(original_ram, ram_added_val)
    if ram_upgraded != "Yes":
        new_ram_val = machine.get("ram", "")

    new_storage_val = normalize_storage(new_storage)
    if storage_upgraded != "Yes":
        new_storage_val = machine.get("storage_type", "")

    return {
        "upgrade_id": str(uuid.uuid4())[:8],
        "machine_code": machine_code,
        "computer_name": machine.get("computer_name", ""),
        "assigned_to": machine.get("assigned_to", ""),
        "assigned_to_code": machine.get("assigned_to_code", ""),
        "machine_location": machine.get("machine_location", ""),
        "upgrade_date": upgrade_date,
        "ram_upgraded": ram_upgraded,
        "ram_added": ram_added_val,
        "original_ram": original_ram,
        "new_ram": new_ram_val or machine.get("ram", ""),
        "storage_upgraded": storage_upgraded,
        "original_storage": original_storage,
        "new_storage": new_storage_val or machine.get("storage_type", ""),
        "includes_service": includes_service,
        "backup_verified": backup_verified,
        "upgrade_cost": upgrade_cost,
        "technician": technician,
        "signature": signature,
        "notes": notes
    }

def apply_upgrade_to_machine(machine, upgrade, repairs):
    if upgrade.get("ram_upgraded") == "Yes":
        machine["ram"] = upgrade.get("new_ram", machine.get("ram", ""))
    if upgrade.get("storage_upgraded") == "Yes":
        machine["storage_type"] = upgrade.get("new_storage", machine.get("storage_type", ""))

    if upgrade.get("includes_service") == "yes":
        machine["last_service_repaste"] = upgrade.get("upgrade_date")
        machine["service_done"] = True

    backup_verified = upgrade.get("backup_verified")
    if backup_verified:
        machine["backup_status"] = backup_verified
        if backup_verified == "Yes" and not machine.get("backup_type"):
            machine["backup_type"] = "Cloud Backup (OneDrive/Google Drive/etc.)"

    calculate_health_score(machine)
    categorize_machine(machine, repairs)
    auto_recommend(machine)

@app.route("/upgrades")
def upgrades_page():
    bulk_added = request.args.get("bulk_added")
    bulk_errors = request.args.get("bulk_errors")
    bulk_message = request.args.get("bulk_message")
    _, all_branches = get_branch_lists()
    machines_by_code = {m.get("machine_code"): m for m in machines}
    upgrades_view = []
    for upgrade in upgrades:
        upgrade_view = dict(upgrade)
        if not upgrade_view.get("assigned_to"):
            machine = machines_by_code.get(upgrade_view.get("machine_code"))
            if machine:
                upgrade_view["assigned_to"] = machine.get("assigned_to", "")
        if not upgrade_view.get("machine_location"):
            machine = machines_by_code.get(upgrade_view.get("machine_code"))
            if machine:
                upgrade_view["machine_location"] = machine.get("machine_location", "")
        if not upgrade_view.get("original_storage"):
            new_storage = upgrade_view.get("new_storage", "")
            if upgrade_view.get("storage_upgraded") == "Yes" and new_storage in ["2.5 SSD", "NVMe", "SSD"]:
                upgrade_view["original_storage"] = "HDD"
            else:
                machine = machines_by_code.get(upgrade_view.get("machine_code"))
                if machine:
                    upgrade_view["original_storage"] = machine.get("storage_type", "")
        upgrades_view.append(upgrade_view)
    return render_template(
        "upgrades.html",
        upgrades=upgrades_view,
        machines=machines,
        branches=all_branches,
        bulk_added=bulk_added,
        bulk_errors=bulk_errors,
        bulk_message=bulk_message
    )

@app.route("/add_upgrade", methods=["POST"])
def add_upgrade():
    global machines, upgrades
    machine_code = request.form["machine_code"]
    upgrade_date = request.form["upgrade_date"]
    signature = request.form.get("signature", "").strip()
    upgrade_cost = request.form.get("upgrade_cost", "")
    
    # Find the machine
    machine = next((m for m in machines if m["machine_code"] == machine_code), None)
    if not machine:
        return "Machine not found", 404
    
    upgrade = build_upgrade_record(
        machine=machine,
        machine_code=machine_code,
        upgrade_date=upgrade_date,
        ram_upgraded=request.form.get("ram_upgraded", "No"),
        ram_added=request.form.get("ram_added", ""),
        new_ram=request.form.get("new_ram", machine.get("ram", "")),
        storage_upgraded=request.form.get("storage_upgraded", "No"),
        new_storage=request.form.get("new_storage", machine.get("storage_type", "")),
        includes_service=request.form.get("includes_service", "no"),
        upgrade_cost=upgrade_cost,
        technician=request.form["technician"],
        notes=request.form.get("notes", ""),
        backup_verified=request.form.get("backup_verified"),
        signature=signature
    )

    apply_upgrade_to_machine(machine, upgrade, repairs)
    
    # Save changes
    upgrades.append(upgrade)
    save_data(MACHINES_FILE, machines)
    save_data(UPGRADES_FILE, upgrades)
    
    log_activity("ADD", "UPGRADE", machine_code, f"Upgraded {machine['computer_name']} - RAM: {upgrade['ram_upgraded']}, Storage: {upgrade['storage_upgraded']}", state_data=dict(upgrade))
    
    return redirect(url_for("upgrades_page"))

@app.route("/add_upgrades_batch", methods=["POST"])
def add_upgrades_batch():
    global machines, upgrades
    machine_codes = request.form.getlist("machine_code[]")
    upgrade_dates = request.form.getlist("upgrade_date[]")
    ram_upgraded_list = request.form.getlist("ram_upgraded[]")
    ram_added_list = request.form.getlist("ram_added[]")
    new_ram_list = request.form.getlist("new_ram[]")
    storage_upgraded_list = request.form.getlist("storage_upgraded[]")
    new_storage_list = request.form.getlist("new_storage[]")
    includes_service_list = request.form.getlist("includes_service[]")
    upgrade_cost_list = request.form.getlist("upgrade_cost[]")
    backup_verified_list = request.form.getlist("backup_verified[]")
    technician_list = request.form.getlist("technician[]")
    signature_list = request.form.getlist("signature[]")
    notes_list = request.form.getlist("notes[]")

    total = len(machine_codes)
    if total == 0:
        return redirect(url_for("upgrades_page", bulk_added=0, bulk_errors=1, bulk_message="No upgrades in batch"))

    added = 0
    errors = []

    for i in range(total):
        machine_code = machine_codes[i].strip() if i < len(machine_codes) else ""
        if not machine_code:
            errors.append(f"Row {i + 1}: Missing machine code")
            continue

        machine = next((m for m in machines if m["machine_code"] == machine_code), None)
        if not machine:
            errors.append(f"Row {i + 1}: Machine code {machine_code} not found")
            continue

        upgrade_date = upgrade_dates[i] if i < len(upgrade_dates) else ""
        technician = technician_list[i] if i < len(technician_list) else ""
        signature = signature_list[i] if i < len(signature_list) else ""
        if not upgrade_date or not technician:
            errors.append(f"Row {i + 1}: Missing upgrade date or technician")
            continue

        upgrade = build_upgrade_record(
            machine=machine,
            machine_code=machine_code,
            upgrade_date=upgrade_date,
            ram_upgraded=ram_upgraded_list[i] if i < len(ram_upgraded_list) else "No",
            ram_added=ram_added_list[i] if i < len(ram_added_list) else "",
            new_ram=new_ram_list[i] if i < len(new_ram_list) else "",
            storage_upgraded=storage_upgraded_list[i] if i < len(storage_upgraded_list) else "No",
            new_storage=new_storage_list[i] if i < len(new_storage_list) else "",
            includes_service=includes_service_list[i] if i < len(includes_service_list) else "no",
            upgrade_cost=upgrade_cost_list[i] if i < len(upgrade_cost_list) else "",
            technician=technician,
            notes=notes_list[i] if i < len(notes_list) else "",
            backup_verified=backup_verified_list[i] if i < len(backup_verified_list) else "",
            signature=signature
        )

        apply_upgrade_to_machine(machine, upgrade, repairs)
        upgrades.append(upgrade)
        added += 1

    if added:
        save_data(MACHINES_FILE, machines)
        save_data(UPGRADES_FILE, upgrades)
        log_activity("ADD_BATCH", "UPGRADE", "BATCH", f"Batch added {added} upgrades")

    bulk_message = "" if not errors else " | ".join(errors[:3])
    return redirect(url_for("upgrades_page", bulk_added=added, bulk_errors=len(errors), bulk_message=bulk_message))

@app.route("/add_upgrades_bulk", methods=["POST"])
def add_upgrades_bulk():
    global machines, upgrades
    default_date = request.form.get("bulk_upgrade_date", "").strip()
    default_technician = request.form.get("bulk_technician", "").strip()
    default_backup_verified = request.form.get("bulk_backup_verified", "").strip()
    default_includes_service = request.form.get("bulk_includes_service", "").strip()
    bulk_notes = request.form.get("bulk_notes", "")

    if not default_date:
        return redirect(url_for("upgrades_page", bulk_added=0, bulk_errors=1, bulk_message="Default upgrade date is required"))
    if not default_technician:
        return redirect(url_for("upgrades_page", bulk_added=0, bulk_errors=1, bulk_message="Default technician is required"))

    lines = bulk_notes.splitlines()
    added = 0
    errors = []

    for idx, raw_line in enumerate(lines, start=1):
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue

        lower_line = line.lower()
        if "machine" in lower_line and ("code" in lower_line or "machine_code" in lower_line):
            continue

        machine_code = ""
        data = {}
        notes = ""

        if "=" in line:
            parts = [p.strip() for p in line.split("|", 1)]
            machine_code = parts[0].strip()
            kv_text = parts[1] if len(parts) > 1 else ""
            kv_pairs = [p.strip() for p in kv_text.split(";") if p.strip()]
            for pair in kv_pairs:
                if "=" in pair:
                    k, v = pair.split("=", 1)
                    data[k.strip().lower()] = v.strip()
                else:
                    notes = (notes + " " + pair).strip()
        else:
            delimiter = "|" if "|" in line else ","
            cols = [c.strip() for c in line.split(delimiter) if c.strip() or c == ""]
            if cols:
                machine_code = cols[0]
            if len(cols) > 1:
                if len(cols[1]) == 10 and cols[1][4] == "-" and cols[1][7] == "-":
                    data["upgrade_date"] = cols[1]
                    start_idx = 2
                else:
                    start_idx = 1
                field_map = [
                    "ram_upgraded",
                    "ram_added",
                    "new_ram",
                    "storage_upgraded",
                    "new_storage",
                    "includes_service",
                    "backup_verified",
                    "technician",
                    "notes"
                ]
                for i, key in enumerate(field_map):
                    col_index = start_idx + i
                    if col_index < len(cols):
                        data[key] = cols[col_index]

        if not machine_code:
            errors.append(f"Line {idx}: Missing machine code")
            continue

        machine = next((m for m in machines if m["machine_code"] == machine_code), None)
        if not machine:
            errors.append(f"Line {idx}: Machine code {machine_code} not found")
            continue

        upgrade_date = data.get("date") or data.get("upgrade_date") or default_date
        technician = data.get("technician") or data.get("tech") or default_technician
        signature = data.get("signature") or default_technician
        if not upgrade_date or not technician:
            errors.append(f"Line {idx}: Missing upgrade date or technician for {machine_code}")
            continue

        ram_upgraded = data.get("ram_upgraded") or "No"
        ram_added = data.get("ram_added") or ""
        new_ram = data.get("new_ram") or ""
        storage_upgraded = data.get("storage_upgraded") or "No"
        new_storage = data.get("new_storage") or data.get("storage") or ""
        includes_service = data.get("includes_service") or data.get("service") or default_includes_service or "no"
        upgrade_cost = data.get("upgrade_cost") or data.get("cost") or ""
        backup_verified = data.get("backup_verified") or data.get("backup") or default_backup_verified
        notes_text = data.get("notes") or data.get("note") or notes

        upgrade = build_upgrade_record(
            machine=machine,
            machine_code=machine_code,
            upgrade_date=upgrade_date,
            ram_upgraded=ram_upgraded,
            ram_added=ram_added,
            new_ram=new_ram,
            storage_upgraded=storage_upgraded,
            new_storage=new_storage,
            includes_service=includes_service,
            upgrade_cost=upgrade_cost,
            technician=technician,
            notes=notes_text,
            backup_verified=backup_verified,
            signature=signature
        )

        apply_upgrade_to_machine(machine, upgrade, repairs)
        upgrades.append(upgrade)
        added += 1

    if added:
        save_data(MACHINES_FILE, machines)
        save_data(UPGRADES_FILE, upgrades)
        log_activity("ADD_BULK", "UPGRADE", "BULK_IMPORT", f"Bulk imported {added} upgrades from text")

    bulk_message = "" if not errors else " | ".join(errors[:3])
    return redirect(url_for("upgrades_page", bulk_added=added, bulk_errors=len(errors), bulk_message=bulk_message))

@app.route("/edit_upgrade/<upgrade_id>", methods=["GET", "POST"])
def edit_upgrade(upgrade_id):
    """Edit an existing upgrade record"""
    upgrade = next((u for u in upgrades if u.get("upgrade_id") == upgrade_id), None)
    if not upgrade:
        return "Upgrade not found", 404
    
    if request.method == "POST":
        # Update upgrade record
        upgrade["upgrade_date"] = request.form.get("upgrade_date")
        upgrade["ram_upgraded"] = request.form.get("ram_upgraded", "No")
        upgrade["ram_added"] = request.form.get("ram_added", "")
        upgrade["new_ram"] = request.form.get("new_ram", "")
        upgrade["storage_upgraded"] = request.form.get("storage_upgraded", "No")
        upgrade["new_storage"] = request.form.get("new_storage", "")
        upgrade["includes_service"] = request.form.get("includes_service", "no")
        upgrade["backup_verified"] = request.form.get("backup_verified", "")
        upgrade["upgrade_cost"] = request.form.get("upgrade_cost", "")
        upgrade["technician"] = request.form.get("technician")
        upgrade["signature"] = request.form.get("signature", "")
        upgrade["notes"] = request.form.get("notes", "")
        
        # Update the associated machine based on upgrade changes
        machine_code = upgrade.get("machine_code")
        if machine_code:
            machine = next((m for m in machines if m["machine_code"] == machine_code), None)
            if machine:
                apply_upgrade_to_machine(machine, upgrade, repairs)
                save_data(MACHINES_FILE, machines)
        
        save_data(UPGRADES_FILE, upgrades)
        log_activity("EDIT", "UPGRADE", upgrade_id, f"Edited upgrade for {upgrade.get('computer_name', 'Unknown')}")
        return redirect(url_for("upgrades_page"))
    
    return render_template("edit_upgrade.html", upgrade=upgrade)

@app.route("/delete_upgrade/<upgrade_id>")
def delete_upgrade(upgrade_id):
    """Delete an upgrade record"""
    global upgrades
    upgrade = next((u for u in upgrades if u.get("upgrade_id") == upgrade_id), None)
    upgrade_name = upgrade.get("computer_name", "Unknown") if upgrade else "Unknown"
    machine_code = upgrade.get("machine_code") if upgrade else None
    
    upgrades = [u for u in upgrades if u.get("upgrade_id") != upgrade_id]
    save_data(UPGRADES_FILE, upgrades)
    log_activity("DELETE", "UPGRADE", upgrade_id, f"Deleted upgrade for {upgrade_name}")
    
    # Recalculate machine recommendations after upgrade deletion
    if machine_code:
        for m in machines:
            if m["machine_code"] == machine_code:
                calculate_health_score(m)
                categorize_machine(m, repairs)
                auto_recommend(m)
                break
        save_data(MACHINES_FILE, machines)
    
    return redirect(url_for("upgrades_page"))

@app.route('/api/debug')
def api_debug():
    return {
        'machines': machines,
        'repairs': repairs,
        'users': users
    }

@app.route("/users")
def users_page():
    sorted_users = sorted(users, key=lambda u: u["user_name"])
    active_branches, _ = get_branch_lists()
    return render_template("users.html", users=sorted_users, branches=active_branches)

@app.route("/machines")
def machines_page():
    selected_tab = request.args.get("tab", "desktops")
    active_branches, _ = get_branch_lists()
    name_counts = get_user_name_counts()
    return render_template(
        "machines.html",
        machines=machines,
        users=users,
        selected_tab=selected_tab,
        branches=active_branches,
        name_counts=name_counts
    )

@app.route("/export_machines_excel")
def export_machines_excel():
    """Export machines to CSV (Excel-compatible)"""
    output = io.BytesIO()
    
    # Write UTF-8 BOM for Excel compatibility
    output.write('\ufeff'.encode('utf-8'))
    
    # Create CSV content
    csv_content = io.StringIO()
    writer = csv.writer(csv_content)
    
    # Write header
    writer.writerow(['Code', 'Name', 'Processor', 'Generation', 'RAM', 'Storage', 'Device Type', 
                     'Recommendations', 'Health Score', 'Backup', 'Repair Risk', 'Assigned', 'Location'])
    
    # Write data
    for m in machines:
        writer.writerow([
            m.get('machine_code', ''),
            m.get('computer_name', ''),
            m.get('processor', ''),
            m.get('generation', ''),
            m.get('ram', ''),
            m.get('storage_type', ''),
            m.get('device_type', ''),
            m.get('recommended_upgrades', ''),
            m.get('health_score', ''),
            m.get('backup_category', ''),
            m.get('repair_risk', ''),
            m.get('assigned_to', ''),
            m.get('machine_location', '')
        ])
    
    # Write CSV content to BytesIO with UTF-8 encoding
    output.write(csv_content.getvalue().encode('utf-8'))
    output.seek(0)
    
    return Response(
        output.getvalue(),
        mimetype='text/csv; charset=utf-8',
        headers={'Content-Disposition': 'attachment;filename=machines_export.csv'}
    )

@app.route("/repairs")
def repairs_page():
    return render_template("repairs.html", repairs=repairs, machines=machines)

@app.route("/recommendations")
def recommendations_page():
    return render_template("recommendations.html", recommendations=SYSTEM_RECOMMENDATIONS)

@app.route("/reallocation")
def reallocation_page():
    sorted_users = sorted(users, key=lambda u: u.get("user_name", ""))
    sorted_machines = sorted(machines, key=lambda m: m.get("machine_code", ""))
    sorted_reallocations = sorted(reallocation_log, key=lambda r: r.get("timestamp", ""), reverse=True)
    name_counts = get_user_name_counts()
    return render_template(
        "reallocation.html",
        users=sorted_users,
        machines=sorted_machines,
        reallocations=sorted_reallocations,
        name_counts=name_counts
    )

@app.route("/purchases")
def purchases_page():
    sorted_purchases = sorted(purchases, key=lambda p: p.get("purchase_date", ""), reverse=True)
    sorted_users = sorted(users, key=lambda u: u.get("user_name", ""))
    sorted_machines = sorted(machines, key=lambda m: m.get("machine_code", ""))
    cost_report_context = build_cost_report_context()
    return render_template(
        "purchases.html",
        purchases=sorted_purchases,
        users=sorted_users,
        machines=sorted_machines,
        **cost_report_context
    )

@app.route("/add_purchase", methods=["POST"])
def add_purchase():
    item = request.form.get("item", "").strip()
    if not item:
        return "Error: Item is required.", 400
    purchase = {
        "purchase_id": str(uuid.uuid4())[:10],
        "item": item,
        "purchase_date": request.form.get("purchase_date", ""),
        "vendor": request.form.get("vendor", ""),
        "price": parse_money(request.form.get("price", ""), 0.0),
        "assigned_user": request.form.get("assigned_user", ""),
        "machine_code": request.form.get("machine_code", ""),
        "notes": request.form.get("notes", "")
    }
    purchases.append(purchase)
    save_data(PURCHASES_FILE, purchases)
    log_activity("ADD", "PURCHASE", purchase["purchase_id"], f"Purchased {purchase['item']} for {purchase['assigned_user']} ({purchase['machine_code']})", state_data=dict(purchase))
    return redirect(url_for("purchases_page"))

@app.route("/delete_purchase/<purchase_id>", methods=["POST"])
def delete_purchase(purchase_id):
    global purchases
    purchase = next((p for p in purchases if p.get("purchase_id") == purchase_id), None)
    purchases = [p for p in purchases if p.get("purchase_id") != purchase_id]
    save_data(PURCHASES_FILE, purchases)
    if purchase:
        log_activity("DELETE", "PURCHASE", purchase_id, f"Deleted purchase: {purchase.get('item', '')}")
    return redirect(url_for("purchases_page"))

@app.route("/assets")
def assets_page():
    sorted_assets = sorted(reserve_assets, key=lambda a: a.get("status", ""))
    return render_template("assets.html", assets=sorted_assets)

@app.route("/add_asset", methods=["POST"])
def add_asset():
    asset = {
        "asset_id": str(uuid.uuid4())[:10],
        "asset_type": request.form.get("asset_type", ""),
        "status": request.form.get("status", "Reserve"),
        "location": request.form.get("location", ""),
        "notes": request.form.get("notes", ""),
        "disposal_required": request.form.get("disposal_required", "No"),
        "wipe_status": request.form.get("wipe_status", "Pending"),
        "certificate_ref": request.form.get("certificate_ref", "")
    }
    reserve_assets.append(asset)
    save_data(ASSETS_FILE, reserve_assets)
    log_activity("ADD", "ASSET", asset["asset_id"], f"Added asset: {asset['asset_type']} ({asset['status']})", state_data=dict(asset))
    return redirect(url_for("assets_page"))

@app.route("/delete_asset/<asset_id>", methods=["POST"])
def delete_asset(asset_id):
    global reserve_assets
    asset = next((a for a in reserve_assets if a.get("asset_id") == asset_id), None)
    reserve_assets = [a for a in reserve_assets if a.get("asset_id") != asset_id]
    save_data(ASSETS_FILE, reserve_assets)
    if asset:
        log_activity("DELETE", "ASSET", asset_id, f"Deleted asset: {asset.get('asset_type', '')}")
    return redirect(url_for("assets_page"))

def build_cost_report_context(start_date=None, end_date=None):
    """Build cost report, optionally filtered by date range (YYYY-MM-DD)"""
    from datetime import datetime
    
    service_cost = parse_money(cost_settings.get("service_cost", 0), 0)
    mouse_cost = parse_money(cost_settings.get("mouse_cost", 0), 0)
    upgrade_default_cost = parse_money(cost_settings.get("upgrade_default_cost", 0), 0)
    repair_default_cost = parse_money(cost_settings.get("repair_default_cost", 0), 0)

    def is_in_date_range(date_str):
        if not date_str or (not start_date and not end_date):
            return True
        try:
            item_date = datetime.strptime(str(date_str).strip(), "%Y-%m-%d").date()
            if start_date:
                start = datetime.strptime(str(start_date).strip(), "%Y-%m-%d").date()
                if item_date < start:
                    return False
            if end_date:
                end = datetime.strptime(str(end_date).strip(), "%Y-%m-%d").date()
                if item_date > end:
                    return False
            return True
        except ValueError:
            return False

    # Filter purchases
    filtered_purchases = [p for p in purchases if is_in_date_range(p.get("purchase_date", ""))]
    purchases_total = sum(parse_money(p.get("price", 0), 0) for p in filtered_purchases)
    
    mouse_total = 0.0
    for p in filtered_purchases:
        if "mouse" in str(p.get("item", "")).lower():
            price_val = parse_money(p.get("price", 0), 0)
            mouse_total += price_val if price_val > 0 else mouse_cost

    # Filter upgrades
    filtered_upgrades = [u for u in upgrades if is_in_date_range(u.get("upgrade_date", ""))]
    upgrade_total = 0.0
    service_total = 0.0
    for u in filtered_upgrades:
        if str(u.get("includes_service", "")).lower() == "yes":
            service_total += service_cost
        cost_val = parse_money(u.get("upgrade_cost", ""), 0)
        upgrade_total += cost_val if cost_val > 0 else upgrade_default_cost

    # Filter repairs
    filtered_repairs = [r for r in repairs if is_in_date_range(r.get("repair_date", ""))]
    repair_total = 0.0
    for r in filtered_repairs:
        cost_val = parse_money(r.get("repair_cost", ""), 0)
        repair_total += cost_val if cost_val > 0 else repair_default_cost

    overall_total = purchases_total + mouse_total + upgrade_total + repair_total + service_total

    return {
        "cost_settings": cost_settings,
        "purchases_total": purchases_total,
        "mouse_total": mouse_total,
        "upgrade_total": upgrade_total,
        "repair_total": repair_total,
        "service_total": service_total,
        "overall_total": overall_total,
        "start_date": start_date,
        "end_date": end_date,
        "filtered_repairs": filtered_repairs,
        "filtered_upgrades": filtered_upgrades,
        "filtered_purchases": filtered_purchases
    }

@app.route("/cost_report")
def cost_report_page():
    start_date = request.args.get("start_date", "")
    end_date = request.args.get("end_date", "")
    cost_report_context = build_cost_report_context(start_date if start_date else None, end_date if end_date else None)
    return render_template(
        "cost_report.html",
        **cost_report_context
    )

@app.route("/update_cost_settings", methods=["POST"])

@app.route("/update_cost_settings", methods=["POST"])
def update_cost_settings():
    cost_settings["service_cost"] = parse_money(request.form.get("service_cost", 0), 0)
    cost_settings["mouse_cost"] = parse_money(request.form.get("mouse_cost", 0), 0)
    cost_settings["upgrade_default_cost"] = parse_money(request.form.get("upgrade_default_cost", 0), 0)
    cost_settings["repair_default_cost"] = parse_money(request.form.get("repair_default_cost", 0), 0)
    save_data(COST_SETTINGS_FILE, cost_settings)
    return redirect(url_for("purchases_page"))

@app.route("/reallocate_machine", methods=["POST"])
def reallocate_machine():
    machine_code = request.form.get("machine_code")
    new_user_code = request.form.get("new_user")
    reason = request.form.get("reason", "").strip()
    performed_by = request.form.get("performed_by", "System").strip() or "System"
    reallocation_mode = request.form.get("reallocation_mode", "swap")
    reserve_targets = RESERVE_TARGETS

    machine = next((m for m in machines if m.get("machine_code") == machine_code), None)
    if not machine:
        return "Machine not found", 404

    new_user = None
    if new_user_code not in reserve_targets:
        new_user = next((u for u in users if u.get("user_code") == new_user_code), None)
        if not new_user:
            return "User not found", 404

    if machine.get("assigned_to_code", "") == new_user_code:
        return "User already assigned to this machine.", 400

    old_user = machine.get("assigned_to", "Unassigned")
    old_user_code = machine.get("assigned_to_code", "")
    old_user_record = next((u for u in users if u.get("user_code") == old_user_code), None)
    new_user_machine = None
    if new_user_code not in reserve_targets:
        new_user_machine = next(
            (m for m in machines if m.get("assigned_to_code") == new_user_code and m.get("machine_code") != machine_code),
            None
        )

    def append_reallocation_entry(target_machine, from_user, to_user, to_user_level=""):
        entry = {
            "reallocation_id": str(uuid.uuid4())[:10],
            "timestamp": datetime.datetime.now().isoformat(),
            "machine_code": target_machine.get("machine_code", ""),
            "computer_name": target_machine.get("computer_name", ""),
            "from_user": from_user,
            "to_user": to_user,
            "user_level": to_user_level,
            "reason": reason,
            "performed_by": performed_by
        }
        reallocation_log.append(entry)

    def reserve_machine(target_machine):
        asset = {
            "asset_id": str(uuid.uuid4())[:10],
            "asset_type": "Machine",
            "status": "Reserve",
            "location": target_machine.get("machine_location", ""),
            "notes": (
                f"Reserved from reallocation. Machine {target_machine.get('machine_code', '')} - "
                f"{target_machine.get('computer_name', '')}."
            ),
            "disposal_required": "No",
            "wipe_status": "Pending",
            "certificate_ref": ""
        }
        reserve_assets.append(asset)
        save_data(ASSETS_FILE, reserve_assets)
        log_activity(
            "ADD",
            "ASSET",
            asset["asset_id"],
            f"Added reserve machine: {target_machine.get('machine_code', '')}",
            user=performed_by,
            state_data=dict(asset)
        )

    if new_user_machine:
        if reallocation_mode == "reserve":
            machine["assigned_to"] = new_user.get("user_name", "")
            machine["assigned_to_code"] = new_user_code
            new_user_machine["assigned_to"] = "Reserve"
            new_user_machine["assigned_to_code"] = ""
            save_data(MACHINES_FILE, machines)

            reserve_machine(new_user_machine)

            append_reallocation_entry(machine, old_user, new_user.get("user_name", ""), new_user.get("user_level", ""))
            append_reallocation_entry(new_user_machine, new_user.get("user_name", ""), "Reserve", "")

            log_activity(
                "REALLOCATE",
                "MACHINE",
                machine_code,
                f"Reallocated from {old_user} to {new_user.get('user_name', '')}. Previous machine moved to Reserve. {reason}".strip(),
                user=performed_by,
                state_data=dict(machine)
            )
            log_activity(
                "REALLOCATE",
                "MACHINE",
                new_user_machine.get("machine_code", ""),
                f"Moved to Reserve due to reallocation to {new_user.get('user_name', '')}. {reason}".strip(),
                user=performed_by,
                state_data=dict(new_user_machine)
            )
        else:
            machine["assigned_to"] = new_user.get("user_name", "")
            machine["assigned_to_code"] = new_user_code
            new_user_machine["assigned_to"] = old_user
            new_user_machine["assigned_to_code"] = old_user_code
            save_data(MACHINES_FILE, machines)

            append_reallocation_entry(machine, old_user, new_user.get("user_name", ""), new_user.get("user_level", ""))
            append_reallocation_entry(new_user_machine, new_user.get("user_name", ""), old_user, (old_user_record or {}).get("user_level", ""))

            log_activity(
                "REALLOCATE",
                "MACHINE",
                machine_code,
                f"Swapped from {old_user} to {new_user.get('user_name', '')}. {reason}".strip(),
                user=performed_by,
                state_data=dict(machine)
            )
            log_activity(
                "REALLOCATE",
                "MACHINE",
                new_user_machine.get("machine_code", ""),
                f"Swapped from {new_user.get('user_name', '')} to {old_user}. {reason}".strip(),
                user=performed_by,
                state_data=dict(new_user_machine)
            )
    else:
        if new_user_code in reserve_targets:
            machine["assigned_to"] = "Reserve"
            machine["assigned_to_code"] = ""
        else:
            machine["assigned_to"] = new_user.get("user_name", "")
            machine["assigned_to_code"] = new_user_code
        save_data(MACHINES_FILE, machines)

        append_reallocation_entry(
            machine,
            old_user,
            "Reserve" if new_user_code in reserve_targets else new_user.get("user_name", ""),
            "" if new_user_code in reserve_targets else new_user.get("user_level", "")
        )

        log_activity(
            "REALLOCATE",
            "MACHINE",
            machine_code,
            f"Reallocated from {old_user} to {('Reserve' if new_user_code in reserve_targets else new_user.get('user_name', ''))}. {reason}".strip(),
            user=performed_by,
            state_data=dict(machine)
        )

    save_data(REALLOCATION_LOG_FILE, reallocation_log)
    return redirect(url_for("reallocation_page"))

@app.route("/servers")
def servers_page():
    """Server & Data Protection page"""
    active_branches, _ = get_branch_lists()
    return render_template("servers.html", servers=servers, branches=active_branches)

@app.route("/branches")
def branches_page():
    sorted_branches = sorted(
        branches,
        key=lambda b: (b.get("status", "Active") != "Active", b.get("name", ""))
    )
    return render_template("branches.html", branches=sorted_branches)

@app.route("/add_branch", methods=["POST"])
def add_branch():
    name = normalize_branch_name(request.form.get("branch_name", ""))
    if not name:
        return "Error: Branch name is required.", 400
    if any(b.get("name", "").lower() == name.lower() for b in branches):
        return "Error: Branch already exists.", 400

    branch = {
        "branch_id": str(uuid.uuid4())[:8],
        "name": name,
        "status": "Active",
        "created_at": datetime.datetime.now().strftime("%Y-%m-%d"),
        "retired_at": ""
    }
    branches.append(branch)
    save_data(BRANCHES_FILE, branches)
    log_activity("ADD_BRANCH", "BRANCH", branch["branch_id"], f"Added branch: {name}")
    return redirect(url_for("branches_page"))

@app.route("/retire_branch/<branch_id>", methods=["POST"])
def retire_branch(branch_id):
    branch = next((b for b in branches if b.get("branch_id") == branch_id), None)
    if not branch:
        return "Branch not found", 404
    if branch.get("status") == "Retired":
        return redirect(url_for("branches_page"))

    branch["status"] = "Retired"
    branch["retired_at"] = datetime.datetime.now().strftime("%Y-%m-%d")

    moved_machines = []
    for m in machines:
        if m.get("machine_location") == branch.get("name"):
            if m.get("assigned_to") != "Reserve":
                m["assigned_to"] = "Reserve"
            moved_machines.append(m)

    if moved_machines:
        save_data(MACHINES_FILE, machines)

        for m in moved_machines:
            machine_code = m.get("machine_code")
            existing = next(
                (a for a in reserve_assets
                 if (a.get("linked_machine_code") == machine_code
                     or machine_code in str(a.get("notes", "")))
                 and a.get("status") == "Reserve"),
                None
            )
            if existing:
                continue
            asset = {
                "asset_id": str(uuid.uuid4())[:10],
                "asset_type": "Machine",
                "status": "Reserve",
                "location": m.get("machine_location", ""),
                "notes": (
                    f"Reserved due to branch retirement. Machine {m.get('machine_code', '')} - "
                    f"{m.get('computer_name', '')}."
                ),
                "disposal_required": "No",
                "wipe_status": "Pending",
                "certificate_ref": "",
                "linked_machine_code": m.get("machine_code", "")
            }
            reserve_assets.append(asset)
            log_activity(
                "ADD",
                "ASSET",
                asset["asset_id"],
                f"Added reserve machine: {m.get('machine_code', '')}",
                state_data=dict(asset)
            )

        save_data(ASSETS_FILE, reserve_assets)

    save_data(BRANCHES_FILE, branches)
    log_activity(
        "RETIRE_BRANCH",
        "BRANCH",
        branch_id,
        f"Retired branch: {branch.get('name', '')}. Moved {len(moved_machines)} machines to Reserve."
    )
    return redirect(url_for("branches_page"))

@app.route("/add_server", methods=["POST"])
def add_server():
    """Add or update server configuration"""
    server = {
        "server_code": request.form.get("server_code") or generate_server_code(),
        "server_name": request.form.get("server_name"),
        "location": request.form.get("location"),
        "raid_redundancy": request.form.get("raid_redundancy"),
        "onsite_server": request.form.get("onsite_server"),
        "offsite_backup": request.form.get("offsite_backup"),
        "offsite_location": request.form.get("offsite_location", ""),
        "backup_frequency": request.form.get("backup_frequency"),
        "client_pc_integration": request.form.get("client_pc_integration"),
        "encryption_enabled": request.form.get("encryption_enabled"),
        "access_control": request.form.get("access_control"),
        "last_audit_date": request.form.get("last_audit_date"),
        "notes": request.form.get("notes", ""),
        "date_added": datetime.datetime.now().strftime("%Y-%m-%d")
    }
    
    # Calculate protection score and recommendations
    server = calculate_server_score(server)
    
    # Check if updating existing server
    server_code = request.form.get("server_code")
    if server_code:
        # Update existing
        global servers
        servers = [s for s in servers if s.get("server_code") != server_code]
        servers.append(server)
    else:
        # Add new
        servers.append(server)
    
    save_data(SERVERS_FILE, servers)
    return redirect(url_for("servers_page"))

@app.route("/delete_server/<server_code>")
def delete_server(server_code):
    """Delete a server entry"""
    global servers
    server = next((s for s in servers if s.get("server_code") == server_code), None)
    server_name = server.get("server_name", "Unknown") if server else "Unknown"
    servers = [s for s in servers if s.get("server_code") != server_code]
    save_data(SERVERS_FILE, servers)
    log_activity("DELETE", "SERVER", server_code, f"Deleted server: {server_name}")
    return redirect(url_for("servers_page"))

@app.route("/activity_log")
def activity_log_page():
    """Display comprehensive activity log - read-only"""
    # Sort by timestamp descending (newest first)
    sorted_log = sorted(activity_log, key=lambda x: x.get("timestamp", ""), reverse=True)
    
    # Filter options
    filter_action = request.args.get("filter_action")
    filter_entity = request.args.get("filter_entity")
    
    if filter_action:
        sorted_log = [log for log in sorted_log if log.get("action") == filter_action]
    if filter_entity:
        sorted_log = [log for log in sorted_log if log.get("entity_type") == filter_entity]
    
    # Get unique actions and entities for filter dropdowns
    unique_actions = sorted(set(log.get("action") for log in activity_log if log.get("action")))
    unique_entities = sorted(set(log.get("entity_type") for log in activity_log if log.get("entity_type")))
    
    return render_template(
        "activity_log.html",
        logs=sorted_log,
        total_logs=len(activity_log),
        unique_actions=unique_actions,
        unique_entities=unique_entities,
        filter_action=filter_action,
        filter_entity=filter_entity
    )

if __name__ == "__main__":
    app.run(debug=True)
