import json
import trimesh
import uuid
import os
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, session, make_response, send_file, abort, jsonify
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
from .models import db, User, PrintFile, SystemConfig
from .tasks import merge_and_slice_task
import os
import uuid
import configparser
from datetime import datetime
from .tasks import slice_stl_task

main_bp = Blueprint('main', __name__)

# def get_quality_presets():
#     preset_dir = os.path.join(current_app.root_path, '..', 'print_config', 'presets', 'creality', 'base')
#     presets = []
#     if os.path.exists(preset_dir):
#         for f in os.listdir(preset_dir):
#             if f.startswith('base_global_') and f.endswith('.inst.cfg'):
#                 config = configparser.ConfigParser()
#                 try:
#                     config.read(os.path.join(preset_dir, f))
#                     name = config.get('general', 'name', fallback=f)
#                     presets.append((f, name))
#                 except Exception as e:
#                     pass
#     # Custom sort order or alphanumeric
#     return sorted(presets, key=lambda x: x[1])

auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')

# Guest User Middleware
@main_bp.before_app_request
def assign_guest_cookie():
    if not current_user.is_authenticated:
        guest_id = request.cookies.get('guest_id')
        if not guest_id:
            guest_id = str(uuid.uuid4())
            user = User(username=f'guest_{guest_id[:8]}', is_guest=True, guest_cookie_id=guest_id)
            db.session.add(user)
            db.session.commit()
            login_user(user)
            # We will set the cookie in the response after request, see below
            request.guest_id_to_set = guest_id
        else:
            user = User.query.filter_by(guest_cookie_id=guest_id).first()
            if user:
                login_user(user)

@main_bp.after_app_request
def set_guest_cookie(response):
    if hasattr(request, 'guest_id_to_set'):
        response.set_cookie('guest_id', request.guest_id_to_set, max_age=60*60*24*365) # 1 year
    return response

# --- Main Routes ---

@main_bp.route('/')
def index():
    return render_template('index.html')

@main_bp.route('/set_language/<lang>')
def set_language(lang):
    from app import i18n_dict
    if lang not in i18n_dict:
        lang = 'en'
    # return to previous page
    response = make_response(redirect(request.referrer or url_for('main.index')))
    response.set_cookie('lang', lang, max_age=60*60*24*365)
    return response

@main_bp.route('/files', methods=['GET', 'POST'])
@login_required
def files():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('No file part', 'danger')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('No selected file', 'danger')
            return redirect(request.url)
        if file and file.filename.lower().endswith('.stl'):
            original_filename = file.filename # Do not use secure_filename to keep Chinese characters
            ext = os.path.splitext(original_filename)[1].lower()
            if not ext:
                ext = '.stl'
            timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
            unique_filename = f"{timestamp}_{uuid.uuid4().hex}{ext}"
            filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
            file.save(filepath)
            
            try:
                mesh = trimesh.load(filepath)
                # Check for overlapping faces or if the mesh is not watertight
                # which can cause issues in CuraEngine
                needs_repair = False
                if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
                    needs_repair = True
                
                if needs_repair:
                    # Attempt automatic repair
                    import trimesh.repair
                    trimesh.repair.fix_normals(mesh)
                    trimesh.repair.fix_inversion(mesh)
                    trimesh.repair.fix_winding(mesh)
                    trimesh.repair.fill_holes(mesh)
                    
                    # Re-check after repair
                    if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
                        if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                            return jsonify({'success': False, 'error': 'Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually before slicing.'}), 400
                        else:
                            flash('Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually.', 'danger')
                            os.remove(filepath)
                            return redirect(request.url)
                    else:
                        # Repair succeeded, rewrite file
                        mesh.export(filepath)
            except Exception as e:
                pass
            
            print_file = PrintFile(
                filename=unique_filename,
                original_filename=original_filename,
                file_type='stl',
                user_id=current_user.id,
                status='uploaded' # Only display as uploaded, no automatic slicing
            )
            db.session.add(print_file)
            db.session.commit()
            
            flash('File uploaded successfully!', 'success')
            return redirect(url_for('main.files'))

    # Order by newest first
    user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all()
    return render_template('files.html', files=user_files)

@main_bp.route('/api/files_status')
@login_required
def files_status():
    files = PrintFile.query.filter_by(user_id=current_user.id).all()
    return jsonify({str(f.id): f.status for f in files})

