# app.py
import os
import logging
from pdf_generator import generate_evaluation_pdf
from datetime import datetime
from flask import Flask, render_template, redirect, url_for, flash, request, abort, jsonify, current_app, send_file
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
from flask_wtf import FlaskForm
from werkzeug.utils import secure_filename 
from dotenv import load_dotenv
from functools import wraps
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError # Import for handling potential database errors
from config import ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, EVALUADO_ROLE_ID, TALENTO_HUMANO_ROLE_ID, DIRECTOR_ROLE_ID, Config
from forms import LoginForm, OdiForm, AssignOdiForm, EditUserForm, SearchUserByCedulaForm, AssignLocationForm, AssignCursosForm, TalentoHumanoObservationForm, ChangePasswordForm
from extensions import db
from models import Localizacion, DivisionesGenerales, Unidades, GerenciasGenerales, Coordinaciones, UbicacionPersonal
 

# Load environment variables from .env file
load_dotenv()

# Initialize the Flask application
app = Flask(__name__)


app.config.from_object(Config)

# --- Flask Configuration ---
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
if not app.config['SECRET_KEY']:
    raise ValueError("No SECRET_KEY set for Flask application. Check your .env file.")


# Initialize extensions
db.init_app(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

# --- Configure logging ---
logging.basicConfig(level=logging.INFO) # Set base logging level
app.logger.setLevel(logging.INFO) # Set Flask app logger level

from models import db, User, Rol, Ano, Trimestre, Odi, Rango, OdiPersonal, UbicacionPersonal, Competencia, CompetenciaPersonal, Escala, Evaluacion, CursoPersonal, Localizacion, DivisionesGenerales, Unidades, GerenciasGenerales, Coordinaciones

# --- User Loader ---
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

# --- Role Decorator ---
def role_required(role_ids):
    """
    A decorator to restrict access to routes based on user roles.
    `role_ids` should be a list of integer role IDs that are allowed to access the route.
    """
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.is_authenticated:
                flash('Debes iniciar sesión para acceder a esta página.', 'warning')
                return redirect(url_for('login'))

            # Check if current_user.id_rol is in the allowed role_ids
            # Ensure current_user.id_rol is not None before checking
            if current_user.id_rol is None or current_user.id_rol not in role_ids:
                flash('No tienes permiso para acceder a esta página.', 'danger')
                return redirect(url_for('dashboard'))

            return f(*args, **kwargs)
        return decorated_function
    return decorator

# --- Routes ---
@app.route('/')
def home():
    if not current_user.is_authenticated:
        # If the user is not authenticated, redirect them to the login page
        return redirect(url_for('login'))
    
    # If the user is authenticated, you can redirect them to their dashboard
    # or render a specific homepage for authenticated users.
    # Assuming 'dashboard' is the primary landing page for authenticated users.
    return redirect(url_for('dashboard'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('dashboard'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(usuario=form.username.data).first()
        if user and user.check_password(form.password.data) and user.active:
            if user.id_rol != ADMIN_ROLE_ID:
                ubicacion = UbicacionPersonal.query.filter_by(id_personal=user.id_personal).first()
                if not ubicacion:
                    flash('No tiene una ubicación asignada. Por favor, contacte con un Gerente/Administrador.', 'danger')
                    return redirect(url_for('login'))
                    
            login_user(user)
            flash('Inicio de sesión exitoso.', 'success')
            next_page = request.args.get('next')
            return redirect(next_page or url_for('dashboard'))
        elif user and not user.active:
            flash('Tu cuenta está inactiva. Por favor, contacta al administrador.', 'danger')
        else:
            flash('Usuario o contraseña incorrectos.', 'danger')
    return render_template('login.html', title='Iniciar Sesión', form=form)

@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('Has cerrado sesión.', 'info')
    return redirect(url_for('home'))

@app.route('/dashboard')
@login_required
def dashboard():
        return render_template('dashboard.html', title='Dashboard') # Generic dashboard

@app.route('/admin_panel')
@login_required
@role_required([ADMIN_ROLE_ID])
def admin_panel():
    # Fetch all personal (users) with their role descriptions
    # We use a join to get the 'descripcion' from the Rol table
    personal_with_roles = db.session.query(User, Rol).\
        join(Rol, User.id_rol == Rol.id_rol).\
        all()

    # Prepare data for the template, including concatenated fields and role description
    users_data = []
    for personal_user, rol_obj in personal_with_roles:
        users_data.append({
            'id_personal': personal_user.id_personal,
            'cedula': personal_user.cedula,
            'nombre': personal_user.nombre,
            'apellido': personal_user.apellido,
            'nacionalidad': personal_user.nacionalidad,
            'estatu': personal_user.estatu, # Boolean status
            'id_rol': personal_user.id_rol,
            'rol_descripcion': rol_obj.descripcion, # Role description from Rol table
            'username': personal_user.usuario, # 'usuario' from personal table for username
        })
    
    return render_template('admin_panel.html', personal=users_data)


@app.route('/manage_odis', methods=['GET', 'POST'])
@login_required
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, DIRECTOR_ROLE_ID])
def manage_odis():
    form = OdiForm()
    
    # --- Fetch ODIs based on Ownership ---
    # Admins can manage everything; others only manage what they created.
    if current_user.id_rol == ADMIN_ROLE_ID:
        all_odis = Odi.query.order_by(Odi.descripcion).all()
    else:
        all_odis = Odi.query.filter_by(id_personal=current_user.id_personal).order_by(Odi.descripcion).all()

    # --- Logic for Adding a new ODI ---
    if form.submit.data and form.validate_on_submit():
        try:
            # We still capture the creator's current location as metadata
            loc = UbicacionPersonal.query.filter_by(id_personal=current_user.id_personal).first()
            
            new_odi = Odi(
                descripcion=form.descripcion.data,
                peso=form.peso.data,
                id_personal=current_user.id_personal # Set the logged-in user as owner
            )
            db.session.add(new_odi)
            db.session.commit()
            flash('¡ODI añadido exitosamente!', 'success')
            return redirect(url_for('manage_odis'))
        except Exception as e:
            db.session.rollback()
            flash(f'Error al añadir ODI: {e}', 'danger')
            app.logger.error(f"Error adding ODI: {e}", exc_info=True)

    # --- Logic for Deleting an ODI ---
    elif request.method == 'POST' and 'delete' in request.form:
        odi_id = request.form.get('odi_id_to_edit', type=int)
        odi_to_delete = Odi.query.get(odi_id)
        
        if odi_to_delete:
            # Authorization check: Admin OR the specific creator/owner
            if current_user.id_rol == ADMIN_ROLE_ID or odi_to_delete.id_personal == current_user.id_personal:
                try:
                    # Prevent deletion if ODI is already linked to a personal evaluation
                    if OdiPersonal.query.filter_by(id_odi=odi_id).first():
                        flash('No se puede eliminar este ODI porque ya está asignado a personal en una evaluación.', 'warning')
                    else:
                        db.session.delete(odi_to_delete)
                        db.session.commit()
                        flash('¡ODI eliminado exitosamente!', 'success')
                except Exception as e:
                    db.session.rollback()
                    flash(f'Error al eliminar ODI: {e}', 'danger')
            else:
                flash('No tienes permiso para eliminar este ODI.', 'danger')
        else:
            flash('ODI no encontrado.', 'danger')
            
        return redirect(url_for('manage_odis'))

    return render_template(
        'manage_odis.html',
        form=form,
        all_odis=all_odis,
    )

# --- New Route for Editing an ODI ---
@app.route('/edit_odi/<int:odi_id>', methods=['GET', 'POST'])
def edit_odi(odi_id):
    odi = Odi.query.get_or_404(odi_id)
    form = OdiForm(obj=odi)
    
    # Check if the ODI has been assigned to anyone
    is_assigned = OdiPersonal.query.filter_by(id_odi=odi_id).first() is not None
    
    if form.validate_on_submit():
        if is_assigned:
            # If assigned, do not update the peso field from the form
            form.descripcion.data = odi.descripcion # Prevents accidental overwrite if description wasn't changed
            form.peso.data = odi.peso # Keep the original peso
            flash('No se puede modificar el peso de un ODI que ya ha sido asignado.', 'danger')
        
        form.populate_obj(odi)
        db.session.commit()
        flash('ODI actualizado exitosamente.', 'success')
        return redirect(url_for('manage_odis'))

    return render_template('edit_odi.html', form=form, odi=odi, is_assigned=is_assigned)

# --- New Route for Deleting an ODI ---
@app.route('/delete_odi/<int:odi_id>', methods=['POST'])
@login_required
@role_required([ADMIN_ROLE_ID, SUPERVISOR_ROLE_ID, DIRECTOR_ROLE_ID])
def delete_odi(odi_id):
    odi = Odi.query.get_or_404(odi_id)
    try:
        # Before deleting an ODI, check if it's assigned to any personal record
        # This prevents breaking foreign key constraints
        if OdiPersonal.query.filter_by(id_odi=odi.id_odi).first():
            flash('No se puede eliminar el ODI porque tiene asignaciones de ODIs personalizadas asociadas.', 'danger')
        else:
            db.session.delete(odi)
            db.session.commit()
            flash('ODI eliminado exitosamente.', 'success')
    except Exception as e:
        db.session.rollback()
        flash(f'Error al eliminar ODI: {e}', 'danger')
        app.logger.error(f"Error deleting ODI {odi_id}: {e}", exc_info=True)
    
    return redirect(url_for('manage_odis'))

# --- Supervisor Page ---
@app.route('/supervisor_page')
@login_required
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, DIRECTOR_ROLE_ID])
def supervisor_page():
    return render_template('supervisor_page.html', title='Página de Supervisor')

