import http.server
import socketserver
import json
import os
import sys
import socket
import tempfile
import shutil
import threading
import time
import webbrowser
from datetime import datetime, timedelta

DEFAULT_PORT = int(os.environ.get('PORT', 5155))
FALLBACK_PORTS = [15155, 25155, 8080]
DIRECTORY = os.path.dirname(os.path.abspath(__file__))
EQUIPMENT_PATH = os.path.join(DIRECTORY, 'data', 'equipment.json')
MEMBERS_PATH = os.path.join(DIRECTORY, 'data', 'members.json')
HISTORY_PATH = os.path.join(DIRECTORY, 'data', 'history.json')
BACKUP_DIR = os.path.join(DIRECTORY, 'data', 'backups')
ACTIVE_PORT_FILE = os.path.join(DIRECTORY, 'data', 'active_port.txt')

# Thread lock for safe file access (RLock allows re-entrant locking to prevent deadlock)
file_lock = threading.RLock()


# =============================================================================
# Utility Functions
# =============================================================================

def parse_dt(date_str):
    if not date_str or not isinstance(date_str, str):
        return None
    try:
        now = datetime.now()
        dt = datetime.strptime(date_str.strip(), "%m/%d")
        # Smart year logic: If today is Dec and dt is Jan, assume next year.
        # If today is Jan and dt is Dec, assume last year (though rare for projects).
        # Otherwise, use current year.
        year = now.year
        if now.month == 12 and dt.month == 1:
            year += 1
        elif now.month == 1 and dt.month == 12:
            year -= 1
        return dt.replace(year=year)
    except ValueError:
        return None


def atomic_write_json(filepath, data):
    """Write JSON data to a file safely using atomic write (temp file + rename).
    This prevents file corruption if the process is interrupted mid-write."""
    dir_name = os.path.dirname(filepath)
    tmp_path = None
    try:
        fd, tmp_path = tempfile.mkstemp(suffix='.tmp', dir=dir_name)
        with os.fdopen(fd, 'w', encoding='utf-8') as tmp_f:
            json.dump(data, tmp_f, ensure_ascii=False, indent=2)
        # On Windows, os.replace atomically replaces the target file
        os.replace(tmp_path, filepath)
    except Exception as e:
        # Clean up temp file on failure
        if tmp_path and os.path.exists(tmp_path):
            try:
                os.remove(tmp_path)
            except Exception:
                pass
        raise e