@main_bp.route('/download/<int:file_id>')
@login_required
def download_gcode(file_id):
    print_file = PrintFile.query.get_or_404(file_id)
    if print_file.user_id != current_user.id and not current_user.is_admin:
        abort(403)
    if print_file.status != 'sliced':
        flash('File is not ready yet.', 'warning')
        return redirect(url_for('main.files'))
    
    gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
    filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
    
    if os.path.exists(filepath):
        safe_name = print_file.original_filename.rsplit('.', 1)[0] + '.gcode'
        return send_file(filepath, as_attachment=True, download_name=safe_name)
    flash('GCode file not found. It might have been deleted.', 'danger')
    return redirect(url_for('main.files'))

@main_bp.route('/preview_gcode/<int:file_id>')
@login_required
def preview_gcode(file_id):
    print_file = PrintFile.query.get_or_404(file_id)
    if print_file.user_id != current_user.id and not current_user.is_admin:
        abort(403)
    
    gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
    filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
    
    content = "File not found or not ready."
    line_count = 0
    if os.path.exists(filepath):
        with open(filepath, 'r') as f:
            lines = f.readlines()
            line_count = len(lines)
            content = "".join(lines[:500]) # Preview first 500 lines
            if line_count > 500:
                content += f"\n... \n[Preview truncated. Total lines: {line_count}. Please download to view full file.]"
                
    return render_template('gcode_preview.html', file=print_file, content=content, line_count=line_count)

@main_bp.route('/delete_file/<int:file_id>', methods=['POST'])
@login_required
def delete_file(file_id):
    print_file = PrintFile.query.get_or_404(file_id)
    if print_file.user_id != current_user.id and not current_user.is_admin:
        abort(403)
        
    stl_path = os.path.join(current_app.config['UPLOAD_FOLDER'], print_file.filename)
    gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
    gcode_path = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
    
    if os.path.exists(stl_path):
        os.remove(stl_path)
    if os.path.exists(gcode_path):
        os.remove(gcode_path)
        
    db.session.delete(print_file)
    db.session.commit()
    flash(f"Deleted {print_file.original_filename} successfully.", 'success')
    return redirect(url_for('main.files'))

# --- Auth Routes ---

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        user = User.query.filter_by(username=username, is_guest=False).first()
        if user and check_password_hash(user.password_hash, password):
            login_user(user)
            return redirect(url_for('main.index'))
        flash('Invalid username or password', 'danger')
    return render_template('login.html')

@auth_bp.route('/logout')
@login_required
def logout():
    logout_user()
    response = make_response(redirect(url_for('main.index')))
    response.delete_cookie('guest_id') # Optionally clear guest cookie
    return response

# --- Admin Routes ---

@admin_bp.before_request
def require_admin():
    if not current_user.is_authenticated or not current_user.is_admin:
        flash('Admin access required', 'danger')
        return redirect(url_for('main.index'))

@admin_bp.route('/settings', methods=['GET', 'POST'])
def settings():
    if request.method == 'POST':
        # concurrent_slices = request.form.get('concurrent_slices')
        offset_x = request.form.get('offset_x', '0')
        offset_y = request.form.get('offset_y', '0')
        
        # update or create config entries
        for key, val in [('offset_x', offset_x), ('offset_y', offset_y)]:
            conf = SystemConfig.query.filter_by(key=key).first()
            if not conf:
                conf = SystemConfig(key=key)
                db.session.add(conf)
            conf.value = val
        db.session.commit()
        flash('Settings updated successfully', 'success')
        return redirect(url_for('admin.settings'))
        
    configs = {c.key: c.value for c in SystemConfig.query.all()}
    return render_template('admin_settings.html', configs=configs)

@admin_bp.route('/users')
def users():
    all_users = User.query.order_by(User.created_at.desc()).all()
    return render_template('admin_users.html', users=all_users)


def get_bed_dimensions():
    try:
        from flask import current_app
        path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
        with open(path, 'r', encoding='utf-8') as f:
            data = json.load(f)
            w = data['overrides']['machine_width']['default_value']
            h = data['overrides']['machine_depth']['default_value']
            hd = data['overrides']['machine_height']['default_value']
            return w, h, hd
    except:
        return 200, 200, 200

def get_quality_presets():
    try:
        from flask import current_app
        path = os.path.join(current_app.root_path, '..', 'print_config', 'presets', 'creality', 'base')
        files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
        presets = []
        for f in files:
            name = f.replace('.inst.cfg', '').replace('base_', '').replace('_', ' ')
            presets.append((f, name))
        presets.sort(key=lambda x: x[1])
        return presets
    except:
        return []