def get_evaluado_data(user_id, ano_id, trimestre_id):
    assigned_odis = []
    total_odi_score = 0.0

    if ano_id != -1 and trimestre_id != -1: # Only query if valid IDs are provided
        odi_assignments = OdiPersonal.query.filter_by(id_personal=user_id, id_ano=ano_id, id_trimestre=trimestre_id).\
            join(Odi, OdiPersonal.id_odi == Odi.id_odi).\
            join(Rango, OdiPersonal.id_rango == Rango.id_rango).\
            order_by(OdiPersonal.id_odi).all() 

        for assignment in odi_assignments:
            try:
                rango_value = float(assignment.rango.descripcion)
            except ValueError:
                rango_value = 0.0
            odi_peso = float(assignment.odi.peso)

            assigned_odis.append({
                'id': assignment.odi.id_odi,
                'descripcion': assignment.odi.descripcion,
                'peso': odi_peso,
                'rango_descripcion': assignment.rango.descripcion,
                'rango_valor': rango_value
            })
            total_odi_score += rango_value * odi_peso

    assigned_competencias = []
    total_competencia_score = 0.0

    if ano_id != -1 and trimestre_id != -1: # Only query if valid IDs are provided
        competencia_assignments = CompetenciaPersonal.query.filter_by(id_personal=user_id, id_ano=ano_id, id_trimestre=trimestre_id).\
            join(Competencia, CompetenciaPersonal.id_competencia == Competencia.id_competencia).\
            join(Rango, CompetenciaPersonal.id_rango == Rango.id_rango).\
            order_by(CompetenciaPersonal.id_competencia).all()

        for assignment in competencia_assignments:
            try:
                rango_value = float(assignment.rango.descripcion)
            except ValueError:
                rango_value = 0.0
            competencia_peso = float(assignment.competencia.peso)

            assigned_competencias.append({
                'id': assignment.competencia.id_competencia,
                'descripcion': assignment.competencia.descripcion,
                'peso': competencia_peso,
                'rango_descripcion': assignment.rango.descripcion,
                'rango_valor': rango_value
            })
            total_competencia_score += rango_value * competencia_peso

    total_evaluacion_score = total_odi_score + total_competencia_score
    final_evaluacion_score = total_evaluacion_score
    escala_remark = "N/A"
    
    # --- Check for assigned course (Moved outside capping logic for front-end access) ---
    has_course = False
    
    if ano_id != -1 and trimestre_id != -1 and total_evaluacion_score > 0:
        # Check if a course exists for the user in the selected period
        has_course = CursoPersonal.query.filter_by(
            id_personal=user_id,
            id_ano=ano_id,
            id_trimestre=trimestre_id
        ).first() is not None

    # --- NEW LOGIC FOR ESCALA REMARK AND SCORE CAPPING ---
    # The capping logic now correctly relies on the determined has_course variable
    if total_evaluacion_score > 0 and not has_course:
        # Find the highest escala with requiere_curso=false
        highest_safe_escala = Escala.query.filter(
            Escala.requiere_curso == False
        ).order_by(Escala.maximo.desc()).first()

        if highest_safe_escala and total_evaluacion_score > highest_safe_escala.maximo:
            # Cap the score if it exceeds the highest "safe" maximum
            final_evaluacion_score = float(highest_safe_escala.maximo)
    
    # Find the corresponding remark for the (potentially capped) final score
    if final_evaluacion_score > 0:
        matched_escala = Escala.query.filter(
            Escala.minimo <= final_evaluacion_score,
            (Escala.maximo >= final_evaluacion_score) | (Escala.maximo.is_(None))
        ).first()

        if matched_escala:
            escala_remark = matched_escala.descripcion
        else:
            escala_remark = "Fuera de Escala Definida"

    # Fetch existing aprobacion and observacion for the selected period
    evaluacion_aprobacion = None
    evaluacion_observacion = ""
    evaluador_name = "No disponible"

    if ano_id != -1 and trimestre_id != -1:
        existing_evaluacion = Evaluacion.query.filter_by(
            id_personal=user_id,
            id_ano=ano_id,
            id_trimestre=trimestre_id
        ).first()

        if existing_evaluacion:
            evaluacion_aprobacion = existing_evaluacion.aprobacion
            evaluacion_observacion = existing_evaluacion.observacion or ""
            if existing_evaluacion.evaluador:
                evaluador_name = existing_evaluacion.evaluador.full_name

    return {
        'assigned_odis': assigned_odis,
        'total_odi_score': round(float(total_odi_score), 2),
        'assigned_competencias': assigned_competencias,
        'total_competencia_score': round(float(total_competencia_score), 2),
        'total_evaluacion_score': round(float(final_evaluacion_score), 2), # Use the capped score here
        'escala_remark': escala_remark,
        'evaluacion_aprobacion': evaluacion_aprobacion,
        'evaluacion_observacion': evaluacion_observacion,
        'has_course': has_course,
        'evaluador_name': evaluador_name
    }

