# routes/travel.py
from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, current_app, abort
from flask_login import login_required, current_user
from sqlalchemy import and_, or_
from datetime import datetime, date
from io import StringIO, BytesIO
import csv
from utils.notify import notify_employee, notify_many
import mimetypes, os
from werkzeug.utils import secure_filename
from werkzeug.routing import BuildError

from extensions import db
from models import Employee, TRRequest, TRItem, TRApprovalLog, Role, Department

tr_bp = Blueprint('tr', __name__, url_prefix='/travel')

# ======================================
# Helpers
# ======================================

ALLOWED_EXTS = {'.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.csv'}
MAX_ITEM_UPLOAD_MB = 15

def _save_tr_item_attachment(file_storage, request_id, item_id):
    """Save upload; return (rel_path, original_name, mimetype)."""
    if not file_storage or not file_storage.filename:
        return None, None, None

    # size guard
    file_storage.stream.seek(0, os.SEEK_END)
    size = file_storage.stream.tell()
    file_storage.stream.seek(0)
    if size > MAX_ITEM_UPLOAD_MB * 1024 * 1024:
        raise ValueError(f"File exceeds {MAX_ITEM_UPLOAD_MB}MB")

    fname = secure_filename(file_storage.filename)
    ext = os.path.splitext(fname)[1].lower()
    if ext not in ALLOWED_EXTS:
        raise ValueError("Unsupported file type")

    root = current_app.config.get('UPLOAD_FOLDER') or os.path.join(current_app.root_path, 'uploads')
    full_dir = os.path.join(root, 'tr', str(request_id))
    os.makedirs(full_dir, exist_ok=True)

    saved_name = f"item_{item_id}_{fname}"
    full_path  = os.path.join(full_dir, saved_name)
    file_storage.save(full_path)

    mime = mimetypes.guess_type(saved_name)[0] or 'application/octet-stream'
    # store relative path under UPLOAD_FOLDER root
    rel_path = os.path.join('tr', str(request_id), saved_name).replace('\\', '/')
    return rel_path, fname, mime

def _hr_ids():
    """Find HR recipients via role or department name."""
    try:
        ids = [e.id for e in Employee.query.join(Role).filter(Role.name.ilike('hr')).all()]
        if ids:
            return ids
    except Exception:
        pass
    try:
        return [e.id for e in Employee.query.join(Department).filter(Department.name.ilike('%hr%')).all()]
    except Exception:
        return []

def _role_name(user) -> str:
    try:
        return (getattr(getattr(user, 'role', None), 'name', '') or '').strip().lower()
    except Exception:
        return ''

def _is_admin(user) -> bool:
    rn = _role_name(user)
    return rn in ('admin', 'system admin', 'superadmin', 'hr admin')

def _is_hr_or_admin(user) -> bool:
    rn = _role_name(user)
    return rn in ('hr', 'hr admin', 'admin', 'system admin', 'superadmin')

def _is_hr(user_id: int) -> bool:
    """Backward-compatible HR detector, but treat Admin as HR-equivalent."""
    if _is_hr_or_admin(current_user):
        return True
    return user_id in set(_hr_ids())

def _hod_team_ids(hod_id: int):
    """Team = employees whose reporting_manager_id == hod_id."""
    return [e.id for e in Employee.query.filter_by(reporting_manager_id=hod_id).all()]

# ======================================
# Create / Edit TR Request
# ======================================

