from datetime import datetime
from decimal import Decimal, InvalidOperation

from flask import Blueprint, current_app, flash, jsonify, redirect, render_template, request, url_for
from flask_babel import gettext as _
from flask_login import current_user, login_required

from app import db, log_event, track_event
from app.models import Client, Invoice, Project, Quote, QuoteAttachment, QuoteImage, QuoteItem, QuoteTemplate
from app.utils.config_manager import ConfigManager
from app.utils.db import safe_commit
from app.utils.permissions import admin_or_permission_required, permission_required
from app.utils.quote_access import quote_list_scope_user_id

quotes_bp = Blueprint("quotes", __name__)


def _parse_quote_form_date(value):
    if not value or not str(value).strip():
        return None
    try:
        return datetime.strptime(str(value).strip()[:10], "%Y-%m-%d").date()
    except ValueError:
        return None


def _pad_form_list(values, length):
    out = list(values)
    while len(out) < length:
        out.append("")
    return out


def _quote_form_inventory_context():
    """Stock + warehouse lists and JSON for quote create/edit forms."""
    import json

    from app.models import StockItem, Warehouse

    stock_items = StockItem.query.filter_by(is_active=True).order_by(StockItem.name).all()
    warehouses = Warehouse.query.filter_by(is_active=True).order_by(Warehouse.code).all()
    return {
        "stock_items": stock_items,
        "warehouses": warehouses,
        "stock_items_json": json.dumps(
            [
                {
                    "id": item.id,
                    "sku": item.sku,
                    "name": item.name,
                    "default_price": float(item.default_price) if item.default_price else None,
                    "unit": item.unit or "pcs",
                    "description": item.name,
                }
                for item in stock_items
            ]
        ),
        "warehouses_json": json.dumps([{"id": wh.id, "code": wh.code, "name": wh.name} for wh in warehouses]),
    }


@quotes_bp.route("/quotes")
@login_required
def list_quotes():
    """List all quotes with optional analytics"""
    status = request.args.get("status", "all")
    search = request.args.get("search", "").strip()
    show_analytics = request.args.get("analytics", "false").lower() == "true"

    # Use service layer for quote listing with analytics
    from app.services import QuoteService

    quote_service = QuoteService()
    result = quote_service.list_quotes(
        user_id=quote_list_scope_user_id(current_user),
        is_admin=current_user.is_admin,
        status=status,
        search=search if search else None,
        include_analytics=show_analytics,
    )

    quotes = result["quotes"]
    analytics = result.get("analytics")

    # Check if this is an AJAX request
    if request.headers.get("X-Requested-With") == "XMLHttpRequest":
        # Return only the quotes list HTML for AJAX requests
        from flask import make_response

        response = make_response(
            render_template(
                "quotes/_quotes_list.html",
                quotes=quotes,
                status=status,
                search=search,
            )
        )
        response.headers["Content-Type"] = "text/html; charset=utf-8"
        return response

    return render_template(
        "quotes/list.html",
        quotes=quotes,
        status=status,
        search=search,
        analytics=analytics,
        show_analytics=show_analytics,
    )


@quotes_bp.route("/quotes/create", methods=["GET", "POST"])
@login_required
@admin_or_permission_required("create_quotes")
def create_quote():
    """Create a new quote"""
    from app.utils.client_lock import get_locked_client_id

    clients = Client.get_active_clients()
    only_one_client = len(clients) == 1
    single_client = clients[0] if only_one_client else None

    if request.method == "POST":
        client_id = request.form.get("client_id", "").strip()
        locked_id = get_locked_client_id()
        if locked_id:
            client_id = str(locked_id)
        title = request.form.get("title", "").strip()
        description = request.form.get("description", "").strip()
        total_amount = request.form.get("total_amount", "").strip()
        hourly_rate = request.form.get("hourly_rate", "").strip()
        estimated_hours = request.form.get("estimated_hours", "").strip()
        tax_rate = request.form.get("tax_rate", "0").strip()
        currency_code = request.form.get("currency_code", "EUR").strip()
        valid_until = request.form.get("valid_until", "").strip()
        notes = request.form.get("notes", "").strip()
        terms = request.form.get("terms", "").strip()
        payment_terms = request.form.get("payment_terms", "").strip()
        discount_type = request.form.get("discount_type", "").strip()
        discount_amount = request.form.get("discount_amount", "").strip()
        discount_reason = request.form.get("discount_reason", "").strip()
        coupon_code = request.form.get("coupon_code", "").strip()
        requires_approval = request.form.get("requires_approval") == "true"

        try:
            current_app.logger.info(
                "POST /quotes/create user=%s title=%s client_id=%s",
                current_user.username,
                title or "<empty>",
                client_id or "<empty>",
            )
        except Exception:
            pass

        # Validate required fields
        if not title or not client_id:
            flash(_("Quote title and client are required"), "error")
            return render_template(
                "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
            )

        # Get client and validate
        client = Client.query.get(client_id)
        if not client:
            flash(_("Selected client not found"), "error")
            return render_template(
                "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
            )

        # Validate amounts
        try:
            total_amount = Decimal(total_amount) if total_amount else None
            if total_amount is not None and total_amount < 0:
                raise InvalidOperation
        except (InvalidOperation, ValueError):
            flash(_("Invalid total amount format"), "error")
            return render_template(
                "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
            )

        try:
            hourly_rate = Decimal(hourly_rate) if hourly_rate else None
            if hourly_rate is not None and hourly_rate < 0:
                raise InvalidOperation
        except (InvalidOperation, ValueError):
            flash(_("Invalid hourly rate format"), "error")
            return render_template(
                "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
            )

        try:
            estimated_hours = float(estimated_hours) if estimated_hours else None
            if estimated_hours is not None and estimated_hours < 0:
                raise ValueError
        except ValueError:
            flash(_("Invalid estimated hours format"), "error")
            return render_template(
                "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
            )

        try:
            tax_rate = Decimal(tax_rate) if tax_rate else Decimal("0")
            if tax_rate < 0 or tax_rate > 100:
                raise InvalidOperation
        except (InvalidOperation, ValueError):
            flash(_("Invalid tax rate format"), "error")
            return render_template(
                "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
            )

        # Validate discount fields
        discount_amount_decimal = None
        if discount_type and discount_amount:
            try:
                discount_amount_decimal = Decimal(discount_amount)
                if discount_type == "percentage":
                    if discount_amount_decimal < 0 or discount_amount_decimal > 100:
                        raise InvalidOperation
                elif discount_type == "fixed":
                    if discount_amount_decimal < 0:
                        raise InvalidOperation
                else:
                    discount_type = None  # Invalid type, ignore discount
            except (InvalidOperation, ValueError):
                flash(_("Invalid discount amount format"), "error")
                return render_template(
                    "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
                )

        # Parse valid_until date
        valid_until_date = None
        if valid_until:
            try:
                valid_until_date = datetime.strptime(valid_until, "%Y-%m-%d").date()
            except ValueError:
                flash(_("Invalid date format for valid until"), "error")
                return render_template(
                    "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
                )

        # Generate quote number
        quote_number = Quote.generate_quote_number()

        # Create quote
        quote = Quote(
            quote_number=quote_number,
            client_id=client_id,
            title=title,
            created_by=current_user.id,
            description=description,
            tax_rate=tax_rate,
            currency_code=currency_code,
            valid_until=valid_until_date,
            notes=notes,
            terms=terms,
            payment_terms=payment_terms if payment_terms else None,
            discount_type=discount_type if discount_type else None,
            discount_amount=discount_amount_decimal if discount_amount_decimal else None,
            discount_reason=discount_reason if discount_reason else None,
            coupon_code=coupon_code.upper() if coupon_code else None,
            requires_approval=requires_approval,
        )

        db.session.add(quote)
        db.session.flush()  # Get quote ID for items

        # Process line items (items + expenses + goods — issue #585)
        item_descriptions = request.form.getlist("item_description[]")
        item_quantities = request.form.getlist("item_quantity[]")
        item_prices = request.form.getlist("item_price[]")
        item_units = request.form.getlist("item_unit[]")
        item_line_sources = request.form.getlist("item_line_source[]")
        item_stock_ids = request.form.getlist("item_stock_item_id[]")
        item_warehouse_ids = request.form.getlist("item_warehouse_id[]")

        qe_titles = request.form.getlist("qe_title[]")
        qe_descriptions = request.form.getlist("qe_description[]")
        qe_categories = request.form.getlist("qe_category[]")
        qe_amounts = request.form.getlist("qe_amount[]")
        qe_dates = request.form.getlist("qe_date[]")

        qg_names = request.form.getlist("qg_name[]")
        qg_descriptions = request.form.getlist("qg_description[]")
        qg_categories = request.form.getlist("qg_category[]")
        qg_quantities = request.form.getlist("qg_quantity[]")
        qg_prices = request.form.getlist("qg_unit_price[]")
        qg_skus = request.form.getlist("qg_sku[]")

        n_items = len(item_descriptions)
        item_line_sources = _pad_form_list(item_line_sources, n_items)
        item_quantities = _pad_form_list(item_quantities, n_items)
        item_prices = _pad_form_list(item_prices, n_items)
        item_units = _pad_form_list(item_units, n_items)
        item_stock_ids = _pad_form_list(item_stock_ids, n_items)
        item_warehouse_ids = _pad_form_list(item_warehouse_ids, n_items)

        n_qe = len(qe_titles)
        qe_descriptions = _pad_form_list(qe_descriptions, n_qe)
        qe_categories = _pad_form_list(qe_categories, n_qe)
        qe_amounts = _pad_form_list(qe_amounts, n_qe)
        qe_dates = _pad_form_list(qe_dates, n_qe)

        n_qg = len(qg_names)
        qg_descriptions = _pad_form_list(qg_descriptions, n_qg)
        qg_categories = _pad_form_list(qg_categories, n_qg)
        qg_quantities = _pad_form_list(qg_quantities, n_qg)
        qg_prices = _pad_form_list(qg_prices, n_qg)
        qg_skus = _pad_form_list(qg_skus, n_qg)

        line_position = 0

        for desc, qty, price, unit, src, stock_id, wh_id in zip(
            item_descriptions,
            item_quantities,
            item_prices,
            item_units,
            item_line_sources,
            item_stock_ids,
            item_warehouse_ids,
        ):
            use_stock = (src or "").strip().lower() == "stock"
            try:
                stock_item_id = int(stock_id) if stock_id and str(stock_id).strip() and use_stock else None
                warehouse_id = int(wh_id) if wh_id and str(wh_id).strip() and use_stock else None
            except (TypeError, ValueError):
                stock_item_id, warehouse_id = None, None
            if not use_stock:
                stock_item_id, warehouse_id = None, None
            desc_s = (desc or "").strip()
            if not desc_s and not stock_item_id:
                continue
            try:
                q_dec = Decimal(qty) if qty and str(qty).strip() else Decimal("1")
                p_dec = Decimal(price) if price and str(price).strip() else Decimal("0")
                item = QuoteItem(
                    quote_id=quote.id,
                    description=desc_s or "-",
                    quantity=q_dec,
                    unit_price=p_dec,
                    unit=unit.strip() if unit and str(unit).strip() else None,
                    stock_item_id=stock_item_id,
                    warehouse_id=warehouse_id,
                    position=line_position,
                    line_kind="item",
                )
                db.session.add(item)
                line_position += 1
            except (ValueError, InvalidOperation):
                pass

        for title, qe_desc, cat, amount, qe_d in zip(
            qe_titles, qe_descriptions, qe_categories, qe_amounts, qe_dates
        ):
            title_s = (title or "").strip()
            qe_desc_s = (qe_desc or "").strip()
            if not title_s and not qe_desc_s and not (amount and str(amount).strip()):
                continue
            try:
                amt = Decimal(amount) if amount and str(amount).strip() else Decimal("0")
            except (InvalidOperation, ValueError):
                continue
            if amt <= 0 and not title_s and not qe_desc_s:
                continue
            ld = _parse_quote_form_date(qe_d)
            cat_s = (cat or "").strip() or None
            try:
                item = QuoteItem(
                    quote_id=quote.id,
                    description=qe_desc_s if qe_desc_s else (title_s or "-"),
                    quantity=Decimal("1"),
                    unit_price=amt,
                    line_kind="expense",
                    display_name=title_s or None,
                    category=cat_s,
                    line_date=ld,
                    position=line_position,
                )
                db.session.add(item)
                line_position += 1
            except (InvalidOperation, ValueError):
                pass

        for name, g_desc, g_cat, g_qty, g_price, g_sku in zip(
            qg_names, qg_descriptions, qg_categories, qg_quantities, qg_prices, qg_skus
        ):
            name_s = (name or "").strip()
            g_desc_s = (g_desc or "").strip()
            if not name_s and not g_desc_s:
                continue
            try:
                gq = Decimal(g_qty) if g_qty and str(g_qty).strip() else Decimal("1")
                gp = Decimal(g_price) if g_price and str(g_price).strip() else Decimal("0")
            except (InvalidOperation, ValueError):
                continue
            if gq <= 0 or gp < 0:
                continue
            g_cat_s = (g_cat or "").strip() or None
            g_sku_s = (g_sku or "").strip() or None
            try:
                item = QuoteItem(
                    quote_id=quote.id,
                    description=g_desc_s if g_desc_s else (name_s or "-"),
                    quantity=gq,
                    unit_price=gp,
                    line_kind="good",
                    display_name=name_s or None,
                    category=g_cat_s,
                    sku=g_sku_s,
                    position=line_position,
                )
                db.session.add(item)
                line_position += 1
            except (InvalidOperation, ValueError):
                pass

        quote.calculate_totals()

        if not safe_commit("create_quote", {"title": title, "client_id": client_id}):
            flash(_("Could not create quote due to a database error. Please check server logs."), "error")
            return render_template(
                "quotes/create.html",
                clients=clients,
                only_one_client=only_one_client,
                single_client=single_client,
                **_quote_form_inventory_context(),
            )

        # Log event
        log_event("quote.created", user_id=current_user.id, quote_id=quote.id, quote_title=title, client_id=client_id)
        track_event(
            current_user.id, "quote.created", {"quote_id": quote.id, "quote_title": title, "client_id": client_id}
        )

        flash(_("Quote created successfully"), "success")
        return redirect(url_for("quotes.view_quote", quote_id=quote.id))

    return render_template(
        "quotes/create.html",
        clients=clients,
        only_one_client=only_one_client,
        single_client=single_client,
        **_quote_form_inventory_context(),
    )


