from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app, Response
from flask_login import login_required, current_user
from websockets.sync.client import connect as ws_connect
import websockets.exceptions
import threading
import requests
from urllib.parse import urlparse
from app.models import SystemConfig, db
from app.utils.octoprint_client import OctoPrintClient
from app.models import PrintFile
import os

printer_bp = Blueprint('printer', __name__, url_prefix='/printer')

def get_octo_client():
    url = SystemConfig.query.filter_by(key='octoprint_url').first()
    apikey = SystemConfig.query.filter_by(key='octoprint_apikey').first()
    if url and url.value and apikey and apikey.value:
        return OctoPrintClient(url.value, apikey.value)
    return None

@printer_bp.route('/status')
@login_required
def status():
    client = get_octo_client()
    status_data = None
    job_data = None
    error = None
    if client:
        try:
            status_data = client.get_printer_status()
            job_data = client.get_job_info()
        except Exception as e:
            error = str(e)
    else:
        error = "OctoPrint is not configured."
        
    return render_template('printer/status.html', status=status_data, job=job_data, error=error)

def get_gcode_dir():
    conf = SystemConfig.query.filter_by(key='gcode_upload_folder').first()
    if conf and conf.value and os.path.exists(conf.value):
        return conf.value
    return current_app.config['UPLOAD_FOLDER']

@printer_bp.route('/prepare')
@login_required
def prepare():
    from app.models import PrintFile
    import os
    
    # Query only the sliced GCode files belonging to the current user
    user_files = PrintFile.query.filter_by(user_id=current_user.id, status='sliced').order_by(PrintFile.created_at.desc()).all()
    
    files = []
    gcode_dir = get_gcode_dir()
    
    client = get_octo_client()
    octo_files_dict = {}
    if client:
        try:
            octo_resp = client.get_files()
            for item in octo_resp.get('files', []):
                octo_files_dict[item.get('name')] = item
        except Exception as e:
            pass
            
    for f in user_files:
        gcode_filename = f.filename.rsplit('.', 1)[0] + '.gcode'
        gcode_path = os.path.join(gcode_dir, gcode_filename)
        
        size = 0
        if os.path.exists(gcode_path):
            size = os.path.getsize(gcode_path)
            
        # Upload to OctoPrint if not found but exists locally
        if client and gcode_filename not in octo_files_dict and size > 0:
            try:
                resp = client.upload_file('local', gcode_path, gcode_filename)
                uploaded_loc = resp.get('files', {}).get('local', {})
                if gcode_filename in uploaded_loc:
                    octo_files_dict[gcode_filename] = uploaded_loc[gcode_filename]
            except Exception as e:
                pass
                
        octo_info = octo_files_dict.get(gcode_filename, {})
        analysis = octo_info.get('gcodeAnalysis', None)
        
        files.append({
            'id': f.id,
            'name': f.original_filename.rsplit('.', 1)[0] + '.gcode',
            'type': 'machinecode',
            'size': size,
            'origin': 'local',
            'path': gcode_filename,
            'gcodeAnalysis': analysis
        })
        
    error = None
    if not get_octo_client():
        error = "OctoPrint is not configured."
        
    return render_template('printer/prepare.html', files=files, error=error)

@printer_bp.route('/api/print_file', methods=['POST'])
@login_required
def api_print_file():
    path = request.json.get('path')
    location = request.json.get('origin', 'local')
    client = get_octo_client()
    if client and path:
        try:
            client.select_file(location, path, print_after_select=True)
            return jsonify({"success": True})
        except Exception as e:
            return jsonify({"success": False, "error": str(e)})
    return jsonify({"success": False, "error": "Not configured or missing path"})

@printer_bp.route('/control')
@login_required
def control():
    client = get_octo_client()
    webcam_url = None
    error = None
    if client:
        try:
            webcam_url = client.get_webcam_stream_url()
        except Exception as e:
            error = str(e)
    else:
        error = "OctoPrint is not configured."
    return render_template('printer/control.html', webcam_url=webcam_url, error=error)