@tr_bp.route('/request/new', methods=['GET', 'POST'])
@login_required
def tr_new():
    today = date.today()
    if request.method == 'POST':
        year  = int(request.form['year'])
        month = int(request.form['month'])

        # Ensure 1 request per employee per month
        existing = TRRequest.query.filter_by(employee_id=current_user.id, month=month, year=year).first()
        if existing:
            flash('⚠️ A request for this month already exists. You can edit it.', 'warning')
            return redirect(url_for('tr.tr_edit', req_id=existing.id))

        tr = TRRequest(employee_id=current_user.id, month=month, year=year)
        db.session.add(tr)
        db.session.flush()  # get tr.id

        # Parse dynamic line items (arrays)
        dates   = request.form.getlist('item_date[]')
        froms   = request.form.getlist('item_from[]')
        tos     = request.form.getlist('item_to[]')
        kms     = request.form.getlist('item_kms[]')
        rates   = request.form.getlist('item_rate[]')
        amts    = request.form.getlist('item_amount[]')
        purpose = request.form.getlist('item_purpose[]')
        mode    = request.form.getlist('item_mode[]')
        notes   = request.form.getlist('item_notes[]')

        # Files (aligned by index with the rows)
        files = request.files.getlist('item_attachment[]') or request.files.getlist('item_file[]')

        total_kms, total_amt = 0.0, 0.0
        for i in range(len(dates)):
            if not dates[i]:  # skip blank rows
                continue

            d = datetime.strptime(dates[i], '%Y-%m-%d').date()
            k = float(kms[i] or 0)
            r = float(rates[i] or 0)
            a = float(amts[i] or (k * r))

            it = TRItem(
                request_id=tr.id,
                trip_date=d,
                from_loc=(froms[i] or '').strip(),
                to_loc=(tos[i] or '').strip(),
                kms_total=k,
                rate=r,
                amount=a,
                purpose=(purpose[i] if i < len(purpose) else None),
                mode=(mode[i] if i < len(mode) else None),
                notes=(notes[i] if i < len(notes) else None),
            )
            db.session.add(it)
            db.session.flush()  # need it.id for file naming

            # Save the attachment for this row (same positional index)
            try:
                if i < len(files):
                    f = files[i]
                    if f and f.filename:
                        rel, oname, mime = _save_tr_item_attachment(f, tr.id, it.id)
                        it.attachment_path = rel
                        it.attachment_name = oname
                        it.attachment_type = mime
            except ValueError as e:
                # Non-fatal: keep the row, warn user about the file
                flash(f"Attachment for row {i+1}: {e}", "warning")

            total_kms += k
            total_amt += a

        tr.total_kms = round(total_kms, 2)
        tr.total_amount = round(total_amt, 2)
        db.session.commit()

        # Notifications
        notify_employee(
            employee_id=current_user.id,
            title="TR submitted",
            message=f"Your TR for {tr.month:02d}/{tr.year} was submitted. Awaiting HOD.",
            link=url_for('tr.tr_view', req_id=tr.id, _external=True)
        )
        mgr_id = getattr(current_user, 'reporting_manager_id', None)
        if mgr_id:
            notify_employee(
                employee_id=mgr_id,
                title="TR approval needed",
                message=f"{current_user.full_name}'s TR ({tr.month:02d}/{tr.year}) awaits your approval.",
                link=url_for('tr.hod_pending', _external=True)
            )

        flash('✅ Travel Reimbursement request created.', 'success')
        return redirect(url_for('tr.tr_my'))

    # GET
    return render_template('tr/tr_new.html', today=today)

@tr_bp.route('/request/<int:req_id>/edit', methods=['GET', 'POST'])
@login_required
def tr_edit(req_id):
    tr = TRRequest.query.get_or_404(req_id)
    if tr.employee_id != current_user.id or tr.status not in ('pending_manager', 'rejected'):
        flash('⚠️ You can only edit your own pending/rejected request.', 'warning')
        return redirect(url_for('tr.tr_view', req_id=req_id))

    if request.method == 'POST':
        # replace all items
        TRItem.query.filter_by(request_id=tr.id).delete()

        dates   = request.form.getlist('item_date[]')
        froms   = request.form.getlist('item_from[]')
        tos     = request.form.getlist('item_to[]')
        kms     = request.form.getlist('item_kms[]')
        rates   = request.form.getlist('item_rate[]')
        amts    = request.form.getlist('item_amount[]')
        purpose = request.form.getlist('item_purpose[]')
        mode    = request.form.getlist('item_mode[]')
        notes   = request.form.getlist('item_notes[]')

        total_kms, total_amt = 0.0, 0.0
        for i in range(len(dates)):
            if not dates[i]:
                continue
            d = datetime.strptime(dates[i], '%Y-%m-%d').date()
            k = float(kms[i] or 0)
            r = float(rates[i] or 0)
            a = float(amts[i] or (k * r))
            db.session.add(TRItem(
                request_id=tr.id, trip_date=d, from_loc=froms[i], to_loc=tos[i],
                kms_total=k, rate=r, amount=a,
                purpose=(purpose[i] if i < len(purpose) else None),
                mode=(mode[i] if i < len(mode) else None),
                notes=(notes[i] if i < len(notes) else None),
            ))
            total_kms += k
            total_amt += a

        tr.total_kms = round(total_kms, 2)
        tr.total_amount = round(total_amt, 2)
        db.session.commit()
        flash('✅ Request updated.', 'success')
        return redirect(url_for('tr.tr_my'))

    return render_template('tr/tr_edit.html', tr=tr)