@quotes_bp.route("/quotes/<int:quote_id>")
@login_required
def view_quote(quote_id):
    """View quote details"""
    from sqlalchemy.orm import joinedload

    from app.models import Comment
    from app.services import QuoteService

    # Use service layer with eager loading
    quote_service = QuoteService()
    quote = quote_service.get_quote_with_details(
        quote_id=quote_id,
        user_id=quote_list_scope_user_id(current_user),
        is_admin=current_user.is_admin,
    )

    if not quote:
        flash(_("Quote not found"), "error")
        return redirect(url_for("quotes.list_quotes"))

    quote.calculate_totals()  # Ensure totals are up to date

    # Get all comments (both internal and client-facing)
    comments = Comment.get_quote_comments(quote_id, include_replies=True, include_internal=True)

    return render_template("quotes/view.html", quote=quote, comments=comments)


@quotes_bp.route("/quotes/<int:quote_id>/edit", methods=["GET", "POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def edit_quote(quote_id):
    """Edit an quote"""
    from sqlalchemy.orm import joinedload, selectinload

    quote = (
        Quote.query.options(joinedload(Quote.client), selectinload(Quote.items)).filter_by(id=quote_id).first_or_404()
    )

    # Only allow editing draft quotes
    if quote.status != "draft":
        flash(_("Only draft quotes can be edited"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if request.method == "POST":
        title = request.form.get("title", "").strip()
        description = request.form.get("description", "").strip()
        tax_rate = request.form.get("tax_rate", "0").strip()
        currency_code = request.form.get("currency_code", "EUR").strip()
        valid_until = request.form.get("valid_until", "").strip()
        notes = request.form.get("notes", "").strip()
        terms = request.form.get("terms", "").strip()
        payment_terms = request.form.get("payment_terms", "").strip()
        visible_to_client = request.form.get("visible_to_client") == "on"

        # Discount fields
        discount_type = request.form.get("discount_type", "").strip()
        discount_amount = request.form.get("discount_amount", "").strip()
        discount_reason = request.form.get("discount_reason", "").strip()
        coupon_code = request.form.get("coupon_code", "").strip()

        try:
            tax_rate = Decimal(tax_rate) if tax_rate else Decimal("0")
            if tax_rate < 0 or tax_rate > 100:
                raise InvalidOperation
        except (InvalidOperation, ValueError):
            flash(_("Invalid tax rate format"), "error")
            inv = _quote_form_inventory_context()
            return render_template("quotes/edit.html", quote=quote, clients=Client.get_active_clients(), **inv)

        # Validate discount fields
        discount_amount_decimal = None
        if discount_type and discount_amount:
            try:
                discount_amount_decimal = Decimal(discount_amount)
                if discount_type == "percentage":
                    if discount_amount_decimal < 0 or discount_amount_decimal > 100:
                        raise InvalidOperation
                elif discount_type == "fixed":
                    if discount_amount_decimal < 0:
                        raise InvalidOperation
                else:
                    discount_type = None  # Invalid type, ignore discount
            except (InvalidOperation, ValueError):
                flash(_("Invalid discount amount format"), "error")
                inv = _quote_form_inventory_context()
                return render_template("quotes/edit.html", quote=quote, clients=Client.get_active_clients(), **inv)

        # Parse valid_until date
        valid_until_date = None
        if valid_until:
            try:
                valid_until_date = datetime.strptime(valid_until, "%Y-%m-%d").date()
            except ValueError:
                flash(_("Invalid date format for valid until"), "error")
                inv = _quote_form_inventory_context()
                return render_template("quotes/edit.html", quote=quote, clients=Client.get_active_clients(), **inv)

        # Update quote
        quote.title = title
        quote.description = description.strip() if description else None
        quote.tax_rate = tax_rate
        quote.currency_code = currency_code
        quote.valid_until = valid_until_date
        quote.notes = notes.strip() if notes else None
        quote.terms = terms.strip() if terms else None
        quote.payment_terms = payment_terms.strip() if payment_terms else None
        quote.visible_to_client = visible_to_client

        # Notify client if quote is made visible
        if visible_to_client and quote.client_id:
            try:
                from app.services.client_notification_service import ClientNotificationService

                notification_service = ClientNotificationService()
                notification_service.notify_quote_available(quote.id, quote.client_id)
            except Exception as e:
                current_app.logger.error(f"Failed to send client notification for quote {quote.id}: {e}", exc_info=True)

        # Update discount fields
        quote.discount_type = discount_type if discount_type else None
        quote.discount_amount = discount_amount_decimal if discount_amount_decimal else None
        quote.discount_reason = discount_reason.strip() if discount_reason else None
        quote.coupon_code = coupon_code.upper().strip() if coupon_code else None

        # Update line items (items + expenses + goods — issue #585)
        item_ids = request.form.getlist("item_id[]")
        item_descriptions = request.form.getlist("item_description[]")
        item_quantities = request.form.getlist("item_quantity[]")
        item_prices = request.form.getlist("item_price[]")
        item_units = request.form.getlist("item_unit[]")
        item_line_sources = request.form.getlist("item_line_source[]")
        item_stock_ids = request.form.getlist("item_stock_item_id[]")
        item_warehouse_ids = request.form.getlist("item_warehouse_id[]")

        qe_ids = request.form.getlist("qe_id[]")
        qe_titles = request.form.getlist("qe_title[]")
        qe_descriptions = request.form.getlist("qe_description[]")
        qe_categories = request.form.getlist("qe_category[]")
        qe_amounts = request.form.getlist("qe_amount[]")
        qe_dates = request.form.getlist("qe_date[]")

        qg_ids = request.form.getlist("qg_id[]")
        qg_names = request.form.getlist("qg_name[]")
        qg_descriptions = request.form.getlist("qg_description[]")
        qg_categories = request.form.getlist("qg_category[]")
        qg_quantities = request.form.getlist("qg_quantity[]")
        qg_prices = request.form.getlist("qg_unit_price[]")
        qg_skus = request.form.getlist("qg_sku[]")

        n_items = len(item_descriptions)
        item_ids = _pad_form_list(item_ids, n_items)
        item_line_sources = _pad_form_list(item_line_sources, n_items)
        item_quantities = _pad_form_list(item_quantities, n_items)
        item_prices = _pad_form_list(item_prices, n_items)
        item_units = _pad_form_list(item_units, n_items)
        item_stock_ids = _pad_form_list(item_stock_ids, n_items)
        item_warehouse_ids = _pad_form_list(item_warehouse_ids, n_items)

        n_qe = len(qe_titles)
        qe_ids = _pad_form_list(qe_ids, n_qe)
        qe_descriptions = _pad_form_list(qe_descriptions, n_qe)
        qe_categories = _pad_form_list(qe_categories, n_qe)
        qe_amounts = _pad_form_list(qe_amounts, n_qe)
        qe_dates = _pad_form_list(qe_dates, n_qe)

        n_qg = len(qg_names)
        qg_ids = _pad_form_list(qg_ids, n_qg)
        qg_descriptions = _pad_form_list(qg_descriptions, n_qg)
        qg_categories = _pad_form_list(qg_categories, n_qg)
        qg_quantities = _pad_form_list(qg_quantities, n_qg)
        qg_prices = _pad_form_list(qg_prices, n_qg)
        qg_skus = _pad_form_list(qg_skus, n_qg)

        existing_item_ids = set()
        for raw in item_ids + qe_ids + qg_ids:
            if raw and str(raw).strip():
                try:
                    existing_item_ids.add(int(raw))
                except (TypeError, ValueError):
                    pass
        for row in list(quote.items):
            if row.id not in existing_item_ids:
                db.session.delete(row)

        line_position = 0

        for item_id, desc, qty, price, unit, src, stock_id, wh_id in zip(
            item_ids,
            item_descriptions,
            item_quantities,
            item_prices,
            item_units,
            item_line_sources,
            item_stock_ids,
            item_warehouse_ids,
        ):
            use_stock = (src or "").strip().lower() == "stock"
            try:
                stock_item_id = int(stock_id) if stock_id and str(stock_id).strip() and use_stock else None
                warehouse_id = int(wh_id) if wh_id and str(wh_id).strip() and use_stock else None
            except (TypeError, ValueError):
                stock_item_id, warehouse_id = None, None
            if not use_stock:
                stock_item_id, warehouse_id = None, None
            desc_s = (desc or "").strip()
            if not desc_s and not stock_item_id:
                continue
            try:
                q_dec = Decimal(qty) if qty and str(qty).strip() else Decimal("1")
                p_dec = Decimal(price) if price and str(price).strip() else Decimal("0")
            except (InvalidOperation, ValueError):
                continue
            try:
                if item_id and str(item_id).strip():
                    item = QuoteItem.query.get(int(item_id))
                    if not item or item.quote_id != quote.id:
                        continue
                    item.line_kind = "item"
                    item.display_name = None
                    item.category = None
                    item.line_date = None
                    item.sku = None
                    item.description = desc_s or "-"
                    item.quantity = q_dec
                    item.unit_price = p_dec
                    item.total_amount = q_dec * p_dec
                    item.unit = unit.strip() if unit and str(unit).strip() else None
                    item.stock_item_id = stock_item_id
                    item.warehouse_id = warehouse_id
                    item.is_stock_item = stock_item_id is not None
                    item.position = line_position
                else:
                    item = QuoteItem(
                        quote_id=quote.id,
                        description=desc_s or "-",
                        quantity=q_dec,
                        unit_price=p_dec,
                        unit=unit.strip() if unit and str(unit).strip() else None,
                        stock_item_id=stock_item_id,
                        warehouse_id=warehouse_id,
                        position=line_position,
                        line_kind="item",
                    )
                    db.session.add(item)
                line_position += 1
            except (TypeError, ValueError, InvalidOperation):
                pass

        for qe_id, title, qe_desc, cat, amount, qe_d in zip(
            qe_ids, qe_titles, qe_descriptions, qe_categories, qe_amounts, qe_dates
        ):
            title_s = (title or "").strip()
            qe_desc_s = (qe_desc or "").strip()
            if not title_s and not qe_desc_s and not (amount and str(amount).strip()):
                continue
            try:
                amt = Decimal(amount) if amount and str(amount).strip() else Decimal("0")
            except (InvalidOperation, ValueError):
                continue
            if amt <= 0 and not title_s and not qe_desc_s:
                continue
            ld = _parse_quote_form_date(qe_d)
            cat_s = (cat or "").strip() or None
            try:
                if qe_id and str(qe_id).strip():
                    item = QuoteItem.query.get(int(qe_id))
                    if not item or item.quote_id != quote.id:
                        continue
                    item.line_kind = "expense"
                    item.display_name = title_s or None
                    item.description = qe_desc_s if qe_desc_s else (title_s or "-")
                    item.category = cat_s
                    item.line_date = ld
                    item.sku = None
                    item.quantity = Decimal("1")
                    item.unit_price = amt
                    item.total_amount = amt
                    item.unit = None
                    item.stock_item_id = None
                    item.warehouse_id = None
                    item.is_stock_item = False
                    item.position = line_position
                else:
                    item = QuoteItem(
                        quote_id=quote.id,
                        description=qe_desc_s if qe_desc_s else (title_s or "-"),
                        quantity=Decimal("1"),
                        unit_price=amt,
                        line_kind="expense",
                        display_name=title_s or None,
                        category=cat_s,
                        line_date=ld,
                        position=line_position,
                    )
                    db.session.add(item)
                line_position += 1
            except (TypeError, ValueError, InvalidOperation):
                pass

        for qg_id, name, g_desc, g_cat, g_qty, g_price, g_sku in zip(
            qg_ids, qg_names, qg_descriptions, qg_categories, qg_quantities, qg_prices, qg_skus
        ):
            name_s = (name or "").strip()
            g_desc_s = (g_desc or "").strip()
            if not name_s and not g_desc_s:
                continue
            try:
                gq = Decimal(g_qty) if g_qty and str(g_qty).strip() else Decimal("1")
                gp = Decimal(g_price) if g_price and str(g_price).strip() else Decimal("0")
            except (InvalidOperation, ValueError):
                continue
            if gq <= 0 or gp < 0:
                continue
            g_cat_s = (g_cat or "").strip() or None
            g_sku_s = (g_sku or "").strip() or None
            try:
                if qg_id and str(qg_id).strip():
                    item = QuoteItem.query.get(int(qg_id))
                    if not item or item.quote_id != quote.id:
                        continue
                    item.line_kind = "good"
                    item.display_name = name_s or None
                    item.description = g_desc_s if g_desc_s else (name_s or "-")
                    item.category = g_cat_s
                    item.line_date = None
                    item.sku = g_sku_s
                    item.quantity = gq
                    item.unit_price = gp
                    item.total_amount = gq * gp
                    item.unit = None
                    item.stock_item_id = None
                    item.warehouse_id = None
                    item.is_stock_item = False
                    item.position = line_position
                else:
                    item = QuoteItem(
                        quote_id=quote.id,
                        description=g_desc_s if g_desc_s else (name_s or "-"),
                        quantity=gq,
                        unit_price=gp,
                        line_kind="good",
                        display_name=name_s or None,
                        category=g_cat_s,
                        sku=g_sku_s,
                        position=line_position,
                    )
                    db.session.add(item)
                line_position += 1
            except (TypeError, ValueError, InvalidOperation):
                pass

        quote.calculate_totals()

        if not safe_commit("edit_quote", {"quote_id": quote_id}):
            flash(_("Could not update quote due to a database error. Please check server logs."), "error")
            inv = _quote_form_inventory_context()
            return render_template(
                "quotes/edit.html",
                quote=quote,
                clients=Client.get_active_clients(),
                **inv,
            )

        log_event("quote.updated", user_id=current_user.id, quote_id=quote.id, quote_title=title)
        track_event(current_user.id, "quote.updated", {"quote_id": quote.id, "quote_title": title})

        flash(_("Quote updated successfully"), "success")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    inv = _quote_form_inventory_context()
    return render_template(
        "quotes/edit.html",
        quote=quote,
        clients=Client.get_active_clients(),
        **inv,
    )


@quotes_bp.route("/quotes/<int:quote_id>/send", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def send_quote(quote_id):
    """Send an quote to the client"""
    quote = Quote.query.get_or_404(quote_id)

    if not quote.can_be_sent:
        if quote.requires_approval and quote.approval_status != "approved":
            flash(_("Quote must be approved before it can be sent"), "error")
        else:
            flash(_("Only draft quotes can be sent"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    try:
        quote.send()
    except ValueError as e:
        flash(_("Cannot send quote: %(error)s", error=str(e)), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # Reserve stock for quote items if enabled
    import os

    from app.models import StockReservation

    auto_reserve_on_send = os.getenv("INVENTORY_AUTO_RESERVE_ON_QUOTE_SENT", "false").lower() == "true"
    if auto_reserve_on_send:
        for item in quote.items:
            if item.is_stock_item and item.stock_item_id and item.warehouse_id:
                try:
                    expires_in_days = ConfigManager.get_setting("INVENTORY_QUOTE_RESERVATION_EXPIRY_DAYS", 30)
                    StockReservation.create_reservation(
                        stock_item_id=item.stock_item_id,
                        warehouse_id=item.warehouse_id,
                        quantity=item.quantity,
                        reservation_type="quote",
                        reservation_id=quote.id,
                        reserved_by=current_user.id,
                        expires_in_days=expires_in_days,
                    )
                except ValueError as e:
                    flash(
                        _(
                            "Warning: Could not reserve stock for item %(item)s: %(error)s",
                            item=item.description,
                            error=str(e),
                        ),
                        "warning",
                    )

    if not safe_commit("send_quote", {"quote_id": quote_id}):
        flash(_("Could not send quote due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # Send notifications
    from app.models import User
    from app.utils.email import send_quote_sent_notification

    # Notify quote creator
    if quote.creator and quote.creator.email:
        send_quote_sent_notification(quote, quote.creator)

    # Notify admins
    # Use the User.is_admin property so RBAC Role-based admins are notified, not just legacy role="admin" users.
    admins = [u for u in User.query.filter_by(is_active=True).all() if u.is_admin]
    for admin in admins:
        if admin.id != quote.creator_id and admin.email:
            send_quote_sent_notification(quote, admin)

    log_event("quote.sent", user_id=current_user.id, quote_id=quote.id, quote_title=quote.title)
    track_event(current_user.id, "quote.sent", {"quote_id": quote.id, "quote_title": quote.title})

    flash(_("Quote sent successfully"), "success")
    return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/accept", methods=["GET", "POST"])
@login_required
@admin_or_permission_required("accept_quotes")
def accept_quote(quote_id):
    """Accept an quote and create a project"""
    quote = Quote.query.get_or_404(quote_id)

    if not quote.can_be_accepted:
        flash(_("This quote cannot be accepted"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if request.method == "POST":
        # Create project from quote
        project_name = request.form.get("project_name", quote.title).strip()
        if not project_name:
            project_name = quote.title

        # Calculate totals to get budget amount
        quote.calculate_totals()
        budget_amount = quote.total_amount

        # Create project
        project = Project(
            name=project_name,
            client_id=quote.client_id,
            description=quote.description,
            billable=True,
            budget_amount=budget_amount,
            quote_id=quote.id,
            status="active",
        )

        db.session.add(project)

        # Accept the quote
        try:
            db.session.flush()  # Get project ID
            quote.accept(current_user.id, project.id)
        except ValueError as e:
            flash(_("Could not accept quote: %(error)s", error=str(e)), "error")
            db.session.rollback()
            return redirect(url_for("quotes.view_quote", quote_id=quote_id))

        # Reserve stock for quote items when accepted (if not already reserved)
        import os

        from app.models import StockReservation

        for item in quote.items:
            if item.is_stock_item and item.stock_item_id and item.warehouse_id:
                # Check if reservation already exists
                existing = StockReservation.query.filter_by(
                    stock_item_id=item.stock_item_id,
                    warehouse_id=item.warehouse_id,
                    reservation_type="quote",
                    reservation_id=quote.id,
                    status="reserved",
                ).first()

                if not existing:
                    try:
                        expires_in_days = int(os.getenv("INVENTORY_QUOTE_RESERVATION_EXPIRY_DAYS", "30"))
                        StockReservation.create_reservation(
                            stock_item_id=item.stock_item_id,
                            warehouse_id=item.warehouse_id,
                            quantity=item.quantity,
                            reservation_type="quote",
                            reservation_id=quote.id,
                            reserved_by=current_user.id,
                            expires_in_days=expires_in_days,
                        )
                    except ValueError as e:
                        flash(
                            _(
                                "Warning: Could not reserve stock for item %(item)s: %(error)s",
                                item=item.description,
                                error=str(e),
                            ),
                            "warning",
                        )

        if not safe_commit("accept_quote", {"quote_id": quote_id, "project_id": project.id}):
            flash(_("Could not accept quote due to a database error. Please check server logs."), "error")
            return redirect(url_for("quotes.view_quote", quote_id=quote_id))

        # Send notifications
        from app.models import User
        from app.utils.email import send_quote_accepted_notification

        # Notify quote creator
        if quote.creator and quote.creator.email:
            send_quote_accepted_notification(quote, quote.creator)

        # Notify admins (use User.is_admin so RBAC Role-based admins are notified, not just legacy role="admin").
        admins = [u for u in User.query.filter_by(is_active=True).all() if u.is_admin]
        for admin in admins:
            if admin.id != quote.creator_id and admin.email:
                send_quote_accepted_notification(quote, admin)

        log_event(
            "quote.accepted", user_id=current_user.id, quote_id=quote.id, quote_title=quote.title, project_id=project.id
        )
        track_event(
            current_user.id,
            "quote.accepted",
            {"quote_id": quote.id, "quote_title": quote.title, "project_id": project.id},
        )

        flash(_("Quote accepted and project created successfully"), "success")
        return redirect(url_for("projects.view_project", project_id=project.id))

    return render_template("quotes/accept.html", quote=quote)


@quotes_bp.route("/quotes/<int:quote_id>/reject", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def reject_quote(quote_id):
    """Reject an quote"""
    quote = Quote.query.get_or_404(quote_id)

    if quote.status not in ["sent", "draft"]:
        flash(_("This quote cannot be rejected"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    try:
        quote.reject()
    except ValueError as e:
        flash(_("Could not reject quote: %(error)s", error=str(e)), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if not safe_commit("reject_quote", {"quote_id": quote_id}):
        flash(_("Could not reject quote due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    log_event("quote.rejected", user_id=current_user.id, quote_id=quote.id, quote_title=quote.title)
    track_event(current_user.id, "quote.rejected", {"quote_id": quote.id, "quote_title": quote.title})

    flash(_("Quote rejected"), "success")
    return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/delete", methods=["POST"])
@login_required
@admin_or_permission_required("delete_quotes")
def delete_quote(quote_id):
    """Delete an quote"""
    quote = Quote.query.get_or_404(quote_id)

    # Only allow deleting draft or rejected quotes
    if quote.status not in ["draft", "rejected"]:
        flash(_("Only draft or rejected quotes can be deleted"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    quote_title = quote.title
    db.session.delete(quote)

    if not safe_commit("delete_quote", {"quote_id": quote_id}):
        flash(_("Could not delete quote due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    log_event("quote.deleted", user_id=current_user.id, quote_id=quote_id, quote_title=quote_title)
    track_event(current_user.id, "quote.deleted", {"quote_id": quote_id, "quote_title": quote_title})

    flash(_("Quote deleted successfully"), "success")
    return redirect(url_for("quotes.list_quotes"))


@quotes_bp.route("/quotes/<int:quote_id>/attachments/upload", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def upload_attachment(quote_id):
    """Upload an attachment to a quote"""
    import os
    from datetime import datetime

    from flask import current_app
    from werkzeug.utils import secure_filename

    quote = Quote.query.get_or_404(quote_id)

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        flash(_("You do not have permission to upload attachments to this quote"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # File upload configuration
    ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "pdf", "doc", "docx", "txt", "xls", "xlsx", "zip", "rar"}
    UPLOAD_FOLDER = "app/static/uploads/quote_attachments"
    MAX_FILE_SIZE = 10 * 1024 * 1024  # 10 MB

    def allowed_file(filename):
        return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS

    if "file" not in request.files:
        flash(_("No file provided"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    file = request.files["file"]
    if file.filename == "":
        flash(_("No file selected"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if not allowed_file(file.filename):
        flash(_("File type not allowed"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # Check file size
    file.seek(0, os.SEEK_END)
    file_size = file.tell()
    file.seek(0)

    if file_size > MAX_FILE_SIZE:
        flash(_("File size exceeds maximum allowed size (10 MB)"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # Save file
    original_filename = secure_filename(file.filename)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"{quote_id}_{timestamp}_{original_filename}"

    # Ensure upload directory exists
    upload_dir = os.path.join(current_app.root_path, "..", UPLOAD_FOLDER)
    os.makedirs(upload_dir, exist_ok=True)

    file_path = os.path.join(upload_dir, filename)
    file.save(file_path)

    # Get file info
    mime_type = file.content_type or "application/octet-stream"
    description = request.form.get("description", "").strip() or None
    is_visible_to_client = request.form.get("is_visible_to_client", "false").lower() == "true"

    # Notify client if quote is being made visible
    if is_visible_to_client and not quote.visible_to_client and quote.client_id:
        try:
            from app.services.client_notification_service import ClientNotificationService

            notification_service = ClientNotificationService()
            notification_service.notify_quote_available(quote.id, quote.client_id)
        except Exception as e:
            current_app.logger.error(f"Failed to send client notification for quote {quote.id}: {e}", exc_info=True)

    # Create attachment record
    attachment = QuoteAttachment(
        quote_id=quote_id,
        filename=filename,
        original_filename=original_filename,
        file_path=os.path.join(UPLOAD_FOLDER, filename),
        file_size=file_size,
        uploaded_by=current_user.id,
        mime_type=mime_type,
        description=description,
        is_visible_to_client=is_visible_to_client,
    )

    db.session.add(attachment)

    if not safe_commit("upload_quote_attachment", {"quote_id": quote_id, "attachment_id": attachment.id}):
        flash(_("Could not upload attachment due to a database error. Please check server logs."), "error")
        # Clean up uploaded file
        try:
            os.remove(file_path)
        except OSError as e:
            current_app.logger.warning(f"Failed to remove uploaded file {file_path}: {e}")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    log_event(
        "quote.attachment.uploaded",
        user_id=current_user.id,
        quote_id=quote_id,
        attachment_id=attachment.id,
        filename=original_filename,
    )
    track_event(
        current_user.id,
        "quote.attachment.uploaded",
        {"quote_id": quote_id, "attachment_id": attachment.id, "filename": original_filename},
    )

    flash(_("Attachment uploaded successfully"), "success")
    return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/attachments/<int:attachment_id>/download")
@login_required
def download_attachment(attachment_id):
    """Download a quote attachment"""
    import os

    from flask import current_app, send_file

    attachment = QuoteAttachment.query.get_or_404(attachment_id)
    quote = attachment.quote

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        flash(_("You do not have permission to download this attachment"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote.id))

    # Build file path
    file_path = os.path.join(current_app.root_path, "..", attachment.file_path)

    if not os.path.exists(file_path):
        flash(_("File not found"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote.id))

    return send_file(
        file_path, as_attachment=True, download_name=attachment.original_filename, mimetype=attachment.mime_type
    )


@quotes_bp.route("/quotes/attachments/<int:attachment_id>/delete", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def delete_attachment(attachment_id):
    """Delete a quote attachment"""
    import os

    from flask import current_app

    attachment = QuoteAttachment.query.get_or_404(attachment_id)
    quote = attachment.quote

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        flash(_("You do not have permission to delete this attachment"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote.id))

    # Delete file
    file_path = os.path.join(current_app.root_path, "..", attachment.file_path)
    if os.path.exists(file_path):
        try:
            os.remove(file_path)
        except Exception as e:
            current_app.logger.error(f"Failed to delete attachment file: {e}")

    # Delete database record
    attachment_id_for_log = attachment.id
    quote_id = quote.id
    db.session.delete(attachment)

    if not safe_commit("delete_quote_attachment", {"attachment_id": attachment_id_for_log}):
        flash(_("Could not delete attachment due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    log_event(
        "quote.attachment.deleted", user_id=current_user.id, quote_id=quote_id, attachment_id=attachment_id_for_log
    )
    track_event(
        current_user.id, "quote.attachment.deleted", {"quote_id": quote_id, "attachment_id": attachment_id_for_log}
    )

    flash(_("Attachment deleted successfully"), "success")
    return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/request-approval", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def request_approval(quote_id):
    """Request approval for a quote"""
    quote = Quote.query.get_or_404(quote_id)

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        flash(_("You do not have permission to request approval for this quote"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if not quote.requires_approval:
        flash(_("This quote does not require approval"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    try:
        quote.request_approval()
    except ValueError as e:
        flash(_("Cannot request approval: %(error)s", error=str(e)), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if not safe_commit("request_quote_approval", {"quote_id": quote_id}):
        flash(_("Could not request approval due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # Send notification to approvers
    from app.models import User
    from app.utils.email import send_quote_approval_request_notification

    # Notify admins (default approvers)
    # Use the User.is_admin property so RBAC Role-based admins are notified, not just legacy role="admin" users.
    admins = [u for u in User.query.filter_by(is_active=True).all() if u.is_admin]
    for admin in admins:
        if admin.email:
            send_quote_approval_request_notification(quote, admin)

    log_event("quote.approval.requested", user_id=current_user.id, quote_id=quote.id, quote_title=quote.title)
    track_event(current_user.id, "quote.approval.requested", {"quote_id": quote.id, "quote_title": quote.title})

    flash(_("Approval requested successfully"), "success")
    return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/approve", methods=["POST"])
@login_required
@admin_or_permission_required("approve_quotes")
def approve_quote(quote_id):
    """Approve a quote"""
    quote = Quote.query.get_or_404(quote_id)

    if not quote.requires_approval:
        flash(_("This quote does not require approval"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if quote.approval_status != "pending":
        flash(_("This quote is not pending approval"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    notes = request.form.get("notes", "").strip() or None

    try:
        quote.approve(current_user.id, notes)
    except ValueError as e:
        flash(_("Cannot approve quote: %(error)s", error=str(e)), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if not safe_commit("approve_quote", {"quote_id": quote_id}):
        flash(_("Could not approve quote due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # Send notification to quote creator
    from app.utils.email import send_quote_approved_notification

    if quote.creator and quote.creator.email:
        send_quote_approved_notification(quote, quote.creator)

    log_event("quote.approved", user_id=current_user.id, quote_id=quote.id, quote_title=quote.title)
    track_event(current_user.id, "quote.approved", {"quote_id": quote.id, "quote_title": quote.title})

    flash(_("Quote approved successfully"), "success")
    return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/reject-approval", methods=["POST"])
@login_required
@admin_or_permission_required("approve_quotes")
def reject_approval(quote_id):
    """Reject a quote in approval workflow"""
    quote = Quote.query.get_or_404(quote_id)

    if not quote.requires_approval:
        flash(_("This quote does not require approval"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if quote.approval_status != "pending":
        flash(_("This quote is not pending approval"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    reason = request.form.get("reason", "").strip()
    if not reason:
        flash(_("Rejection reason is required"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    try:
        quote.reject_approval(current_user.id, reason)
    except ValueError as e:
        flash(_("Cannot reject quote: %(error)s", error=str(e)), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    if not safe_commit("reject_quote_approval", {"quote_id": quote_id}):
        flash(_("Could not reject quote due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # Send notification to quote creator
    from app.utils.email import send_quote_approval_rejected_notification

    if quote.creator and quote.creator.email:
        send_quote_approval_rejected_notification(quote, quote.creator)

    log_event("quote.approval.rejected", user_id=current_user.id, quote_id=quote.id, quote_title=quote.title)
    track_event(current_user.id, "quote.approval.rejected", {"quote_id": quote.id, "quote_title": quote.title})

    flash(_("Quote approval rejected"), "success")
    return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/templates")
@login_required
def list_templates():
    """List all quote templates"""
    templates = QuoteTemplate.get_user_templates(current_user.id, include_public=True)
    return render_template("quotes/templates.html", templates=templates)


@quotes_bp.route("/quotes/templates/create", methods=["GET", "POST"])
@login_required
@admin_or_permission_required("create_quotes")
def create_template():
    """Create a new quote template"""
    if request.method == "POST":
        name = request.form.get("name", "").strip()
        description = request.form.get("description", "").strip() or None

        if not name:
            flash(_("Template name is required"), "error")
            return render_template("quotes/template_form.html")

        # Get template settings
        default_tax_rate = request.form.get("default_tax_rate", "0").strip()
        default_currency_code = request.form.get("default_currency_code", "EUR").strip()
        default_payment_terms = request.form.get("default_payment_terms", "").strip() or None
        default_terms = request.form.get("default_terms", "").strip() or None
        default_valid_until_days = request.form.get("default_valid_until_days", type=int) or 30
        default_requires_approval = request.form.get("default_requires_approval", "false").lower() == "true"
        default_approval_level = request.form.get("default_approval_level", type=int) or 1
        is_public = request.form.get("is_public", "false").lower() == "true"

        try:
            default_tax_rate = Decimal(default_tax_rate) if default_tax_rate else Decimal("0")
        except (ValueError, InvalidOperation):
            default_tax_rate = Decimal("0")

        # Get default items
        item_descriptions = request.form.getlist("item_description[]")
        item_quantities = request.form.getlist("item_quantity[]")
        item_prices = request.form.getlist("item_price[]")
        item_units = request.form.getlist("item_unit[]")

        default_items = []
        for desc, qty, price, unit in zip(item_descriptions, item_quantities, item_prices, item_units):
            if desc.strip():
                default_items.append(
                    {
                        "description": desc.strip(),
                        "quantity": float(qty) if qty else 1,
                        "unit_price": float(price) if price else 0,
                        "unit": unit.strip() if unit else None,
                    }
                )

        # Create template
        template = QuoteTemplate(
            name=name,
            created_by=current_user.id,
            description=description,
            default_tax_rate=default_tax_rate,
            default_currency_code=default_currency_code,
            default_payment_terms=default_payment_terms,
            default_terms=default_terms,
            default_valid_until_days=default_valid_until_days,
            default_requires_approval=default_requires_approval,
            default_approval_level=default_approval_level,
            is_public=is_public,
        )
        template.items_list = default_items if default_items else None

        db.session.add(template)

        if not safe_commit("create_quote_template", {"template_id": template.id}):
            flash(_("Could not create template due to a database error. Please check server logs."), "error")
            return render_template("quotes/template_form.html")

        log_event("quote.template.created", user_id=current_user.id, template_id=template.id, template_name=name)
        track_event(current_user.id, "quote.template.created", {"template_id": template.id, "template_name": name})

        flash(_("Template created successfully"), "success")
        return redirect(url_for("quotes.list_templates"))

    return render_template("quotes/template_form.html")


@quotes_bp.route("/quotes/templates/<int:template_id>/save-from-quote", methods=["POST"])
@login_required
@admin_or_permission_required("create_quotes")
def save_template_from_quote(template_id):
    """Save current quote as a template"""
    quote_id = request.form.get("quote_id", type=int)
    if not quote_id:
        flash(_("Quote ID is required"), "error")
        return redirect(url_for("quotes.list_templates"))
    quote = Quote.query.get_or_404(quote_id)

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        flash(_("You do not have permission to create a template from this quote"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    name = request.form.get("name", "").strip()
    if not name:
        flash(_("Template name is required"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    description = request.form.get("description", "").strip() or None
    is_public = request.form.get("is_public", "false").lower() == "true"

    # Extract items
    default_items = []
    for item in quote.items:
        default_items.append(
            {
                "description": item.description,
                "quantity": float(item.quantity),
                "unit_price": float(item.unit_price),
                "unit": item.unit,
            }
        )

    # Create template
    template = QuoteTemplate(
        name=name,
        created_by=current_user.id,
        description=description,
        default_tax_rate=quote.tax_rate,
        default_currency_code=quote.currency_code,
        default_payment_terms=quote.payment_terms,
        default_terms=quote.terms,
        default_valid_until_days=30,  # Default
        default_requires_approval=quote.requires_approval,
        default_approval_level=quote.approval_level or 1,
        is_public=is_public,
    )
    template.items_list = default_items if default_items else None

    db.session.add(template)

    if not safe_commit("save_quote_template", {"template_id": template.id, "quote_id": quote_id}):
        flash(_("Could not save template due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    log_event("quote.template.saved_from_quote", user_id=current_user.id, template_id=template.id, quote_id=quote_id)
    track_event(current_user.id, "quote.template.saved_from_quote", {"template_id": template.id, "quote_id": quote_id})

    flash(_("Template saved successfully"), "success")
    return redirect(url_for("quotes.list_templates"))


@quotes_bp.route("/quotes/<int:quote_id>/export-pdf", methods=["GET"])
@login_required
def export_quote_pdf(quote_id):
    """Export quote as PDF"""
    current_app.logger.info(f"[PDF_EXPORT] Action: export_request, QuoteID: {quote_id}, User: {current_user.username}")

    quote = Quote.query.get_or_404(quote_id)
    current_app.logger.info(f"[PDF_EXPORT] Quote found: {quote.quote_number}, Status: {quote.status}")

    if not current_user.is_admin and quote.created_by != current_user.id:
        current_app.logger.warning(
            f"[PDF_EXPORT] Permission denied - QuoteID: {quote_id}, User: {current_user.username}"
        )
        flash(_("You do not have permission to export this quote"), "error")
        return redirect(request.referrer or url_for("quotes.list_quotes"))

    # Get page size from query parameter, default to A4
    page_size_raw = request.args.get("size", "A4")
    current_app.logger.info(f"[PDF_EXPORT] PageSize from query param: '{page_size_raw}', QuoteID: {quote_id}")

    # Validate page size
    valid_sizes = ["A4", "Letter", "Legal", "A3", "A5", "Tabloid"]
    if page_size_raw not in valid_sizes:
        current_app.logger.warning(
            f"[PDF_EXPORT] Invalid page size '{page_size_raw}', defaulting to A4, QuoteID: {quote_id}"
        )
        page_size = "A4"
    else:
        page_size = page_size_raw

    current_app.logger.info(
        f"[PDF_EXPORT] Final validated PageSize: '{page_size}', QuoteID: {quote_id}, QuoteNumber: {quote.quote_number}"
    )

    try:
        import io

        from flask import send_file

        from app.models import Settings
        from app.utils.pdf_generator import QuotePDFGenerator

        settings = Settings.get_settings()
        current_app.logger.info(
            f"[PDF_EXPORT] Creating QuotePDFGenerator - PageSize: '{page_size}', QuoteID: {quote_id}"
        )
        pdf_generator = QuotePDFGenerator(quote, settings=settings, page_size=page_size)
        current_app.logger.info(f"[PDF_EXPORT] Starting PDF generation - PageSize: '{page_size}', QuoteID: {quote_id}")
        pdf_bytes = pdf_generator.generate_pdf()
        pdf_size_bytes = len(pdf_bytes)
        current_app.logger.info(
            f"[PDF_EXPORT] PDF generation completed successfully - PageSize: '{page_size}', QuoteID: {quote_id}, PDFSize: {pdf_size_bytes} bytes"
        )
        filename = f"quote_{quote.quote_number}_{page_size}.pdf"
        current_app.logger.info(
            f"[PDF_EXPORT] Returning PDF file - Filename: '{filename}', PageSize: '{page_size}', QuoteID: {quote_id}"
        )
        return send_file(io.BytesIO(pdf_bytes), mimetype="application/pdf", as_attachment=True, download_name=filename)
    except ImportError:
        # Fallback if QuotePDFGenerator doesn't exist yet
        current_app.logger.warning(
            f"[PDF_EXPORT] QuotePDFGenerator import failed, using fallback - PageSize: '{page_size}', QuoteID: {quote_id}"
        )
        import io

        from flask import send_file

        from app.models import Settings
        from app.utils.pdf_generator_fallback import QuotePDFGeneratorFallback

        settings = Settings.get_settings()
        pdf_generator = QuotePDFGeneratorFallback(quote, settings=settings)
        pdf_bytes = pdf_generator.generate_pdf()
        pdf_size_bytes = len(pdf_bytes)
        current_app.logger.info(
            f"[PDF_EXPORT] Fallback PDF generated successfully - PageSize: '{page_size}', QuoteID: {quote_id}, PDFSize: {pdf_size_bytes} bytes"
        )
        filename = f"quote_{quote.quote_number}_{page_size}.pdf"
        return send_file(io.BytesIO(pdf_bytes), mimetype="application/pdf", as_attachment=True, download_name=filename)
    except Exception as e:
        current_app.logger.error(
            f"[PDF_EXPORT] Exception in PDF generation - PageSize: '{page_size}', QuoteID: {quote_id}, Error: {str(e)}",
            exc_info=True,
        )
        flash(_("Error generating PDF: %(error)s", error=str(e)), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/send-email", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def send_quote_email(quote_id):
    """Send quote via email"""
    quote = Quote.query.get_or_404(quote_id)

    # Get recipient email from request
    recipient_email = (
        request.form.get("recipient_email", "").strip() or request.json.get("recipient_email", "").strip()
        if request.is_json
        else ""
    )

    if not recipient_email:
        # Try to use quote client email
        if quote.client and quote.client.email:
            recipient_email = quote.client.email

    if not recipient_email:
        return jsonify({"error": _("Recipient email address is required")}), 400

    # Get custom message if provided
    custom_message = request.form.get("custom_message", "").strip() or (
        request.json.get("custom_message", "").strip() if request.is_json else ""
    )

    try:
        from app.utils.email import send_quote_email

        success, result, message = send_quote_email(
            quote=quote,
            recipient_email=recipient_email,
            sender_user=current_user,
            custom_message=custom_message if custom_message else None,
        )

        if success:
            flash(_("Quote sent successfully to %(email)s", email=recipient_email), "success")
            log_event(
                "quote.emailed",
                user_id=current_user.id,
                quote_id=quote.id,
                quote_title=quote.title,
                recipient_email=recipient_email,
            )
            track_event(
                current_user.id,
                "quote.emailed",
                {"quote_id": quote.id, "quote_title": quote.title, "recipient_email": recipient_email},
            )
            if request.is_json:
                return jsonify({"success": True, "message": message})
            return redirect(url_for("quotes.view_quote", quote_id=quote_id))
        else:
            flash(_("Failed to send quote: %(error)s", error=message), "error")
            if request.is_json:
                return jsonify({"error": message}), 400
            return redirect(url_for("quotes.view_quote", quote_id=quote_id))
    except Exception as e:
        current_app.logger.error(f"Error sending quote email: {e}", exc_info=True)
        flash(_("Error sending email: %(error)s", error=str(e)), "error")
        if request.is_json:
            return jsonify({"error": str(e)}), 500
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/duplicate")
@login_required
@admin_or_permission_required("create_quotes")
def duplicate_quote(quote_id):
    """Duplicate an existing quote"""
    from datetime import timedelta

    from app.utils.timezone import local_now

    original_quote = Quote.query.get_or_404(quote_id)

    # Check access permissions
    if not current_user.is_admin and original_quote.created_by != current_user.id:
        flash(_("You do not have permission to duplicate this quote"), "error")
        return redirect(url_for("quotes.list_quotes"))

    # Generate new quote number
    new_quote_number = Quote.generate_quote_number()

    # Calculate new valid_until date (30 days from now, or extend original if it exists)
    if original_quote.valid_until:
        new_valid_until = local_now().date() + timedelta(days=30)
    else:
        new_valid_until = None

    # Create new quote
    new_quote = Quote(
        quote_number=new_quote_number,
        client_id=original_quote.client_id,
        title=original_quote.title,
        description=original_quote.description,
        status="draft",  # Always start as draft
        valid_until=new_valid_until,
        notes=original_quote.notes,
        terms=original_quote.terms,
        payment_terms=original_quote.payment_terms,
        created_by=current_user.id,
        visible_to_client=original_quote.visible_to_client,
        template_id=original_quote.template_id,
        currency_code=original_quote.currency_code,
        tax_rate=original_quote.tax_rate,
        discount_type=original_quote.discount_type,
        discount_amount=original_quote.discount_amount,
        discount_reason=original_quote.discount_reason,
        coupon_code=original_quote.coupon_code,
    )

    db.session.add(new_quote)
    if not safe_commit(
        "duplicate_quote_create", {"source_quote_id": original_quote.id, "new_quote_number": new_quote_number}
    ):
        flash(_("Could not duplicate quote due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.list_quotes"))

    # Duplicate quote items
    for original_item in original_quote.items:
        new_item = QuoteItem(
            quote_id=new_quote.id,
            description=original_item.description,
            quantity=original_item.quantity,
            unit_price=original_item.unit_price,
            unit=original_item.unit,
            position=original_item.position,
            stock_item_id=original_item.stock_item_id,
            warehouse_id=original_item.warehouse_id,
            line_kind=getattr(original_item, "line_kind", None) or "item",
            display_name=getattr(original_item, "display_name", None),
            category=getattr(original_item, "category", None),
            line_date=getattr(original_item, "line_date", None),
            sku=getattr(original_item, "sku", None),
        )
        db.session.add(new_item)

    # Calculate totals
    new_quote.calculate_totals()
    if not safe_commit("duplicate_quote_finalize", {"quote_id": new_quote.id}):
        flash(_("Could not finalize duplicated quote due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.list_quotes"))

    flash(_("Quote %(quote_number)s created as duplicate", quote_number=new_quote_number), "success")
    log_event(
        "quote.duplicated",
        user_id=current_user.id,
        quote_id=new_quote.id,
        original_quote_id=original_quote.id,
        quote_title=new_quote.title,
    )
    track_event(
        current_user.id,
        "quote.duplicated",
        {"quote_id": new_quote.id, "original_quote_id": original_quote.id, "quote_title": new_quote.title},
    )
    return redirect(url_for("quotes.edit_quote", quote_id=new_quote.id))


@quotes_bp.route("/quotes/bulk_action", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def bulk_action():
    """Perform bulk actions on selected quotes"""
    action = request.form.get("action")
    quote_ids = request.form.getlist("quote_ids[]")

    if not action or not quote_ids:
        flash(_("Please select an action and at least one quote"), "error")
        return redirect(url_for("quotes.list_quotes"))

    try:
        quote_ids = [int(qid) for qid in quote_ids]
    except ValueError:
        flash(_("Invalid quote IDs"), "error")
        return redirect(url_for("quotes.list_quotes"))

    # Get quotes (with permission check)
    quotes = Quote.query.filter(Quote.id.in_(quote_ids)).all()
    if not current_user.is_admin:
        quotes = [q for q in quotes if q.created_by == current_user.id]

    if not quotes:
        flash(_("No quotes found or you do not have permission"), "error")
        return redirect(url_for("quotes.list_quotes"))

    success_count = 0
    error_count = 0

    if action == "duplicate":
        from datetime import timedelta

        from app.utils.timezone import local_now

        for quote in quotes:
            try:
                new_quote_number = Quote.generate_quote_number()
                new_valid_until = local_now().date() + timedelta(days=30) if quote.valid_until else None

                new_quote = Quote(
                    quote_number=new_quote_number,
                    client_id=quote.client_id,
                    title=quote.title,
                    description=quote.description,
                    status="draft",
                    valid_until=new_valid_until,
                    notes=quote.notes,
                    terms=quote.terms,
                    payment_terms=quote.payment_terms,
                    created_by=current_user.id,
                    visible_to_client=quote.visible_to_client,
                    template_id=quote.template_id,
                    currency_code=quote.currency_code,
                    tax_rate=quote.tax_rate,
                    discount_type=quote.discount_type,
                    discount_amount=quote.discount_amount,
                    discount_reason=quote.discount_reason,
                    coupon_code=quote.coupon_code,
                    approval_status="not_required",
                )
                db.session.add(new_quote)
                db.session.flush()

                # Duplicate items
                for item in quote.items:
                    new_item = QuoteItem(
                        quote_id=new_quote.id,
                        description=item.description,
                        quantity=item.quantity,
                        unit_price=item.unit_price,
                        unit=item.unit,
                        position=item.position,
                        stock_item_id=item.stock_item_id,
                        warehouse_id=item.warehouse_id,
                        line_kind=getattr(item, "line_kind", None) or "item",
                        display_name=getattr(item, "display_name", None),
                        category=getattr(item, "category", None),
                        line_date=getattr(item, "line_date", None),
                        sku=getattr(item, "sku", None),
                    )
                    db.session.add(new_item)

                new_quote.calculate_totals()
                success_count += 1
            except Exception as e:
                current_app.logger.error(f"Error duplicating quote {quote.id}: {e}")
                error_count += 1

        if safe_commit("bulk_duplicate_quotes", {"count": success_count}):
            flash(_("Duplicated %(count)d quote(s)", count=success_count), "success")
            if error_count > 0:
                flash(_("Failed to duplicate %(count)d quote(s)", count=error_count), "error")
        else:
            flash(_("Error duplicating quotes"), "error")

    elif action == "mark_sent":
        for quote in quotes:
            try:
                if quote.status == "draft" and quote.approval_status != "pending":
                    quote.send()
                    success_count += 1
                else:
                    error_count += 1
            except Exception as e:
                current_app.logger.error(f"Error marking quote {quote.id} as sent: {e}")
                error_count += 1

        if safe_commit("bulk_mark_sent", {"count": success_count}):
            flash(_("Marked %(count)d quote(s) as sent", count=success_count), "success")
            if error_count > 0:
                flash(_("Could not mark %(count)d quote(s) as sent", count=error_count), "error")
        else:
            flash(_("Error updating quotes"), "error")

    elif action == "delete":
        for quote in quotes:
            try:
                # Check if quote can be deleted
                if quote.status in ["draft", "rejected", "expired"]:
                    db.session.delete(quote)
                    success_count += 1
                else:
                    error_count += 1
            except Exception as e:
                current_app.logger.error(f"Error deleting quote {quote.id}: {e}")
                error_count += 1

        if safe_commit("bulk_delete_quotes", {"count": success_count}):
            flash(_("Deleted %(count)d quote(s)", count=success_count), "success")
            if error_count > 0:
                flash(_("Could not delete %(count)d quote(s) (may be in use)", count=error_count), "error")
        else:
            flash(_("Error deleting quotes"), "error")

    else:
        flash(_("Invalid action"), "error")

    return redirect(url_for("quotes.list_quotes"))


@quotes_bp.route("/quotes/<int:quote_id>/images/upload", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def upload_quote_image(quote_id):
    """Upload a decorative image to a quote"""
    import os
    from datetime import datetime
    from decimal import Decimal

    from werkzeug.utils import secure_filename

    quote = Quote.query.get_or_404(quote_id)

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        if request.is_json:
            return jsonify({"error": "Permission denied"}), 403
        flash(_("You do not have permission to upload images to this quote"), "error")
        return redirect(url_for("quotes.view_quote", quote_id=quote_id))

    # File upload configuration - only images
    ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
    UPLOAD_FOLDER = "app/static/uploads/quote_images"
    MAX_FILE_SIZE = 5 * 1024 * 1024  # 5 MB

    def allowed_file(filename):
        return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS

    if "file" not in request.files:
        if request.is_json:
            return jsonify({"error": "No file provided"}), 400
        flash(_("No file provided"), "error")
        return redirect(url_for("quotes.edit_quote", quote_id=quote_id))

    file = request.files["file"]
    if file.filename == "":
        if request.is_json:
            return jsonify({"error": "No file selected"}), 400
        flash(_("No file selected"), "error")
        return redirect(url_for("quotes.edit_quote", quote_id=quote_id))

    if not allowed_file(file.filename):
        if request.is_json:
            return jsonify({"error": "File type not allowed. Only images (PNG, JPG, JPEG, GIF, WEBP) are allowed"}), 400
        flash(_("File type not allowed. Only images are allowed"), "error")
        return redirect(url_for("quotes.edit_quote", quote_id=quote_id))

    # Check file size
    file.seek(0, os.SEEK_END)
    file_size = file.tell()
    file.seek(0)

    if file_size > MAX_FILE_SIZE:
        if request.is_json:
            return (
                jsonify({"error": f"File size exceeds maximum allowed size ({MAX_FILE_SIZE / (1024*1024):.0f} MB)"}),
                400,
            )
        flash(_("File size exceeds maximum allowed size (5 MB)"), "error")
        return redirect(url_for("quotes.edit_quote", quote_id=quote_id))

    # Save file
    original_filename = secure_filename(file.filename)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"{quote_id}_{timestamp}_{original_filename}"

    # Ensure upload directory exists
    upload_dir = os.path.join(current_app.root_path, "..", UPLOAD_FOLDER)
    os.makedirs(upload_dir, exist_ok=True)

    file_path = os.path.join(upload_dir, filename)
    file.save(file_path)

    # Get file info
    mime_type = file.content_type or "image/png"

    # Get position from form (default to 0,0)
    position_x = Decimal(str(request.form.get("position_x", 0)))
    position_y = Decimal(str(request.form.get("position_y", 0)))
    width = Decimal(str(request.form.get("width", 0))) if request.form.get("width") else None
    height = Decimal(str(request.form.get("height", 0))) if request.form.get("height") else None
    opacity = Decimal(str(request.form.get("opacity", 1.0)))
    z_index = int(request.form.get("z_index", 0))

    # Create image record
    image = QuoteImage(
        quote_id=quote_id,
        filename=filename,
        original_filename=original_filename,
        file_path=os.path.join(UPLOAD_FOLDER, filename),
        file_size=file_size,
        uploaded_by=current_user.id,
        mime_type=mime_type,
        position_x=position_x,
        position_y=position_y,
        width=width,
        height=height,
        opacity=opacity,
        z_index=z_index,
    )

    db.session.add(image)

    if not safe_commit("upload_quote_image", {"quote_id": quote_id, "image_id": image.id}):
        if request.is_json:
            return jsonify({"error": "Database error"}), 500
        flash(_("Could not upload image due to a database error. Please check server logs."), "error")
        # Clean up uploaded file
        try:
            os.remove(file_path)
        except OSError:
            pass
        return redirect(url_for("quotes.edit_quote", quote_id=quote_id))

    log_event(
        "quote.image.uploaded",
        user_id=current_user.id,
        quote_id=quote_id,
        image_id=image.id,
        filename=original_filename,
    )
    track_event(
        current_user.id,
        "quote.image.uploaded",
        {"quote_id": quote_id, "image_id": image.id, "filename": original_filename},
    )

    if request.is_json:
        return jsonify({"success": True, "image": image.to_dict()})

    flash(_("Image uploaded successfully"), "success")
    return redirect(url_for("quotes.edit_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/images/<int:image_id>/position", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def update_quote_image_position(quote_id, image_id):
    """Update the position and properties of a decorative image"""
    from decimal import Decimal

    quote = Quote.query.get_or_404(quote_id)
    image = QuoteImage.query.filter_by(id=image_id, quote_id=quote_id).first_or_404()

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        return jsonify({"error": "Permission denied"}), 403

    # Get position data from request
    data = request.get_json() if request.is_json else request.form

    if "position_x" in data:
        image.position_x = Decimal(str(data["position_x"]))
    if "position_y" in data:
        image.position_y = Decimal(str(data["position_y"]))
    if "width" in data:
        image.width = Decimal(str(data["width"])) if data["width"] else None
    if "height" in data:
        image.height = Decimal(str(data["height"])) if data["height"] else None
    if "opacity" in data:
        image.opacity = Decimal(str(data["opacity"]))
    if "z_index" in data:
        image.z_index = int(data["z_index"])

    if not safe_commit("update_quote_image_position", {"quote_id": quote_id, "image_id": image_id}):
        return jsonify({"error": "Database error"}), 500

    return jsonify({"success": True, "image": image.to_dict()})


@quotes_bp.route("/quotes/<int:quote_id>/images/<int:image_id>/delete", methods=["POST"])
@login_required
@admin_or_permission_required("edit_quotes")
def delete_quote_image(quote_id, image_id):
    """Delete a decorative image from a quote"""
    import os

    quote = Quote.query.get_or_404(quote_id)
    image = QuoteImage.query.filter_by(id=image_id, quote_id=quote_id).first_or_404()

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        if request.is_json:
            return jsonify({"error": "Permission denied"}), 403
        flash(_("You do not have permission to delete images from this quote"), "error")
        return redirect(url_for("quotes.edit_quote", quote_id=quote_id))

    # Delete file from disk
    file_path = os.path.join(current_app.root_path, "..", image.file_path)
    if os.path.exists(file_path):
        try:
            os.remove(file_path)
        except OSError as e:
            current_app.logger.warning(f"Failed to delete image file {file_path}: {e}")

    image_id_for_log = image.id
    db.session.delete(image)

    if not safe_commit("delete_quote_image", {"quote_id": quote_id, "image_id": image_id_for_log}):
        if request.is_json:
            return jsonify({"error": "Database error"}), 500
        flash(_("Could not delete image due to a database error. Please check server logs."), "error")
        return redirect(url_for("quotes.edit_quote", quote_id=quote_id))

    log_event(
        "quote.image.deleted",
        user_id=current_user.id,
        quote_id=quote_id,
        image_id=image_id_for_log,
    )
    track_event(
        current_user.id,
        "quote.image.deleted",
        {"quote_id": quote_id, "image_id": image_id_for_log},
    )

    if request.is_json:
        return jsonify({"success": True})

    flash(_("Image deleted successfully"), "success")
    return redirect(url_for("quotes.edit_quote", quote_id=quote_id))


@quotes_bp.route("/quotes/<int:quote_id>/images/<int:image_id>/base64", methods=["GET"])
@login_required
def get_quote_image_base64(quote_id, image_id):
    """Get base64-encoded image for PDF embedding or serve image directly"""
    import base64
    import mimetypes
    import os

    from flask import send_file

    quote = Quote.query.get_or_404(quote_id)
    image = QuoteImage.query.filter_by(id=image_id, quote_id=quote_id).first_or_404()

    # Check permissions
    if not current_user.is_admin and quote.created_by != current_user.id:
        return jsonify({"error": "Permission denied"}), 403

    file_path = os.path.join(current_app.root_path, "..", image.file_path)
    if not os.path.exists(file_path):
        return jsonify({"error": "File not found"}), 404

    # If request wants JSON (for API), return base64 data URI
    if request.args.get("format") == "json" or request.headers.get("Accept") == "application/json":
        try:
            with open(file_path, "rb") as img_file:
                image_data = base64.b64encode(img_file.read()).decode("utf-8")

            # Detect MIME type
            mime_type, _ = mimetypes.guess_type(file_path)
            if not mime_type:
                mime_type = image.mime_type or "image/png"

            return jsonify(
                {
                    "success": True,
                    "data_uri": f"data:{mime_type};base64,{image_data}",
                    "mime_type": mime_type,
                }
            )
        except Exception as e:
            current_app.logger.error(f"Error reading image file: {e}")
            return jsonify({"error": "Error reading image file"}), 500

    # Otherwise, serve the image directly (for img src tags)
    try:
        mime_type, _ = mimetypes.guess_type(file_path)
        if not mime_type:
            mime_type = image.mime_type or "image/png"

        return send_file(file_path, mimetype=mime_type)
    except Exception as e:
        current_app.logger.error(f"Error serving image file: {e}")
        return jsonify({"error": "Error serving image file"}), 500