# Your existing evaluado_page route, with feedback data integration
@app.route('/evaluado_page', methods=['GET', 'POST'])
@login_required
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, EVALUADO_ROLE_ID, DIRECTOR_ROLE_ID])
def evaluado_page():
    user_id = current_user.id_personal # CORRECT: Uses current_user.id_personal

    # --- 1. Fetch unique years for dropdown ---
    unique_years_query = db.session.query(Ano.id_ano, Ano.descripcion)\
        .order_by(Ano.descripcion.desc())\
        .all()

    years_for_dropdown = [{'id': y.id_ano, 'description': y.descripcion} for y in unique_years_query]

    # --- 2. Fetch unique trimesters for dropdown ---
    unique_trimesters_query = db.session.query(Trimestre.id_trimestre, Trimestre.descripcion)\
        .order_by(Trimestre.id_trimestre.asc())\
        .all()

    trimesters_for_dropdown = [{'id': t.id_trimestre, 'description': t.descripcion} for t in unique_trimesters_query]

    # --- 3. IMPORTANT FIX: Force initial IDs to -1 for initial page load ---
    initial_ano_id = -1
    initial_trimestre_id = -1

    # --- 4. Handle AJAX requests (POST) or initial page load (GET) ---
    if request.method == 'POST':
        selected_ano_id = int(request.json.get('ano_id', -1)) # Ensure integer conversion and default
        selected_trimestre_id = int(request.json.get('trimestre_id', -1)) # Ensure integer conversion and default

        if selected_ano_id == -1 or selected_trimestre_id == -1: # Simplified check for missing/invalid IDs
            # Return empty data for -1,-1 selection, as per get_evaluado_data behavior
            data = get_evaluado_data(user_id, selected_ano_id, selected_trimestre_id)
            return jsonify(data)

        # get_evaluado_data already handles -1 by returning empty lists and None for feedback
        data = get_evaluado_data(user_id, selected_ano_id, selected_trimestre_id)
        return jsonify(data)

    # Initial page load (GET request)
    # get_evaluado_data will return empty lists and None/empty for feedback for -1,-1
    initial_data = get_evaluado_data(user_id, initial_ano_id, initial_trimestre_id)

    return render_template(
        'evaluado_page.html',
        title='Página de Evaluación Personal',
        years=years_for_dropdown,
        trimesters=trimesters_for_dropdown,
        initial_ano_id=initial_ano_id,
        initial_trimestre_id=initial_trimestre_id,
        initial_data=initial_data # Pass initial data to the template
    )


# NEW API ENDPOINT: To save evaluation feedback
@app.route('/save_evaluacion_feedback', methods=['POST'])
@login_required
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, EVALUADO_ROLE_ID, DIRECTOR_ROLE_ID]) # Add appropriate roles
def save_evaluacion_feedback():
    data = request.get_json()
    user_personal_id = current_user.id_personal # CORRECT: Uses current_user.id_personal

    ano_id = int(data.get('ano_id'))
    trimestre_id = int(data.get('trimestre_id'))
    aprobacion = data.get('aprobacion') # Boolean
    observacion = data.get('observacion', '').strip() # Text, strip whitespace

    if aprobacion is None: # Ensure aprobacion is explicitly true/false
        return jsonify({'success': False, 'message': 'Aprobación no especificada.'}), 400
    if aprobacion is False and not observacion:
        return jsonify({'success': False, 'message': 'La observación es requerida si no está de acuerdo.'}), 400

    try:
        # Check if an Evaluacion record already exists for this user, year, and trimester
        existing_evaluacion = Evaluacion.query.filter_by(
            id_personal=user_personal_id,
            id_ano=ano_id,
            id_trimestre=trimestre_id
        ).first()

        if existing_evaluacion:
            # Update existing record
            existing_evaluacion.aprobacion = aprobacion
            existing_evaluacion.observacion = observacion
            db.session.add(existing_evaluacion) # Add to session for update tracking
            message = "Feedback de evaluación actualizado exitosamente."
        else:
            new_evaluacion = Evaluacion(
                id_personal=user_personal_id,
                id_ano=ano_id,
                id_trimestre=trimestre_id,
                ids_odi_personal=[],  # Initialize as empty array
                ids_competencia_personal=[], # Initialize as empty array
                aprobacion=aprobacion,
                observacion=observacion,
                ids_curso="" # Initialize as empty or None
            )
            db.session.add(new_evaluacion)
            message = "Feedback de evaluación guardado exitosamente."

        db.session.commit()
        return jsonify({'success': True, 'message': message}), 200

    except Exception as e:
        db.session.rollback()
        print(f"Error saving evaluation feedback: {e}")
        return jsonify({'success': False, 'message': 'Error interno al guardar el feedback.'}), 500


@app.route('/generate_pdf/<int:ano_id>/<int:trimestre_id>')
@login_required
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, EVALUADO_ROLE_ID, DIRECTOR_ROLE_ID])
def generate_pdf(ano_id, trimestre_id):
    # Fetch data for the specified period
    data = get_evaluado_data(current_user.id_personal, ano_id, trimestre_id)
    
    # Get Year and Trimester descriptions
    ano = Ano.query.get(ano_id)
    trimestre = Trimestre.query.get(trimestre_id)
    
    periodo_desc = str(trimestre.descripcion) if trimestre else 'N/A'
    city_name = ""
    if current_user.ubicacion and current_user.ubicacion.id_localizacion == 1:
        city_name = "Caracas"
    
    # Generate PDF using the new module
    buffer = generate_evaluation_pdf(
        data=data,
        colaborador_name=current_user.full_name,
        colaborador_cedula=current_user.cedula,
        periodo_desc=periodo_desc,
        evaluador_name=data.get('evaluador_name', 'N/A'),
        ano_desc=str(ano.descripcion) if ano else 'N/A',
        id_trimestre=trimestre_id,
        city_name=city_name
    )
    
    filename = str(f"Evaluacion_{current_user.usuario}_{str(ano.descripcion) if ano else 'year'}_{str(trimestre.descripcion) if trimestre else 'trimester'}.pdf")
    
    return send_file(
        buffer,
        as_attachment=False,
        download_name=filename,
        mimetype='application/pdf'
    )


@app.route('/admin/edit_user/<int:user_id>', methods=['GET', 'POST'])
@login_required
@role_required([ADMIN_ROLE_ID])
def edit_user(user_id):
    user = User.query.get_or_404(user_id)

    form = EditUserForm(obj=user)

    if request.method == 'GET':
        form.estatu.data = str(user.estatu)
        form.id_rol.data = user.id_rol

    if form.validate_on_submit():
        print("DEBUG: form.validate_on_submit() returned TRUE. Proceeding to save.")

        # --- MODIFIED LINE FOR EXPLICIT STRING CONVERSION ---
        # Ensure form.estatu.data is treated as a string for comparison, even if WTForms acts unexpectedly.
        submitted_estatu_value = str(form.estatu.data)
        print(f"DEBUG: On POST, submitted_estatu_value (after explicit str conversion): '{submitted_estatu_value}' (Type: {type(submitted_estatu_value)})")

        # Now, compare the string value for correct boolean conversion
        user.estatu = (submitted_estatu_value == 'True')
        print(f"DEBUG: On POST, user.estatu (calculated for saving): {user.estatu} (Type: {type(user.estatu)})")

        if form.password.data:
            user.set_password(form.password.data)
            print("DEBUG: Password data present, password hash will be updated.")

        user.id_rol = form.id_rol.data

        try:
            db.session.commit()
            print("DEBUG: db.session.commit() successful.")
            flash(f'Usuario {user.nombre} {user.apellido} actualizado exitosamente.', 'success')
            return redirect(url_for('admin_panel'))
        except Exception as e:
            db.session.rollback()
            print(f"DEBUG: !!! EXCEPTION DURING DB COMMIT: {e}")
            flash(f'Error al actualizar el usuario: {str(e)}', 'danger')
            app.logger.error(f"Error updating user {user_id}: {e}", exc_info=True)
    else:
        if request.method == 'POST':
            print("DEBUG: Form validation failed. Errors:")
            for field_name, errors in form.errors.items():
                print(f"  Field '{field_name}': {errors}")
            print(f"DEBUG: On POST (validation failed), form.estatu.data: '{form.estatu.data}' (Type: {type(form.estatu.data)})")

    return render_template('edit_user.html', form=form, user=user)