@tr_bp.route('/request/<int:req_id>')
@login_required
def tr_view(req_id):
    tr = TRRequest.query.get_or_404(req_id)

    # authorize view: HR/Admin (all), owner, or HOD of owner
    if _is_hr_or_admin(current_user):
        return render_template('tr/tr_view.html', tr=tr)

    team_ids = set(_hod_team_ids(current_user.id))
    if tr.employee_id == current_user.id or tr.employee_id in team_ids:
        return render_template('tr/tr_view.html', tr=tr)

    flash('❌ Not authorized.', 'danger')
    return redirect(url_for('employee.dashboard'))

# ======================================
# HOD Inbox
# ======================================

@tr_bp.route('/hod/pending')
@login_required
def hod_pending():
    """
    HODs see their team. HR/Admin can open this view too (read-only oversight of ALL).
    """
    if _is_hr_or_admin(current_user):
        base_q = TRRequest.query.order_by(TRRequest.created_on.desc())
        employees = Employee.query.order_by(Employee.full_name).all()
    else:
        team_ids = _hod_team_ids(current_user.id)
        base_q = (TRRequest.query
                  .filter(TRRequest.employee_id.in_(team_ids))
                  .order_by(TRRequest.created_on.desc()))
        employees = Employee.query.filter(Employee.id.in_(team_ids)).order_by(Employee.full_name).all()

    # filters
    emp_id = request.args.get('employee_id', type=int)
    month  = request.args.get('month', type=int)
    year   = request.args.get('year', type=int)
    # free text reserved: q = (request.args.get('q') or '').strip().lower()

    q = base_q
    if emp_id:
        q = q.filter(TRRequest.employee_id == emp_id)
    if month:
        q = q.filter(TRRequest.month == month)
    if year:
        q = q.filter(TRRequest.year == year)

    pending   = q.filter(TRRequest.status == 'pending_manager').all()
    processed = q.filter(TRRequest.status.in_(('manager_approved', 'rejected'))).all()

    return render_template(
        'tr/hod_pending.html',
        pending=pending, processed=processed,
        employees=employees, emp_id=emp_id, month=month, year=year
    )

@tr_bp.route('/hod/approve/<int:req_id>', methods=['POST'])
@login_required
def hod_approve(req_id):
    tr = TRRequest.query.get_or_404(req_id)
    if tr.status != 'pending_manager':
        flash('⚠️ Already processed.', 'warning')
        return redirect(url_for('tr.hod_pending'))

    team_ids = _hod_team_ids(current_user.id)
    if tr.employee_id not in team_ids:
        flash('❌ Not authorized.', 'danger')
        return redirect(url_for('tr.hod_pending'))

    tr.status = 'manager_approved'
    db.session.add(TRApprovalLog(
        request_id=tr.id, actor_id=current_user.id, actor_role='hod',
        action='approved', remark=request.form.get('remark', '').strip()
    ))
    db.session.commit()

    notify_employee(
        tr.employee_id, "TR approved by HOD",
        f"Your TR ({tr.month:02d}/{tr.year}) was approved by HOD and sent to HR.",
        url_for('tr.tr_view', req_id=tr.id, _external=True)
    )

    flash('✅ Approved and sent to HR.', 'success')
    return redirect(url_for('tr.hod_pending'))