def record_history(project_data):
    """Append a completed project to history.json"""
    with file_lock:
        history = []
        if os.path.exists(HISTORY_PATH):
            try:
                with open(HISTORY_PATH, 'r', encoding='utf-8') as f:
                    history = json.load(f)
            except: history = []
        
        # Add timestamp of archive
        project_data["archived_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        history.append(project_data)
        
        atomic_write_json(HISTORY_PATH, history)


# =============================================================================
# Auto Backup
# =============================================================================

def create_backup(label=""):
    """Create a timestamped backup of equipment.json"""
    if not os.path.exists(EQUIPMENT_PATH):
        return
    os.makedirs(BACKUP_DIR, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    suffix = f"_{label}" if label else ""
    backup_name = f"equipment_{timestamp}{suffix}.json"
    backup_path = os.path.join(BACKUP_DIR, backup_name)
    try:
        shutil.copy2(EQUIPMENT_PATH, backup_path)
        print(f"[Backup] Created: {backup_name}")
        cleanup_old_backups()
    except Exception as e:
        print(f"[Backup] Failed: {e}")


def cleanup_old_backups(max_backups=30):
    """Keep only the most recent N backup files to prevent disk bloat."""
    if not os.path.exists(BACKUP_DIR):
        return
    backups = sorted([
        f for f in os.listdir(BACKUP_DIR) if f.startswith("equipment_") and f.endswith(".json")
    ])
    while len(backups) > max_backups:
        oldest = backups.pop(0)
        os.remove(os.path.join(BACKUP_DIR, oldest))
        print(f"[Backup] Cleaned old: {oldest}")


# =============================================================================
# Background Automation Thread
# =============================================================================

def run_automation_logic():
    """Run the schedule shift & status automation on equipment data.
    This is called by the background thread, NOT by GET requests."""
    with file_lock:
        try:
            if not os.path.exists(EQUIPMENT_PATH):
                return
            with open(EQUIPMENT_PATH, 'r', encoding='utf-8') as f:
                data = json.load(f)
        except Exception as e:
            print(f"[Automation] Failed to read data: {e}")
            return

        today = datetime.now()
        modified = False

        for eq in data:
            curr = eq.setdefault("current", { "type": "ETC", "project": "", "item": "", "engineer": "", "start": "", "end": "" })
            nxt = eq.setdefault("next", { "type": "ETC", "project": "", "item": "", "engineer": "", "start": "", "end": "" })
            
            # 1. Intelligent Shift & Gap Management
            c_start_dt = parse_dt(curr.get("start"))
            c_end_dt = parse_dt(curr.get("end"))
            n_start_dt = parse_dt(nxt.get("start"))
            
            # CASE A: Current project has ended -> Archive and shift
            if c_end_dt and today.date() > c_end_dt.date():
                # Only record real projects to history (skip automated waiting labels)
                if "waiting for test" not in curr.get("project", "").lower():
                    record_history({
                        "eq_id": eq["id"], "eq_name": eq["name"],
                        "type": curr.get("type", "ETC"), "project": curr["project"], "item": curr["item"], "engineer": curr["engineer"],
                        "start": curr["start"], "end": curr["end"]
                    })
                eq["previous"] = curr.copy()
                eq["current"] = { "type": "ETC", "project": "STAND BY: waiting for test", "item": "waiting for test", "engineer": "-", "start": (c_end_dt + timedelta(days=1)).strftime("%m/%d"), "end": "" }
                modified = True
                curr = eq["current"]
            
            # CASE B: Current project starts in the future -> Push back to Next, set Current to Waiting
            elif c_start_dt and today.date() < c_start_dt.date():
                if curr.get("project") != "STOP: waiting for test" and not curr.get("project").startswith("STAND BY:"):
                    # Only push to Next if Next is empty or just a standby placeholder
                    if not nxt.get("project") or nxt.get("project").startswith("STAND BY:"):
                        eq["next"] = curr.copy()
                    prev_end_dt = parse_dt(eq.get("previous", {}).get("end"))
                    w_start = (prev_end_dt + timedelta(days=1)).strftime("%m/%d") if prev_end_dt else today.strftime("%m/%d")
                    eq["current"] = {
                        "type": "ETC", "project": "STAND BY: waiting for test", "item": "waiting for test", "engineer": "-",
                        "start": w_start, "end": (c_start_dt - timedelta(days=1)).strftime("%m/%d")
                    }
                    modified = True
                    curr = eq["current"]
                    nxt = eq.get("next", nxt)

            # CASE C: Time to start the Next project?
            if nxt.get("project") and n_start_dt and today.date() >= n_start_dt.date():
                eq["current"] = nxt.copy()
                eq["next"] = { "type": "ETC", "project": "", "item": "", "engineer": "", "start": "", "end": "" }
                modified = True
                curr = eq["current"]
                nxt = eq["next"]

            # 2. Status Automation Logic
            target_status = "STOP"
            c_proj = curr.get("project", "")
            is_idle = not c_proj or "waiting for test" in c_proj.lower()
            
            if not is_idle:
                s_dt = parse_dt(curr.get("start"))
                e_dt = parse_dt(curr.get("end"))
                if s_dt and e_dt and s_dt.date() <= today.date() <= e_dt.date():
                    target_status = "RUN"
                else:
                    target_status = "STAND BY"
            elif nxt.get("project"):
                target_status = "STAND BY"
            
            if eq.get("status") != target_status:
                eq["status"] = target_status
                modified = True
            
            # CASE E: If truly idle and status is STOP, ensure label is STAND BY (user request)
            if is_idle and target_status == "STOP":
                if curr.get("project") != "STAND BY: waiting for test":
                    curr["project"] = "STAND BY: waiting for test"
                    curr["item"] = "waiting for test"
                    modified = True

        # Save if mutated
        if modified:
            try:
                atomic_write_json(EQUIPMENT_PATH, data)
                print(f"[Automation] Data updated at {today.strftime('%H:%M:%S')}")
            except Exception as e:
                print(f"[Automation] Failed to save: {e}")


def automation_thread_worker():
    """Background thread: runs automation every 60 seconds and daily backup at midnight."""
    last_backup_date = None
    print("[Thread] Background automation started (interval: 60s)")
    
    while True:
        try:
            # Run automation logic
            run_automation_logic()
            
            # Daily auto-backup at midnight (once per day)
            today_date = datetime.now().date()
            if last_backup_date != today_date:
                create_backup("daily")
                last_backup_date = today_date
                
        except Exception as e:
            print(f"[Thread] Automation error: {e}")
        
        time.sleep(60)  # Check every 60 seconds


# =============================================================================
# HTTP Request Handler
# =============================================================================

class APIHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=DIRECTORY, **kwargs)
        
    def end_headers(self):
        self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
        self.send_header('Pragma', 'no-cache')
        super().end_headers()

    def do_GET(self):
        # Serve the JSON file dynamically from /api/data
        if self.path.startswith('/api/data'):
            try:
                with file_lock:
                    with open(EQUIPMENT_PATH, 'r', encoding='utf-8') as f:
                        data = json.load(f)

                self.send_response(200)
                self.send_header('Content-type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
            except Exception as e:
                self.send_error(500, f"Error reading data: {e}")
        elif '/api/history' in self.path:
            try:
                data = []
                with file_lock:
                    if os.path.exists(HISTORY_PATH):
                        with open(HISTORY_PATH, 'r', encoding='utf-8') as f:
                            content = f.read().strip()
                            if content:
                                data = json.loads(content)
                
                self.send_response(200)
                self.send_header('Content-type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
            except Exception as e:
                print(f"History Fetch Error: {e}")
                self.send_error(500, f"Error reading history: {e}")
        elif self.path.startswith('/api/members'):
            try:
                data = []
                with file_lock:
                    if os.path.exists(MEMBERS_PATH):
                        with open(MEMBERS_PATH, 'r', encoding='utf-8') as f:
                            data = json.load(f)

                self.send_response(200)
                self.send_header('Content-type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
            except Exception as e:
                print(f"Members Fetch Error: {e}")
                self.send_error(500, f"Error reading members: {e}")
        else:
            super().do_GET()

    def do_POST(self):
        if self.path == '/api/data':
            try:
                content_length = int(self.headers['Content-Length'])
                post_data = self.rfile.read(content_length)
                
                # Parse to ensure it's valid JSON
                json_data = json.loads(post_data.decode('utf-8'))
                
                # Save to file with atomic write and lock
                with file_lock:
                    atomic_write_json(EQUIPMENT_PATH, json_data)
                
                self.send_response(200)
                self.send_header('Content-type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({"status": "success"}).encode('utf-8'))
            except Exception as e:
                self.send_response(500)
                self.end_headers()
                self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
        elif self.path == '/api/members':
            try:
                content_length = int(self.headers['Content-Length'])
                post_data = self.rfile.read(content_length)
                
                json_data = json.loads(post_data.decode('utf-8'))
                
                with file_lock:
                    atomic_write_json(MEMBERS_PATH, json_data)
                
                self.send_response(200)
                self.send_header('Content-type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({"status": "success"}).encode('utf-8'))
            except Exception as e:
                self.send_response(500)
                self.end_headers()
                self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
        else:
            self.send_error(404, "Not Found")


class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    allow_reuse_address = True
    daemon_threads = True


def create_server():
    """Try preferred port first; if blocked by Windows (e.g. WinError 10013 in dynamic exclusion range),
    automatically fall back to safe ports outside 1024-15001."""
    ports_to_try = [DEFAULT_PORT] + [p for p in FALLBACK_PORTS if p != DEFAULT_PORT]
    for p in ports_to_try:
        try:
            httpd = ReusableTCPServer(("", p), APIHandler)
            return httpd, p
        except PermissionError as pe:
            print(f"[Port Warning] Port {p} is excluded/blocked by Windows (WinError 10013): {pe}. Trying next port...")
        except OSError as oe:
            print(f"[Port Warning] Port {p} could not be bound: {oe}. Trying next port...")
    raise RuntimeError(f"Could not bind to any candidate ports: {ports_to_try}")


if __name__ == '__main__':
    # 1. Create startup backup
    create_backup("startup")
    
    # 2. Start background automation thread
    automation = threading.Thread(target=automation_thread_worker, daemon=True)
    automation.start()
    
    # 3. Start HTTP server
    httpd, active_port = create_server()
    
    # Save active port so batch files and scripts can read it
    try:
        os.makedirs(os.path.dirname(ACTIVE_PORT_FILE), exist_ok=True)
        with open(ACTIVE_PORT_FILE, 'w', encoding='utf-8') as f:
            f.write(str(active_port))
    except Exception as e:
        print(f"[Port] Could not write active port file: {e}")

    dashboard_url = f"http://localhost:{active_port}/Testlab_status.html"
    print("=" * 60)
    print(f"APTIV TEST LAB STATUS SERVER RUNNING")
    print(f"URL: {dashboard_url}")
    print(f"Backups stored in: {BACKUP_DIR}")
    print("=" * 60)

    if '--open' in sys.argv:
        def open_browser():
            time.sleep(0.5)
            webbrowser.open(dashboard_url)
        threading.Thread(target=open_browser, daemon=True).start()

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n[Server] Shutting down gracefully...")
        httpd.shutdown()
        httpd.server_close()