@app.route('/assign_odi', methods=['GET', 'POST'])
@login_required
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, DIRECTOR_ROLE_ID])
def assign_odi():
    form = AssignOdiForm()

    if request.method == 'GET':
        form.id_ano.data = ''
        form.id_trimestre.data = ''
        form.personal_id.data = ''

    current_user_location = UbicacionPersonal.query.filter_by(id_personal=current_user.id_personal).first()

    # --- Fetch and Filter ODIs for the Modal (Owner-Based + Universals 1,2,3) ---
    if current_user.id_rol == ADMIN_ROLE_ID:
        all_odis = Odi.query.order_by(Odi.descripcion).all()
    else:
        # User sees: Their own created ODIs OR the Universal ODIs (1, 2, 3)
        all_odis = Odi.query.filter(
            or_(
                Odi.id_personal == current_user.id_personal,
                Odi.id_odi.in_([1, 2, 3])
            )
        ).order_by(Odi.descripcion).all()
        app.logger.info(f"User {current_user.id_personal} fetched {len(all_odis)} ODIs (Own + Universals)")

    all_competencias = Competencia.query.order_by(Competencia.id_competencia).all()
    
    # --- Filter Evaluados for the Dropdown (STILL LOCATION BASED) ---
    # Even if ODIs are user-based, a supervisor only manages people in their department.
    evaluados_for_dropdown = []
    if current_user_location:
        allowed_roles = [EVALUADO_ROLE_ID, SUPERVISOR_ROLE_ID, GERENTE_ROLE_ID]
        if current_user.id_rol == SUPERVISOR_ROLE_ID:
            allowed_roles = [EVALUADO_ROLE_ID, SUPERVISOR_ROLE_ID]

        base_filter = [
            User.id_rol.in_(allowed_roles),
            User.id_personal != current_user.id_personal,
            User.estatu == True,
        ]

        if current_user_location.id_localizacion == 1:
            # Sede Principal: filter by Gerencia General (id_unidades)
            base_filter.append(UbicacionPersonal.id_unidades == current_user_location.id_unidades)
        else:
            # Regional office: filter by Localizacion only
            base_filter.append(UbicacionPersonal.id_localizacion == current_user_location.id_localizacion)

        evaluados_for_dropdown = db.session.query(User).join(UbicacionPersonal, User.id_personal == UbicacionPersonal.id_personal).filter(
            *base_filter
        ).order_by(User.nombre, User.apellido).all()

    form.personal_id.choices = [('', '--- Selecciona un Evaluado ---')] + \
                               [(e.id_personal, f"{e.nombre} {e.apellido} (C.I.: {e.cedula})") for e in evaluados_for_dropdown]

    all_rangos = Rango.query.order_by(Rango.descripcion).all()
    all_rangos_serialized = [{'id_rango': r.id_rango, 'descripcion': r.descripcion} for r in all_rangos]

    # --- POST Request Handling ---
    if form.validate_on_submit():
        ano_id = form.id_ano.data
        trimestre_id = form.id_trimestre.data
        personal_id_str = form.personal_id.data
        
        if not personal_id_str:
            flash('Por favor, selecciona un evaluado.', 'danger')
            return redirect(url_for('assign_odi'))

        personal_id = int(personal_id_str)
        assignments_made = 0
        assignments_updated = 0
        assignments_deleted = 0

        try:
            # 1. Process ODIs
            submitted_odi_ids = {int(x) for x in request.form.getlist('odi_ids') if x.strip().isdigit()}
            
            # --- PESO VALIDATION ---
            total_peso = 0
            if submitted_odi_ids:
                total_peso = db.session.query(db.func.sum(Odi.peso)).filter(Odi.id_odi.in_(submitted_odi_ids)).scalar() or 0
            
            if total_peso != 50:
                flash(f'Error: El peso total de los ODI debe ser exactamente 50. El total actual de tu selección es {total_peso}.', 'danger')
                return redirect(url_for('assign_odi', id_ano=ano_id, id_trimestre=trimestre_id, personal_id=personal_id))

            existing_odi_assignments = OdiPersonal.query.filter_by(
                id_personal=personal_id, id_ano=ano_id, id_trimestre=trimestre_id
            ).all()
            existing_odi_map = {a.id_odi: a for a in existing_odi_assignments}

            for odi_id in submitted_odi_ids:
                rango_id_str = request.form.get(f'odi_rango_{odi_id}')
                if not rango_id_str or int(rango_id_str) == 0:
                    flash(f'Rango inválido para ODI {odi_id}.', 'danger')
                    db.session.rollback()
                    return redirect(url_for('assign_odi'))

                rango_id = int(rango_id_str)
                if odi_id in existing_odi_map:
                    item = existing_odi_map[odi_id]
                    if item.id_rango != rango_id:
                        item.id_rango = rango_id
                        assignments_updated += 1
                else:
                    new_a = OdiPersonal(id_personal=personal_id, id_odi=odi_id, id_ano=ano_id, id_trimestre=trimestre_id, id_rango=rango_id)
                    db.session.add(new_a)
                    assignments_made += 1

            for eid in existing_odi_map:
                if eid not in submitted_odi_ids:
                    db.session.delete(existing_odi_map[eid])
                    assignments_deleted += 1

            # 2. Process Competencias (Simplified for this snippet)
            submitted_comp_ids = {int(x) for x in request.form.getlist('competencia_ids') if x.strip().isdigit()}
            existing_comp_assignments = CompetenciaPersonal.query.filter_by(
                id_personal=personal_id, id_ano=ano_id, id_trimestre=trimestre_id
            ).all()
            existing_comp_map = {a.id_competencia: a for a in existing_comp_assignments}

            for cid in submitted_comp_ids:
                rango_id_str = request.form.get(f'competencia_rango_{cid}')
                if rango_id_str and int(rango_id_str) != 0:
                    rango_id = int(rango_id_str)
                    if cid in existing_comp_map:
                        item = existing_comp_map[cid]
                        if item.id_rango != rango_id:
                            item.id_rango = rango_id
                            assignments_updated += 1
                    else:
                        new_c = CompetenciaPersonal(id_personal=personal_id, id_competencia=cid, id_ano=ano_id, id_trimestre=trimestre_id, id_rango=rango_id)
                        db.session.add(new_c)
                        assignments_made += 1

            for ecid in existing_comp_map:
                if ecid not in submitted_comp_ids:
                    db.session.delete(existing_comp_map[ecid])
                    assignments_deleted += 1

            db.session.commit()

            # --- NEW: UPDATE EVALUACION RECORD WITH EVALUADOR AND ASSIGNMENT IDs ---
            # 1. Fetch current IDs for this period
            current_odi_ids = [a.id_odi_personal for a in OdiPersonal.query.filter_by(id_personal=personal_id, id_ano=ano_id, id_trimestre=trimestre_id).all()]
            current_comp_ids = [c.id_competencia_personal for c in CompetenciaPersonal.query.filter_by(id_personal=personal_id, id_ano=ano_id, id_trimestre=trimestre_id).all()]

            # 2. Find or Create Evaluacion
            eval_record = Evaluacion.query.filter_by(
                id_personal=personal_id, id_ano=ano_id, id_trimestre=trimestre_id
            ).first()

            if not eval_record:
                eval_record = Evaluacion(
                    id_personal=personal_id,
                    id_ano=ano_id,
                    id_trimestre=trimestre_id,
                    ids_curso=""
                )
                db.session.add(eval_record)
            
            # 3. Update evaluator and IDs
            eval_record.id_evaluador = current_user.id_personal
            eval_record.ids_odi_personal = current_odi_ids
            eval_record.ids_competencia_personal = current_comp_ids
            
            db.session.commit()

            flash(f'Asignación completada: {assignments_made} nuevas, {assignments_updated} actualizadas, {assignments_deleted} eliminadas.', 'success')
            return redirect(url_for('assign_odi', id_ano=ano_id, id_trimestre=trimestre_id, personal_id=personal_id_str))

        except Exception as e:
            db.session.rollback()
            app.logger.error(f"Error in assign_odi: {e}", exc_info=True)
            flash(f'Error al procesar la asignación: {e}', 'danger')
            return redirect(url_for('assign_odi'))

    return render_template('assign_odi.html', form=form, all_odis=all_odis, all_competencias=all_competencias, all_rangos=all_rangos_serialized)