@main_bp.route('/plater')
@login_required
def plater():
    w, h, hd = get_bed_dimensions()
    presets = get_quality_presets()
    
    # get offset configs
    conf_x = SystemConfig.query.filter_by(key='offset_x').first()
    conf_y = SystemConfig.query.filter_by(key='offset_y').first()
    offset_x = float(conf_x.value) if conf_x else 0.0
    offset_y = float(conf_y.value) if conf_y else 0.0
    
    last_quality = request.cookies.get('last_quality_preset', 'base_global_standard.inst.cfg')
    user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all()
    models = [{'id': f.id, 'name': f.original_filename, 'status': f.status, 'url': url_for('main.serve_proxy_file', file_id=f.id), 'transform_matrix': f.transform_matrix} for f in user_files]
    return render_template('plater.html', w=w, h=h, hd=hd, presets=presets, last_quality=last_quality, models=models, offset_x=offset_x, offset_y=offset_y)

@main_bp.route('/file/<int:file_id>')
@login_required
def serve_file(file_id):
    f = PrintFile.query.get_or_404(file_id)
    if f.user_id != current_user.id and not current_user.is_admin:
        abort(403)
    path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
    return send_file(path)

@main_bp.route('/proxy/<int:file_id>')
@login_required
def serve_proxy_file(file_id):
    f = PrintFile.query.get_or_404(file_id)
    if f.user_id != current_user.id and not current_user.is_admin:
        abort(403)
    path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
    proxy_path = path + '.proxy.stl'
    if not os.path.exists(proxy_path):
        from stl_simplifier import simplify_stl
        try:
            simplify_stl(path, proxy_path, keep_ratio=0.1) # compress to 10%
        except:
            return send_file(path) # fallback to original if error
    if os.path.exists(proxy_path):
        return send_file(proxy_path)
    return send_file(path)

@main_bp.route('/api/merge_and_slice', methods=['POST'])
@login_required
def merge_and_slice():
    data = request.json
    pieces = data.get('pieces', [])
    quality = data.get('quality', 'base_global_standard.inst.cfg')
    infill_density = data.get('infill', '20')
    support_enable = data.get('support', 'false')
    support_pattern = data.get('support_pattern', 'lines')
    
    if not pieces:
        return jsonify({'error': 'No pieces provided'}), 400
        
    inputs = []
    # Build a combined name
    names = []
    for p in pieces[:3]: # Cap names at 3 to avoid super long string
        f = PrintFile.query.get(p['file_id'])
        if f and (f.user_id == current_user.id or current_user.is_admin):
            names.append(f.original_filename.replace('.stl', ''))
            
    combined_name = ", ".join(names)
    if len(pieces) > 3:
        combined_name += "等合并切片"
    else:
        combined_name += "合并切片"
        
    for p in pieces:
        f = PrintFile.query.get(p['file_id'])
        if f and (f.user_id == current_user.id or current_user.is_admin):
            path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
            inputs.append((path, p['matrix']))
            if 'raw_matrix' in p:
                f.transform_matrix = json.dumps(p['raw_matrix'])
                db.session.add(f)

    db.session.commit()

    if len(inputs) == 0:
        return jsonify({'error': 'Invalid files'}), 400

    if len(inputs) == 1:
        # User is just generating gcode for a single original model, do NOT pollute list with new STL
        target_file_id = pieces[0]['file_id']
        print_file = PrintFile.query.get(target_file_id)
        if not print_file:
            return jsonify({'error': 'Original file not found'}), 404
        print_file.status = 'merging'
        db.session.commit()
        
        # We still need to apply transforms to a temporary STL to generate correct GCode
        temp_filename = f"temp_{uuid.uuid4().hex}.stl"
        temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename)
        
        merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=True)
    else:
        # Multiple models, create a new "Merged Slice" PrintFile entry to keep track of combination
        timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
        unique_filename = f"{timestamp}_{uuid.uuid4().hex}.stl"
        merged_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
        
        print_file = PrintFile(
            filename=unique_filename,
            original_filename=f"{combined_name}.stl",
            file_type='stl',
            user_id=current_user.id,
            status='merging'
        )
        db.session.add(print_file)
        db.session.commit()
        
        merge_and_slice_task(print_file.id, inputs, merged_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=False)

    return jsonify({'success': True, 'message': 'Plater slice queued!'})