@tr_bp.route('/hod/reject/<int:req_id>', methods=['POST'])
@login_required
def hod_reject(req_id):
    tr = TRRequest.query.get_or_404(req_id)
    if tr.status != 'pending_manager':
        flash('⚠️ Already processed.', 'warning')
        return redirect(url_for('tr.hod_pending'))

    team_ids = _hod_team_ids(current_user.id)
    if tr.employee_id not in team_ids:
        flash('❌ Not authorized.', 'danger')
        return redirect(url_for('tr.hod_pending'))

    tr.status = 'rejected'
    db.session.add(TRApprovalLog(
        request_id=tr.id, actor_id=current_user.id, actor_role='hod',
        action='rejected', remark=request.form.get('remark', '').strip()
    ))
    db.session.commit()

    notify_employee(
        tr.employee_id, "TR rejected by HOD",
        f"Your TR ({tr.month:02d}/{tr.year}) was rejected.",
        url_for('tr.tr_view', req_id=tr.id, _external=True)
    )

    flash('❌ Rejected.', 'danger')
    return redirect(url_for('tr.hod_pending'))

# ======================================
# HR Inbox + Disbursement (HR or Admin)
# ======================================

@tr_bp.route('/hr/pending')
@login_required
def hr_pending():
    if not _is_hr_or_admin(current_user):
        flash('❌ HR/Admin only.', 'danger')
        return redirect(url_for('employee.dashboard'))

    q = TRRequest.query.order_by(TRRequest.created_on.desc())

    emp_id = request.args.get('employee_id', type=int)
    month  = request.args.get('month', type=int)
    year   = request.args.get('year', type=int)
    status = request.args.get('status', default='pending', type=str)  # pending|approved|rejected|paid|all

    if emp_id:
        q = q.filter(TRRequest.employee_id == emp_id)
    if month:
        q = q.filter(TRRequest.month == month)
    if year:
        q = q.filter(TRRequest.year == year)

    pending   = q.filter(TRRequest.status == 'manager_approved').all()
    processed = q.filter(TRRequest.status.in_(('hr_approved', 'rejected', 'paid'))).all()

    employees = Employee.query.order_by(Employee.full_name).all()
    return render_template(
        'tr/hr_pending.html',
        pending=pending, processed=processed,
        employees=employees, emp_id=emp_id, month=month, year=year, status=status
    )

@tr_bp.route('/hr/approve/<int:req_id>', methods=['POST'])
@login_required
def hr_approve(req_id):
    if not _is_hr_or_admin(current_user):
        flash('❌ HR/Admin only.', 'danger')
        return redirect(url_for('employee.dashboard'))

    tr = TRRequest.query.get_or_404(req_id)
    if tr.status != 'manager_approved':
        flash('⚠️ Not eligible.', 'warning')
        return redirect(url_for('tr.hr_pending'))

    tr.status = 'hr_approved'
    db.session.add(TRApprovalLog(
        request_id=tr.id, actor_id=current_user.id, actor_role='hr',
        action='approved', remark=request.form.get('remark', '').strip()
    ))
    db.session.commit()

    notify_employee(
        tr.employee_id, "TR approved by HR",
        f"Your TR ({tr.month:02d}/{tr.year}) was HR approved.",
        url_for('tr.tr_view', req_id=tr.id, _external=True)
    )

    flash('✅ HR approved.', 'success')
    return redirect(url_for('tr.hr_pending'))

@tr_bp.route('/hr/mark-paid/<int:req_id>', methods=['POST'])
@login_required
def hr_mark_paid(req_id):
    if not _is_hr_or_admin(current_user):
        flash('❌ HR/Admin only.', 'danger')
        return redirect(url_for('employee.dashboard'))

    tr = TRRequest.query.get_or_404(req_id)
    if tr.status not in ('hr_approved',):  # require HR approval before payment
        flash('⚠️ Only HR-approved items can be marked paid.', 'warning')
        return redirect(url_for('tr.hr_pending'))

    tr.status = 'paid'
    db.session.add(TRApprovalLog(
        request_id=tr.id, actor_id=current_user.id, actor_role='hr',
        action='paid', remark=request.form.get('remark', '').strip()
    ))
    db.session.commit()

    notify_employee(
        tr.employee_id, "TR paid",
        f"Your TR ({tr.month:02d}/{tr.year}) has been marked as paid.",
        url_for('tr.tr_view', req_id=tr.id, _external=True)
    )

    flash('💸 Marked as paid.', 'success')
    return redirect(url_for('tr.hr_pending'))