# --- AJAX endpoint to get assigned ODIs (Existing - remains the same) ---
@app.route('/get_assigned_odis', methods=['GET'])
@login_required # Use login_required for consistency and security
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, SUPERVISOR_ROLE_ID, DIRECTOR_ROLE_ID]) # Restrict to relevant roles
def get_assigned_odis():
    personal_id = request.args.get('personal_id', type=int)
    # Corrected parameter names to match frontend (ano_id, trimestre_id)
    ano_id = request.args.get('ano_id', type=int)
    trimestre_id = request.args.get('trimestre_id', type=int)

    if not all([personal_id, ano_id, trimestre_id]):
        return jsonify({'error': 'Parámetros incompletos para buscar ODIs.'}), 400

    try:
        assigned_odis = OdiPersonal.query.filter_by(
            id_personal=personal_id,
            id_ano=ano_id,           # Use the corrected variable name
            id_trimestre=trimestre_id # Use the corrected variable name
        ).options(
            db.joinedload(OdiPersonal.odi),
            db.joinedload(OdiPersonal.rango)
        ).all()

        results = []
        for assign in assigned_odis:
            results.append({
                'odi_id': assign.id_odi,
                'odi_descripcion': assign.odi.descripcion,
                'odi_peso': assign.odi.peso,
                'rango_id': assign.id_rango,
                'rango_descripcion': assign.rango.descripcion
            })
        return jsonify(results)
    except Exception as e:
        logging.error(f"Error fetching assigned ODIs: {e}")
        return jsonify({'error': 'Error interno al cargar ODIs asignados.'}), 500


# --- NEW: AJAX endpoint to get assigned Competencias (remains the same) ---
@app.route('/get_assigned_competencias', methods=['GET'])
@login_required # Use login_required for consistency and security
@role_required([ADMIN_ROLE_ID, SUPERVISOR_ROLE_ID]) # Restrict to relevant roles
def get_assigned_competencias():
    personal_id = request.args.get('personal_id', type=int)
    # Corrected parameter names to match frontend (ano_id, trimestre_id)
    ano_id = request.args.get('ano_id', type=int)
    trimestre_id = request.args.get('trimestre_id', type=int)

    if not all([personal_id, ano_id, trimestre_id]):
        return jsonify({'error': 'Parámetros incompletos para buscar competencias.'}), 400

    try:
        assigned_competencias = CompetenciaPersonal.query.filter_by(
            id_personal=personal_id,
            id_ano=ano_id,           # Use the corrected variable name
            id_trimestre=trimestre_id # Use the corrected variable name
        ).options(
            db.joinedload(CompetenciaPersonal.competencia),
            db.joinedload(CompetenciaPersonal.rango)
        ).order_by(CompetenciaPersonal.id_competencia).all()

        results = []
        for assign in assigned_competencias:
            results.append({
                'competencia_id': assign.id_competencia,
                'competencia_descripcion': assign.competencia.descripcion,
                'competencia_peso': assign.competencia.peso,
                'rango_id': assign.id_rango,
                'rango_descripcion': assign.rango.descripcion
            })
        return jsonify(results)
    except Exception as e:
        logging.error(f"Error fetching assigned competencias: {e}")
        return jsonify({'error': 'Error interno al cargar competencias asignadas.'}), 500


def allowed_file(filename):
    print(f"DEBUG: Checking allowed_file for: '{filename}'")
    if not isinstance(filename, str):
        print("DEBUG: Filename is not a string type.")
        return False
    is_allowed = '.' in filename and \
                 filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
    print(f"DEBUG: Is '{filename}' allowed? {is_allowed}")
    return is_allowed