@printer_bp.route('/api/command', methods=['POST'])
@login_required
def api_command():
    cmd = request.json.get('command')
    client = get_octo_client()
    if client and cmd:
        try:
            if cmd == 'home':
                client.home_axes()
            elif cmd == 'pause':
                client.pause_print()
            elif cmd == 'cancel':
                client.cancel_print()
            return jsonify({"success": True})
        except Exception as e:
            return jsonify({"success": False, "error": str(e)})
    return jsonify({"success": False, "error": "Invalid client or command"})

@printer_bp.route('/api/upload_gcode', methods=['POST'])
@login_required
def upload_gcode():
    from app.models import PrintFile
    import os
    import uuid
    from werkzeug.utils import secure_filename
    
    if 'file' not in request.files:
        return jsonify({"success": False, "error": "No file part"}), 400
        
    file = request.files['file']
    if file.filename == '':
        return jsonify({"success": False, "error": "No selected file"}), 400
        
    if not file.filename.lower().endswith(('.gcode', '.gco', '.g')):
        return jsonify({"success": False, "error": "Only standard .gcode files are supported."}), 400
        
    sec_name = secure_filename(file.filename)
    random_prefix = uuid.uuid4().hex[:8]
    
    # We save pseudo-STL filename so that our conventional parser works (replaces .ext with .gcode)
    # i.e., "some_file.gcode" -> pseudo .stl tracker
    pseudo_stl_filename = f"{random_prefix}_{sec_name.rsplit('.', 1)[0]}.stl"
    gcode_filename = f"{random_prefix}_{sec_name.rsplit('.', 1)[0]}.gcode"
    
    gcode_path = os.path.join(get_gcode_dir(), gcode_filename)
    file.save(gcode_path)
    
    print_file = PrintFile(
        user_id=current_user.id,
        original_filename=file.filename, # keep original GCode name
        filename=pseudo_stl_filename,
        file_type='stl',
        status='sliced'
    )
    db.session.add(print_file)
    db.session.commit()
    
    client = get_octo_client()
    if client:
        try:
            client.upload_file('local', gcode_path, gcode_filename)
        except Exception:
            pass
            
    return jsonify({"success": True})

@printer_bp.route('/octo_config', methods=['GET', 'POST'])
@login_required
def octo_config():
    if not current_user.is_admin:
        flash("Admin access required", "danger")
        return redirect(url_for('printer.status'))
        
    if request.method == 'POST':
        url = request.form.get('octoprint_url', '')
        apikey = request.form.get('octoprint_apikey', '')
        
        conf_url = SystemConfig.query.filter_by(key='octoprint_url').first()
        if not conf_url:
            conf_url = SystemConfig(key='octoprint_url')
            db.session.add(conf_url)
        conf_url.value = url.rstrip('/')
        
        conf_key = SystemConfig.query.filter_by(key='octoprint_apikey').first()
        if not conf_key:
            conf_key = SystemConfig(key='octoprint_apikey')
            db.session.add(conf_key)
        conf_key.value = apikey
        
        db.session.commit()
        if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return jsonify({'success': True}), 200
        flash("OctoPrint settings updated", "success")
        return redirect(url_for('printer.octo_config'))
        
    configs = {c.key: c.value for c in SystemConfig.query.all()}
    return render_template('printer/octo_config.html', configs=configs)

@printer_bp.route('/octo_embed')
@login_required
def octo_embed():
    if not current_user.is_admin:
        flash("Admin access required", "danger")
        return redirect(url_for('printer.status'))
        
    url = SystemConfig.query.filter_by(key='octoprint_url').first()
    embed_url = url_for('printer.octo_proxy') if url and url.value else None
    return render_template('printer/octo_embed.html', embed_url=embed_url)