@tr_bp.route('/hr/disbursement.csv')
@login_required
def hr_disbursement_csv():
    # Guard: HR or Admin
    if not _is_hr_or_admin(current_user):
        flash('❌ HR/Admin only.', 'danger')
        try:
            return redirect(url_for('employee.dashboard'))
        except BuildError:
            return redirect('/')

    month = request.args.get('month', type=int)
    year  = request.args.get('year', type=int)
    if not (month and year):
        flash('Pick month & year to export.', 'warning')
        return redirect(url_for('tr.hr_pending'))

    rows = (TRRequest.query
            .filter(
                TRRequest.month == month,
                TRRequest.year == year,
                TRRequest.status.in_(('hr_approved', 'paid'))
            )
            .order_by(TRRequest.employee_id)
            .all())

    # Build CSV with UTF-8 BOM (Excel-friendly)
    si = StringIO()
    w = csv.writer(si)
    w.writerow(['Employee Code','Employee Name','Bank Name','IFSC','Account No','TR Amount','Month','Year','Status'])
    for r in rows:
        emp = r.employee
        w.writerow([
            getattr(emp, 'employee_code', ''),
            getattr(emp, 'full_name', ''),
            getattr(emp, 'bank_name', '') or '',
            getattr(emp, 'ifsc_code', '') or '',
            getattr(emp, 'bank_account_number', '') or '',
            f'{(r.total_amount or 0):.2f}',
            r.month, r.year, r.status
        ])

    data_bytes = si.getvalue().encode('utf-8-sig')
    bio = BytesIO(data_bytes)
    bio.seek(0)

    filename = f"TR-Disbursement-{year}-{month:02d}.csv"
    return send_file(
        bio,
        mimetype='text/csv',
        as_attachment=True,
        download_name=filename,
        max_age=0  # avoid caching
    )

# ======================================
# “My TR” (employee-scoped)
# ======================================

@tr_bp.route('/my', methods=['GET'])
@login_required
def tr_my():
    # Filters
    month  = request.args.get('month', type=int)   # 1..12
    year   = request.args.get('year', type=int)
    status = (request.args.get('status') or '').strip().lower()  # pending_manager|manager_approved|hr_approved|rejected|paid|all

    q = (TRRequest.query
         .filter(TRRequest.employee_id == current_user.id)
         .order_by(TRRequest.created_on.desc()))

    if month:
        q = q.filter(TRRequest.month == month)
    if year:
        q = q.filter(TRRequest.year == year)
    if status and status != 'all':
        q = q.filter(TRRequest.status == status)

    my_reqs = q.all()
    return render_template('tr/tr_my.html',
                           requests=my_reqs,
                           month=month, year=year, status=status)

# ======================================
# TR Item attachment serve
# ======================================

@tr_bp.route('/item/<int:item_id>/file')
@login_required
def tr_item_file(item_id):
    item = TRItem.query.get_or_404(item_id)

    # Access control: owner, their manager (HOD), or HR/Admin
    emp = item.request.employee
    role = getattr(current_user, 'role', None)
    role_name = (getattr(role, 'name', '') or '').lower()
    is_hr_admin = role_name in ('hr', 'hr admin', 'admin', 'system admin', 'superadmin')
    allowed = (
        current_user.id == emp.id or
        current_user.id == (emp.reporting_manager_id or -1) or
        is_hr_admin
    )
    if not allowed:
        abort(403)

    if not item.attachment_path:
        abort(404)

    root = current_app.config.get('UPLOAD_FOLDER') or os.path.join(current_app.root_path, 'uploads')
    rel  = item.attachment_path.lstrip('/').replace('\\', '/')
    abs_path = rel if os.path.isabs(rel) else os.path.join(root, rel)

    if not os.path.isfile(abs_path):
        abort(404)

    mime = mimetypes.guess_type(abs_path)[0] or 'application/octet-stream'
    resp = send_file(abs_path, mimetype=mime, as_attachment=False, conditional=True)
    resp.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
    resp.headers['X-Content-Type-Options'] = 'nosniff'
    return resp