@app.route('/assign_cursos', methods=['GET', 'POST'])
@login_required
@role_required([ADMIN_ROLE_ID, SUPERVISOR_ROLE_ID, DIRECTOR_ROLE_ID, GERENTE_ROLE_ID]) # Only supervisors and gerentes should access this page
def assign_cursos():
    form = AssignCursosForm()
    
    # Populate Año and Trimestre choices for the form
    anos = Ano.query.order_by(Ano.descripcion.desc()).all()
    form.id_ano.choices = [(0, '--- Selecciona un Año ---')] + \
                          [(ano.id_ano, ano.descripcion) for ano in anos]

    trimesters = Trimestre.query.order_by(Trimestre.id_trimestre).all()
    form.id_trimestre.choices = [(0, '--- Selecciona un Trimestre ---')] + \
                                [(trimestre.id_trimestre, trimestre.descripcion) for trimestre in trimesters]

    # --- Fetch Evaluados based on Supervisor's Location ---
    current_user_location = UbicacionPersonal.query.filter_by(id_personal=current_user.id_personal).first()

    evaluados_for_dropdown = []
    if not current_user_location:
        flash('Error: No se encontró su información de ubicación. No se pueden cargar los evaluados. Por favor, contacte a un administrador.', 'danger')
        app.logger.warning(f"User {current_user.id_personal} has no location info. No evaluados will be loaded for dropdown.")
    else:
        allowed_roles = [EVALUADO_ROLE_ID, SUPERVISOR_ROLE_ID, GERENTE_ROLE_ID]
        if current_user.id_rol == SUPERVISOR_ROLE_ID:
            allowed_roles = [EVALUADO_ROLE_ID, SUPERVISOR_ROLE_ID]

        base_filter = [
            User.id_rol.in_(allowed_roles),
            User.id_personal != current_user.id_personal,
            User.estatu == True,
        ]

        if current_user_location.id_localizacion == 1:
            # Sede Principal: filter by Gerencia General (id_unidades)
            base_filter.append(UbicacionPersonal.id_unidades == current_user_location.id_unidades)
        else:
            # Regional office: filter by Localizacion only
            base_filter.append(UbicacionPersonal.id_localizacion == current_user_location.id_localizacion)

        evaluados_query = db.session.query(User).join(UbicacionPersonal, User.id_personal == UbicacionPersonal.id_personal).filter(
            *base_filter
        ).order_by(User.nombre, User.apellido).all()

        evaluados_for_dropdown = evaluados_query

    form.personal_id.choices = [(0, '--- Selecciona un Evaluado ---')] + \
                               [(evaluado.id_personal, f"{evaluado.nombre} {evaluado.apellido} (C.I.: {evaluado.cedula})") for evaluado in evaluados_for_dropdown]

    if form.validate_on_submit():        
        ano_id = form.id_ano.data
        trimestre_id = form.id_trimestre.data
        personal_id = form.personal_id.data
        if ano_id == 0 or trimestre_id == 0 or personal_id == 0:
            flash('Por favor, selecciona Año, Trimestre y Evaluado válidos.', 'danger')
            # print("DEBUG: Invalid dropdown selection (0 values). Redirecting.") <--- REMOVED
            return redirect(url_for('assign_cursos'))

        # Get existing courses for this selection
        existing_cursos = CursoPersonal.query.filter_by(
            id_personal=personal_id,
            id_ano=ano_id,
            id_trimestre=trimestre_id
        ).all()
        existing_curso_ids = {c.id_curso: c for c in existing_cursos}
        present_curso_ids_on_submit = set()
        for key in request.form:
            if key.startswith('curso_desc_'):
                try:
                    parts = key.split('_')
                    if len(parts) == 3 and parts[2].isdigit():
                        present_curso_ids_on_submit.add(int(parts[2]))
                except ValueError:
                    pass

        # --- Handle Deletions ---
        cursos_deleted_count = 0
        for existing_id, curso_obj in list(existing_curso_ids.items()):
            if existing_id not in present_curso_ids_on_submit:
                try:
                    file_to_delete_path = None
                    if curso_obj.path_to_file:
                        file_to_delete_path = os.path.join(current_app.config['UPLOAD_FOLDER'], os.path.basename(curso_obj.path_to_file))
                    
                    if file_to_delete_path and os.path.exists(file_to_delete_path):
                        os.remove(file_to_delete_path)
                        app.logger.info(f"Deleted file: {file_to_delete_path}")                
                    elif curso_obj.path_to_file:
                        pass # Logged via app.logger.info/warning if desired, but not via print
                        
                    db.session.delete(curso_obj)
                    flash(f"Curso '{curso_obj.descripcion}' eliminado.", 'info')
                    app.logger.info(f"Deleted CursoPersonal ID {existing_id} for Personal ID {personal_id}.")
                    cursos_deleted_count += 1
                except OSError as e:
                    flash(f"Error al eliminar archivo para curso '{curso_obj.descripcion}': {e}", 'warning')
                    app.logger.error(f"Error deleting file for CursoPersonal ID {existing_id}: {e}")
                    # print(f"DEBUG: OSError during file deletion for {existing_id}: {e}") <--- REMOVED
                except Exception as e:
                    flash(f"Error al eliminar curso '{curso_obj.descripcion}': {e}", 'danger')
                    app.logger.error(f"Error deleting CursoPersonal ID {existing_id}: {e}")
        cursos_added_count = 0
        cursos_updated_count = 0

        for key in request.form.keys():
            if key.startswith('curso_desc_'):
                try:
                    course_identifier = key.replace('curso_desc_', '')
                    description = request.form.get(key)
                    file_key = f'curso_file_{course_identifier}'
                    uploaded_file = request.files.get(file_key)
                    path_to_store_in_db = None

                    if uploaded_file and uploaded_file.filename != '':
                        if allowed_file(uploaded_file.filename):
                            filename_base, ext = os.path.splitext(secure_filename(uploaded_file.filename))
                            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
                            new_filename = f"{filename_base}_{timestamp}{ext}"
                            absolute_save_path = os.path.join(current_app.config['UPLOAD_FOLDER'], new_filename)

                            try:
                                uploaded_file.save(absolute_save_path)

                                static_folder_absolute = os.path.join(current_app.root_path, 'static')
                                path_to_store_in_db = os.path.relpath(absolute_save_path, static_folder_absolute).replace(os.sep, '/')

                            except Exception as save_e:
                                app.logger.error(f"Error saving file for course {course_identifier}: {save_e}")
                                flash(f"Error al guardar el archivo '{uploaded_file.filename}': {save_e}", 'danger')
                                path_to_store_in_db = None
                        else:
                            app.logger.warning(f"Attempted upload of disallowed file type: {uploaded_file.filename}")
                            flash(f'Tipo de archivo no permitido para "{uploaded_file.filename}". Solo se permiten PDFs.', 'warning')

                    if course_identifier.isdigit(): # Existing course
                        curso_id = int(course_identifier)
                        if curso_id in existing_curso_ids:
                            curso_to_update = existing_curso_ids[curso_id]
                            
                            has_updates = False
                            
                            # Update description if changed
                            if curso_to_update.descripcion != description:
                                curso_to_update.descripcion = description
                                has_updates = True
                                
                            # Update file path if a new file was successfully uploaded
                            if path_to_store_in_db:
                                if curso_to_update.path_to_file:
                                    # Attempt to delete old file
                                    old_absolute_path = os.path.join(current_app.root_path, 'static', curso_to_update.path_to_file)
                                    if os.path.exists(old_absolute_path):
                                        try:
                                            os.remove(old_absolute_path)
                                            app.logger.info(f"Removed old file: {old_absolute_path}")
                                            # print(f"DEBUG: Removed old file '{old_absolute_path}'.") <--- REMOVED
                                        except OSError as e:
                                            app.logger.error(f"Error removing old file {old_absolute_path}: {e}")

                                # Set new file path
                                curso_to_update.path_to_file = path_to_store_in_db
                                has_updates = True

                            if has_updates:
                                db.session.add(curso_to_update)
                                cursos_updated_count += 1
                                flash(f"Curso '{description}' actualizado.", 'info')

                        else:
                            app.logger.warning(f"Submitted existing course ID {curso_id} not found in existing DB records. Skipping update.")
                            flash(f"Error: Curso existente con ID {curso_id} no encontrado en la base de datos.", 'warning')
                            continue

                    else: # New course (course_identifier is like 'new_X')
                        if not description:
                            flash(f"Error: La descripción es requerida para un nuevo curso.", 'warning')
                            continue

                        if not path_to_store_in_db:
                            flash(f"Advertencia: Se recomienda adjuntar un archivo para el nuevo curso '{description}'.", 'warning')
                            
                        new_curso = CursoPersonal(
                            descripcion=description,
                            path_to_file=path_to_store_in_db,
                            id_personal=personal_id,
                            id_ano=ano_id,
                            id_trimestre=trimestre_id
                        )
                        db.session.add(new_curso)
                        flash(f"Nuevo curso '{description}' añadido.", 'success')
                        app.logger.info(f"Added new CursoPersonal: {description} for Personal ID {personal_id}.")
                        cursos_added_count += 1

                except Exception as e:
                    flash(f'Error al procesar curso "{description or course_identifier}": {e}', 'danger')
                    app.logger.error(f"Error processing course '{description or course_identifier}' for user {current_user.id_personal}: {e}")


        # --- Final Database Commit ---
        try:
            db.session.commit()
            if any([cursos_added_count, cursos_updated_count, cursos_deleted_count]):
                message_parts = []
                if cursos_added_count > 0: message_parts.append(f'{cursos_added_count} cursos nuevos añadidos.')
                if cursos_updated_count > 0: message_parts.append(f'{cursos_updated_count} cursos actualizados.')
                if cursos_deleted_count > 0: message_parts.append(f'{cursos_deleted_count} cursos eliminados.')
                
                flash('¡Operación de cursos completada! ' + ' '.join(message_parts), 'success')
            else:
                flash('No se realizaron cambios en los cursos para el evaluado seleccionado.', 'info')

        except IntegrityError as e:
            db.session.rollback()
            flash(f'Error de integridad de la base de datos: {e}', 'danger')
            app.logger.error(f"Integrity error saving cursos for user {personal_id} by supervisor {current_user.id_personal}: {e}")
        except Exception as e:
            db.session.rollback()
            flash(f'Error general al guardar los cursos: {e}', 'danger')
            app.logger.error(f"General error saving cursos for user {personal_id} by supervisor {current_user.id_personal}: {e}")

        return redirect(url_for('assign_cursos',
                                id_ano=ano_id,
                                id_trimestre=trimestre_id,
                                personal_id=personal_id))

    ano_id = request.args.get('id_ano', 0, type=int)
    trimestre_id = request.args.get('id_trimestre', 0, type=int)
    personal_id = request.args.get('personal_id', 0, type=int)
    
    form.id_ano.data = ano_id
    form.id_trimestre.data = trimestre_id
    form.personal_id.data = personal_id
    
    return render_template('assign_cursos.html', form=form)