@printer_bp.route('/proxy', defaults={'path': ''}, websocket=True, strict_slashes=False)
@printer_bp.route('/proxy/<path:path>', websocket=True)
@printer_bp.route('/proxy', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], strict_slashes=False)
@printer_bp.route('/proxy/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@login_required
def octo_proxy(path):
    if not current_user.is_admin:
        return "Unauthorized", 403

    url_config = SystemConfig.query.filter_by(key='octoprint_url').first()
    if not url_config or not url_config.value:
        return "OctoPrint URL not configured", 404

    base_url = url_config.value.rstrip('/')
    
    # print("----- REQUEST HEADERS -----")
    # for k, v in request.headers:
    #     print(f"{k}: {v}")
    # print("----- END REQUEST HEADERS -----")

    # --- WebSocket Proxy Logic ---
    if request.headers.get('Upgrade', '').lower() == 'websocket':
        from flask_sock import Server, ConnectionClosed
        
        # Check if environment supports WebSockets
        try:
            ws = Server(request.environ)
        except Exception as e:
            env_keys = sorted(list(request.environ.keys()))
            print(f"FAILED. ENV KEYS: {env_keys}")
            return f"WebSocket Upgrade Failed: {str(e)}", 400
            
        def handle_ws():
            if base_url.startswith('https://'):
                ws_base = base_url.replace('https://', 'wss://', 1)
            else:
                ws_base = base_url.replace('http://', 'ws://', 1)
                
            target_url = f"{ws_base}/{path}"
            if request.query_string:
                target_url = f"{target_url}?{request.query_string.decode('utf-8')}"
                print(f"WS Proxy Query String: {request.query_string.decode('utf-8')}")
            # Copy most headers, especially Origin to pass CORS
            ws_headers = {}
            for k, v in request.headers:
                if k.lower() not in ['host', 'connection', 'upgrade', 'sec-websocket-key', 'sec-websocket-version', 'sec-websocket-extensions', 'content-length']:
                    ws_headers[k] = v
            
            # Match Tornado's expectations for Origin to avoid 400 Bad Request
            parsed_base = urlparse(base_url)
            ws_headers['Host'] = parsed_base.netloc
            if 'Origin' in request.headers:
                ws_headers['Origin'] = base_url
            if 'Referer' in request.headers:
                ws_headers['Referer'] = f"{base_url}/{path}"
                
            ws_headers['X-Real-IP'] = request.remote_addr
            forwarded_for = request.headers.get('X-Forwarded-For', '')
            ws_headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
            ws_headers['X-Forwarded-Proto'] = request.scheme
            ws_headers['X-Forwarded-Host'] = request.host
            ws_headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
            ws_headers['X-Script-Name'] = '/printer/proxy'
            
            print(f"WS Proxy Connecting to: {target_url}")
            try:
                remote_ws = ws_connect(target_url, additional_headers=ws_headers)
                print("WS Proxy connected to remote.")
            except Exception as e:
                import traceback
                traceback.print_exc()
                print(f"Remote WS Connection Error: {e}")
                ws.close(1011, str(e))
                return

            def recv_loop():
                print("WS recv_loop started")
                try:
                    for message in remote_ws:
                        ws.send(message)
                except Exception as e:
                    print("WS recv error:", e)
                finally:
                    try: remote_ws.close()
                    except: pass
                    try: ws.close()
                    except: pass
                print("WS recv_loop ended")

            t = threading.Thread(target=recv_loop)
            t.daemon = True
            t.start()

            print("WS Entering client receive loop")
            try:
                while True:
                    data = ws.receive()
                    if data is None:
                        break
                    remote_ws.send(data)
            except Exception as e:
                print("WS send error:", e)
            finally:
                try: remote_ws.close()
                except: pass
                try: ws.close()
                except: pass
            print("WS client loop ended")
        
        try:
            handle_ws()
        except ConnectionClosed:
            print("WS Connection Closed")
        except Exception as e:
            print("WS Error in handle_ws:", e)
        finally:
            try: ws.close() 
            except: pass

        class WebSocketResponse(Response):
            def __call__(self, *args, **kwargs):
                print("WS Response __call__")
                if getattr(ws, 'mode', 'werkzeug') == 'werkzeug':
                    return super().__call__(*args, **kwargs)
                return []
                
        return WebSocketResponse()

    # --- Standard HTTP Proxy Logic ---
    # from urllib.parse import urlparse
    target_url = f"{base_url}/{path}"
    
    if request.query_string:
        target_url = f"{target_url}?{request.query_string.decode('utf-8')}"

    # Build headers for reverse proxy based on nginx config reference
    parsed_base = urlparse(base_url)
    headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'content-length', 'origin', 'referer']}
    
    # print(f"Proxying to: {target_url}")
    # Spoof Host, Origin, and Referer to match the backend URL completely
    # This prevents Tornado's strict Origin vs Host CSRF/CORS validation from failing
    # headers['Host'] = parsed_base.netloc
    headers['Host'] = request.host
    
    if 'Origin' in request.headers:
        headers['Origin'] = base_url
    if 'Referer' in request.headers:
        headers['Referer'] = f"{base_url}/{path}"
    
    headers['X-Real-IP'] = request.remote_addr
    headers['X-Real-Port'] = str(request.environ.get('REMOTE_PORT', ''))
    
    forwarded_for = request.headers.get('X-Forwarded-For', '')
    headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
    
    headers['X-Forwarded-Proto'] = request.scheme
    headers['X-Script-Name'] = "/printer/proxy"
    headers['X-Forwarded-Host'] = request.host
    headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
    headers['REMOTE-HOST'] = request.remote_addr
    
    if request.headers.get('Upgrade'):
        headers['Upgrade'] = request.headers.get('Upgrade')
    if request.headers.get('Connection'):
        headers['Connection'] = request.headers.get('Connection')
        
    # OctoPrint requires an X-CSRF-Token header matching the csrf_token_* cookie for POST/PUT/DELETE
    if request.method not in ['GET', 'HEAD', 'OPTIONS'] and 'X-CSRF-Token' not in request.headers:
        for cookie_name, cookie_value in request.cookies.items():
            if cookie_name.startswith('csrf_token_'):
                headers['X-CSRF-Token'] = cookie_value
                break

    # if path == 'api/login':
    #     print("----- SEND HEADERS -----")
    #     for k, v in headers.items():
    #         if k in request.headers.keys():
    #             print(f"{k} :(from request): {request.headers[k]} (to send): {v}")
    #         else:
    #             print(f"{k} :(from request): None (to send): {v}")
        
    #     for k,v in request.headers.items():
    #         if k not in headers.keys():
    #             print(f"{k} :(from request): {request.headers[k]} (to send): None")
        
    #     print("----- END SEND HEADERS -----")

    try:
        # proxy_connect_timeout 60s, proxy_read_timeout 600s
        resp = requests.request(
            method=request.method,
            url=target_url,
            headers=headers,
            data=request.get_data(),
            cookies=request.cookies,
            allow_redirects=False,
            stream=True,
            timeout=(60, 600)
        )
    except requests.exceptions.RequestException as e:
        return f"Proxy connection error: {str(e)}", 502

    # Strip headers that might break the iframe or framing
    excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'x-frame-options', 'content-security-policy']
    
    # We must use raw headers to prevent requests from joining multiple Set-Cookie headers with a comma
    # Joining Set-Cookie with a comma breaks standard cookie parsing in the browser due to commas in dates
    response_headers = [(name, value) for name, value in resp.raw.headers.items() 
                        if name.lower() not in excluded_headers and name.lower() != 'set-cookie']
    
    for cookie in resp.raw.headers.get_all('set-cookie', []):
        # ensure we preserve the proxy path override
        response_headers.append(('Set-Cookie', cookie))

    def generate():
        for chunk in resp.iter_content(chunk_size=8192):
            if chunk:
                yield chunk

    return Response(generate(), resp.status_code, response_headers)