# --- New AJAX Endpoint to fetch existing courses ---
@app.route('/get_personal_cursos', methods=['POST'])
@login_required
def get_personal_cursos():
    data = request.get_json()
    personal_id = data.get('personal_id')
    ano_id = data.get('ano_id')
    trimestre_id = data.get('trimestre_id')

    print(f"DEBUG: AJAX call to /get_personal_cursos for Personal ID: {personal_id}, Ano: {ano_id}, Trimestre: {trimestre_id}")

    if not all([personal_id, ano_id, trimestre_id]):
        print("DEBUG: Missing parameters for /get_personal_cursos.")
        return jsonify({'error': 'Missing parameters'}), 400

    try:
        cursos = CursoPersonal.query.filter_by(
            id_personal=personal_id,
            id_ano=ano_id,
            id_trimestre=trimestre_id
        ).all()
        print(f"DEBUG: Found {len(cursos)} courses in DB for AJAX request.")

        cursos_data = []
        for curso in cursos:
            file_url = None
            if curso.path_to_file:
                file_url = url_for('static', filename=curso.path_to_file)

            cursos_data.append({
                'id_curso': curso.id_curso,
                'descripcion': curso.descripcion,
                'path_to_file': file_url
            })
        print(f"DEBUG: Prepared {len(cursos_data)} courses for JSON response.")
        return jsonify({'cursos': cursos_data})
    except Exception as e:
        app.logger.error(f"Error fetching personal courses: {e}")
        print(f"DEBUG: Error in /get_personal_cursos: {e}")
        return jsonify({'error': 'Error fetching data', 'details': str(e)}), 500

# --- Gerente Page AJAX Endpoints ---

@app.route('/get_unidades/<int:div_id>', methods=['GET'])
@login_required
def get_unidades(div_id):
    """Returns Unidades (Gerencias Generales) for a given DivisionesGenerales id."""
    if div_id == 0:
        return jsonify([])
    results = Unidades.query.filter_by(id_divisiones_generales=div_id, valido=True).order_by(Unidades.id_unidades).all()
    return jsonify([{'id': r.id_unidades, 'description': r.nombre} for r in results])


@app.route('/get_gerencias_generales/<int:unidad_id>', methods=['GET'])
@login_required
def get_gerencias_generales(unidad_id):
    """Returns GerenciasGenerales (Gerencias de Línea) for a given Unidades id."""
    if unidad_id == 0:
        return jsonify([])
    results = GerenciasGenerales.query.filter_by(id_unidades=unidad_id, valido=True).order_by(GerenciasGenerales.id_gerencias_generales).all()
    return jsonify([{'id': r.id_gerencias_generales, 'description': r.nombre} for r in results])


@app.route('/get_coordinaciones/<int:gerencia_id>', methods=['GET'])
@login_required
def get_coordinaciones(gerencia_id):
    """Returns Coordinaciones for a given GerenciasGenerales id."""
    if gerencia_id == 0:
        return jsonify([])
    results = Coordinaciones.query.filter_by(id_gerencias_generales=gerencia_id, valido=True).order_by(Coordinaciones.id_coordinaciones).all()
    return jsonify([{'id': r.id_coordinaciones, 'description': r.nombre} for r in results])

@app.route('/gerente_page', methods=['GET', 'POST'])
@login_required
@role_required([ADMIN_ROLE_ID, GERENTE_ROLE_ID, DIRECTOR_ROLE_ID])
def gerente_page():
    search_form = SearchUserByCedulaForm()
    assign_form = AssignLocationForm()

    assign_form.id_rol.choices = [
        (EVALUADO_ROLE_ID, 'Evaluado'),
        (SUPERVISOR_ROLE_ID, 'Supervisor'),
        (GERENTE_ROLE_ID, 'Gerente')
    ]

    found_user = None

    if request.method == 'POST':
        # --- Handle Assignment/Update ---
        if request.form.get('user_id_to_assign'):
            user_id = request.form.get('user_id_to_assign', type=int)

            # Re-populate cascading choices so WTForms validation doesn't fail
            try:
                div_id  = int(request.form.get('id_divisiones_generales', 0))
                uni_id  = int(request.form.get('id_unidades', 0))
                ger_id  = int(request.form.get('id_gerencias_generales', 0))
                coord_id = int(request.form.get('id_coordinaciones', 0))

                if uni_id != 0:
                    assign_form.id_unidades.choices += [
                        (u.id_unidades, u.nombre)
                        for u in Unidades.query.filter_by(id_divisiones_generales=div_id, valido=True).all()
                    ]
                if ger_id != 0:
                    assign_form.id_gerencias_generales.choices += [
                        (g.id_gerencias_generales, g.nombre)
                        for g in GerenciasGenerales.query.filter_by(id_unidades=uni_id, valido=True).all()
                    ]
                if coord_id != 0:
                    assign_form.id_coordinaciones.choices += [
                        (c.id_coordinaciones, c.nombre)
                        for c in Coordinaciones.query.filter_by(id_gerencias_generales=ger_id, valido=True).all()
                    ]
            except Exception:
                pass

            if assign_form.validate():
                user_to_update = User.query.get(user_id)
                if user_to_update:
                    try:
                        # 1. Update Role
                        user_to_update.id_rol = assign_form.id_rol.data

                        loc_val  = assign_form.id_localizacion.data or None
                        div_val  = assign_form.id_divisiones_generales.data
                        uni_val  = assign_form.id_unidades.data
                        ger_val  = assign_form.id_gerencias_generales.data
                        coord_val = assign_form.id_coordinaciones.data

                        # When localizacion != 1, the sub-levels are not required — store None
                        if loc_val != 1:
                            div_val = uni_val = ger_val = coord_val = None
                        else:
                            div_val  = div_val  if div_val  and div_val  != 0 else None
                            uni_val  = uni_val  if uni_val  and uni_val  != 0 else None
                            ger_val  = ger_val  if ger_val  and ger_val  != 0 else None
                            coord_val = coord_val if coord_val and coord_val != 0 else None

                        # 2. Update/Create UbicacionPersonal
                        existing_loc = UbicacionPersonal.query.filter_by(id_personal=user_id).first()
                        if existing_loc:
                            existing_loc.id_localizacion          = loc_val
                            existing_loc.id_divisiones_generales  = div_val
                            existing_loc.id_unidades              = uni_val
                            existing_loc.id_gerencias_generales   = ger_val
                            existing_loc.id_coordinaciones        = coord_val
                        else:
                            new_loc = UbicacionPersonal(
                                id_personal=user_id,
                                id_localizacion=loc_val,
                                id_divisiones_generales=div_val,
                                id_unidades=uni_val,
                                id_gerencias_generales=ger_val,
                                id_coordinaciones=coord_val
                            )
                            db.session.add(new_loc)

                        db.session.commit()
                        flash('Configuración de usuario actualizada correctamente.', 'success')
                        return redirect(url_for('gerente_page'))
                    except Exception as e:
                        db.session.rollback()
                        flash(f'Error al guardar: {e}', 'danger')

        # --- Handle Delete ---
        elif 'remove_id' in request.form:
            remove_id = request.form.get('remove_id', type=int)
            loc_to_remove = UbicacionPersonal.query.get(remove_id)
            if loc_to_remove:
                try:
                    db.session.delete(loc_to_remove)
                    db.session.commit()
                    flash('Ubicación de usuario eliminada correctamente.', 'success')
                except Exception as e:
                    db.session.rollback()
                    flash(f'Error al eliminar ubicación: {e}', 'danger')
            return redirect(url_for('gerente_page'))

        # --- Handle Search ---
        elif search_form.validate_on_submit():
            user = User.query.filter_by(cedula=str(search_form.cedula.data)).first()
            if user:
                found_user = user
                assign_form.id_rol.data = user.id_rol
                flash(f"Usuario {user.nombre} encontrado.", "info")
            else:
                flash("Usuario no encontrado.", "danger")

    current_locations = UbicacionPersonal.query.options(
        db.joinedload(UbicacionPersonal.user_details_in_ubicacion)
    ).all()

    return render_template('gerente_page.html',
                           search_form=search_form,
                           assign_form=assign_form,
                           found_user=found_user,
                           current_locations=current_locations,
                           title="Gestión de Ubicaciones")
# --- Talento Humano Page ---

@app.route('/talento_humano', methods=['GET', 'POST'])
@login_required
@role_required([ADMIN_ROLE_ID, TALENTO_HUMANO_ROLE_ID])
def talento_humano():
    if request.method == 'POST':
        form = TalentoHumanoObservationForm()
        form.process(formdata=request.form)
        
        if form.validate_on_submit():
            evaluacion_id = request.form.get('evaluacion_id')
            observacion = form.observacion_th.data
            
            if evaluacion_id and observacion:
                evaluacion = Evaluacion.query.get(evaluacion_id)
                if evaluacion:
                    evaluacion.observacion_th = observacion
                    db.session.commit()
                    flash('Observación de Talento Humano guardada exitosamente.', 'success')
                else:
                    flash('Evaluación no encontrada.', 'error')
            else:
                flash('Datos incompletos para guardar la observación.', 'error')
        else:
            for field, errors in form.errors.items():
                for error in errors:
                    flash(f"Error en el campo '{field}': {error}", 'error')

        return redirect(url_for('talento_humano'))

    # For GET requests, fetch the data
    disapproved_evaluaciones_raw = db.session.query(
        Evaluacion,
        User.nombre,
        Ano.descripcion,
        Trimestre.descripcion
    ).join(User, Evaluacion.id_personal == User.id_personal)\
    .join(Ano, Evaluacion.id_ano == Ano.id_ano)\
    .join(Trimestre, Evaluacion.id_trimestre == Trimestre.id_trimestre)\
    .filter(Evaluacion.aprobacion == False).all()
    
    disapproved_evaluaciones = []
    for evaluacion, personal_name, ano_name, trimestre_name in disapproved_evaluaciones_raw:
        # Fetch detailed ODI assignments
        odi_details = []
        total_odi_score = 0.0
        if evaluacion.ids_odi_personal:
            odi_personal_assignments = OdiPersonal.query.filter(OdiPersonal.id_odi_personal.in_(evaluacion.ids_odi_personal)).all()
            for assignment in odi_personal_assignments:
                try:
                    rango_value = float(assignment.rango.descripcion)
                except (ValueError, AttributeError):
                    rango_value = 0.0
                score = assignment.odi.peso * rango_value
                odi_details.append({
                    'descripcion': assignment.odi.descripcion,
                    'peso': assignment.odi.peso,
                    'score': score,
                    'rango_descripcion': assignment.rango.descripcion
                })
                total_odi_score += score

        # Fetch detailed Competencia assignments
        comp_details = []
        total_comp_score = 0.0
        if evaluacion.ids_competencia_personal:
            comp_personal_assignments = CompetenciaPersonal.query.filter(CompetenciaPersonal.id_competencia_personal.in_(evaluacion.ids_competencia_personal)).all()
            for assignment in comp_personal_assignments:
                try:
                    rango_value = float(assignment.rango.descripcion)
                except (ValueError, AttributeError):
                    rango_value = 0.0
                score = assignment.competencia.peso * rango_value
                comp_details.append({
                    'descripcion': assignment.competencia.descripcion,
                    'peso': assignment.competencia.peso,
                    'score': score,
                    'rango_descripcion': assignment.rango.descripcion
                })
                total_comp_score += score
                
        total_evaluacion_score = total_odi_score + total_comp_score
        final_evaluacion_score = total_evaluacion_score
        
        # Check if a course exists for the user in the selected period
        has_course = CursoPersonal.query.filter_by(
            id_personal=evaluacion.id_personal,
            id_ano=evaluacion.id_ano,
            id_trimestre=evaluacion.id_trimestre
        ).first() is not None

        # Score capping logic if no course is found
        is_capped = False
        if total_evaluacion_score > 0 and not has_course:
            highest_safe_escala = Escala.query.filter(
                Escala.requiere_curso == False
            ).order_by(Escala.maximo.desc()).first()

            if highest_safe_escala and total_evaluacion_score > highest_safe_escala.maximo:
                final_evaluacion_score = float(highest_safe_escala.maximo)
                is_capped = True
                
        disapproved_evaluaciones.append({
            'evaluacion': evaluacion,
            'personal_name': personal_name,
            'ano_name': ano_name,
            'trimestre_name': trimestre_name,
            'odi_details': odi_details,
            'comp_details': comp_details,
            'total_score': round(float(total_evaluacion_score), 2),
            'final_score': round(float(final_evaluacion_score), 2),
            'has_course': has_course,
            'is_capped': is_capped
        })

    forms = {eval_dict['evaluacion'].id_evaluacion: TalentoHumanoObservationForm(observacion_th=eval_dict['evaluacion'].observacion_th) for eval_dict in disapproved_evaluaciones}
    
    return render_template(
        'talento_humano_page.html', 
        disapproved_evaluaciones=disapproved_evaluaciones, 
        title='Página de Talento Humano',
        forms=forms
    )

@app.route('/change_password', methods=['GET', 'POST'])
@login_required
def change_password():
    """
    Allows the currently logged-in user to change their own password.
    """
    form = ChangePasswordForm()
    user_to_edit = current_user

    if form.validate_on_submit():
        try:
            # Hash the new password and update the user record
            user_to_edit.set_password(form.password.data) 
            db.session.commit()
            
            flash('¡Contraseña actualizada exitosamente! Usa tu nueva contraseña la próxima vez que inicies sesión.', 'success')
            return redirect(url_for('dashboard')) 
        except Exception as e:
            db.session.rollback()
            flash(f"Error al actualizar la contraseña: {e}", 'danger')

    # Note: We display user details (non-sensitive) using the current_user object
    return render_template('change_password.html', 
                           title='Cambiar Contraseña', 
                           user=user_to_edit, 
                           form=form)

# --- Run App ---
if __name__ == '__main__':

    app.run(host='0.0.0.0', port=5005, debug=os.environ.get('FLASK_DEBUG') == 'True')