import json
import os
import random
import re
import string
from datetime import datetime, timedelta

from app.services.shipping_mapping import get_carrier, get_internal_carrier, rules_from_json

from flask import (
    Blueprint, Response, current_app, flash, jsonify, redirect,
    render_template, request, stream_with_context, url_for,
)
from flask_login import login_required

from app import db as database
from app.services import eracuni
from app.services import shopify as shopify_svc
from app.services import sync as sync_svc
from app.services import woocommerce as wc_svc
from app.services import printer as printer_svc
from app.services.shipping import dhl, dpd, paket24
from app.labels import order_summary as summary_label
from app.labels import order_item as item_label

orders_bp = Blueprint('orders', __name__)

LETTERS = ['A', 'B', 'C', 'D']

EU_COUNTRIES = {
    'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI',
    'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT',
    'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK',
}


# ── Helpers ────────────────────────────────────────────────────────────────────

def _detect_carrier(delivery_method: str, country: str = '') -> str:
    """Return internal carrier key by matching delivery method against saved rules."""
    db_path = current_app.config['DATABASE_PATH']
    rules_json = database.get_setting('SHIPPING_RULES_JSON', db_path)
    rules = rules_from_json(rules_json)
    return get_internal_carrier(delivery_method, country, rules)


def _parse_shipping_address(order: dict) -> dict:
    """Extract shipping address from order dict for use with carrier APIs."""
    # Use delivery address fields if present, else fall back to buyer fields
    name    = order.get('deliveryName')    or order.get('buyerName', '')
    street  = order.get('deliveryStreet')  or order.get('buyerStreet', '')
    postal  = order.get('deliveryPostalCode') or order.get('buyerPostalCode', '')
    city    = order.get('deliveryCity')    or order.get('buyerCity', '')
    country = order.get('deliveryCountry') or order.get('buyerCountry', 'HR')
    return {
        'name':        name,
        'street':      street,
        'postal_code': postal,
        'city':        city,
        'country':     country,
        'email':       order.get('buyerEMail', ''),
        'phone':       order.get('buyerPhone', ''),
    }


def _sse_event(order_number: str, step: int, status: str, message: str) -> str:
    """Format a single SSE data line."""
    payload = json.dumps({
        'order': order_number,
        'step': step,
        'status': status,
        'message': message,
    })
    return f"data: {payload}\n\n"


def _parse_items(order: dict) -> list:
    """Return items list from order, deserializing if needed."""
    items = order.get('items')
    if isinstance(items, str):
        try:
            items = json.loads(items)
        except (json.JSONDecodeError, TypeError):
            items = []
    return items or []


def _resolve_item(item: dict, db_path: str) -> dict:
    """Normalise a raw e-računi order item into consistent keys.

    e-računi uses 'description' for the product name and doesn't include
    warehouse location — that's looked up from our local products table.
    """
    sku = item.get('productCode', item.get('sku', ''))
    name = item.get('description') or item.get('productName') or item.get('name') or sku
    qty = int(float(item.get('quantity', 1)))
    # Look up warehouse location from local products DB
    location = ''
    if sku:
        product = database.get_product(sku, db_path)
        if product:
            location = product.get('defaultWarehouseLocation', '') or ''
    return {'sku': sku, 'name': name, 'qty': qty, 'location': location}


_S_ORDER_RE = re.compile(r'^S\d+$')

def _barcode_id(order_number: str) -> str:
    """Return a short 6-char barcode identifier for an order number.

    S-followed-by-digits orders (e.g. S10042) are used as-is.
    All other formats (e.g. 2026-00044) get a random 6-char uppercase hash
    so the barcode fits on the label.
    """
    if _S_ORDER_RE.match(order_number):
        return order_number
    return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))


def _save_pdf(pdf_bytes: bytes, filename: str) -> str:
    """Save PDF bytes to ./pdfs/ directory. Returns the file path."""
    pdfs_dir = os.path.join(os.getcwd(), 'pdfs')
    os.makedirs(pdfs_dir, exist_ok=True)
    path = os.path.join(pdfs_dir, filename)
    with open(path, 'wb') as f:
        f.write(pdf_bytes)
    return path


def _process_single_order(order: dict, letter: str = None):
    """Generator that yields SSE event strings while processing one order."""
    number = order['number']
    db_path = current_app.config['DATABASE_PATH']
    items = _parse_items(order)
    recipient = _parse_shipping_address(order)
    carrier = _detect_carrier(
        order.get('deliveryMethod', ''),
        order.get('deliveryCountry') or order.get('buyerCountry', ''),
    )

    # ── Step 1: Update e-računi + create invoice ───────────────────────────────
    yield _sse_event(number, 1, 'running', 'Updating e-računi order and creating invoice…')
    invoice_id = None
    invoice_number = None
    try:
        tax_number = order.get('buyerTaxNumber', '')
        update_kwargs = {}
        if tax_number:
            update_kwargs['buyerTaxNumber'] = tax_number
            update_kwargs['vatTransactionType'] = 'B2B' if tax_number else 'B2C'
        if update_kwargs:
            eracuni.update_order(number, **update_kwargs)

        invoice_id = eracuni.create_invoice(number)
        if not invoice_id:
            raise ValueError("create_invoice returned empty ID")

        invoice_data = eracuni.get_invoice(invoice_id)
        invoice_number = invoice_data.get('number', invoice_id)
        yield _sse_event(number, 1, 'ok', f'Invoice {invoice_number} created')
    except Exception as exc:
        yield _sse_event(number, 1, 'error', f'Invoice creation failed: {exc}')
        return

    # ── Step 2: Download PDFs ──────────────────────────────────────────────────
    yield _sse_event(number, 2, 'running', 'Downloading invoice and delivery note PDFs…')
    invoice_pdf_bytes = None
    delivery_pdf_bytes = None
    try:
        invoice_pdf_bytes = eracuni.get_invoice_pdf(invoice_id)
        _save_pdf(invoice_pdf_bytes, f"{number}_invoice.pdf")

        delivery_notes = eracuni.get_delivery_note_list(number)
        if delivery_notes:
            dn_id = delivery_notes[0].get('documentID') or delivery_notes[0].get('id')
            delivery_pdf_bytes = eracuni.get_delivery_note_pdf(dn_id)
            _save_pdf(delivery_pdf_bytes, f"{number}_delivery_note.pdf")

        yield _sse_event(number, 2, 'ok', 'PDFs downloaded')
    except Exception as exc:
        yield _sse_event(number, 2, 'error', f'PDF download failed: {exc}')
        return

    # ── Step 3: Print PDFs ─────────────────────────────────────────────────────
    yield _sse_event(number, 3, 'running', 'Sending PDFs to printer…')
    try:
        if invoice_pdf_bytes:
            printer_svc.print_pdf(invoice_pdf_bytes, printer='large')
        if delivery_pdf_bytes:
            printer_svc.print_pdf(delivery_pdf_bytes, printer='large')
        yield _sse_event(number, 3, 'ok', 'PDFs sent to printer')
    except Exception as exc:
        yield _sse_event(number, 3, 'error', f'PDF print failed: {exc}')
        # Non-fatal — continue

    # ── Step 4: Create shipping label ──────────────────────────────────────────
    yield _sse_event(number, 4, 'running', f'Creating {carrier.upper()} shipment…')
    tracking_number = None
    zpl_label = None
    try:
        is_cod = (order.get('methodOfPayment', '').lower() in ('cod', 'pouzece', 'pouzećem'))
        cod_amount = order.get('totalAmountInDomCurr') if is_cod else None
        country = recipient.get('country', 'HR')
        is_eu = country in EU_COUNTRIES

        if carrier == 'paket24':
            tracking_number = paket24.create_shipment(
                number, recipient, cod_amount=cod_amount
            )
            zpl_label = paket24.get_label(tracking_number)

        elif carrier == 'dpd':
            tracking_number = dpd.create_shipment(
                number, recipient, cod_amount=cod_amount
            )
            zpl_label = dpd.get_label(tracking_number)

        elif carrier in ('dhl_eu', 'dhl_int'):
            tracking_number, zpl_bytes = dhl.create_shipment(
                number, recipient, items,
                invoice_number=invoice_number,
                currency=order.get('documentCurrency', 'EUR'),
                is_eu=(carrier == 'dhl_eu' and is_eu),
            )
            zpl_label = zpl_bytes.decode('utf-8') if isinstance(zpl_bytes, bytes) else zpl_bytes

        elif carrier == 'pickup':
            tracking_number = 'OSOBNO'
            yield _sse_event(number, 4, 'ok', 'Osobno preuzimanje — no shipment created')

        if carrier != 'pickup' and not tracking_number:
            raise ValueError("No tracking number returned from carrier API")

        if carrier != 'pickup':
            yield _sse_event(number, 4, 'ok', f'Shipment created: {tracking_number}')

    except Exception as exc:
        yield _sse_event(number, 4, 'error', f'Shipment creation failed: {exc}')
        return

    # ── Step 5: Print shipping label ───────────────────────────────────────────
    yield _sse_event(number, 5, 'running', 'Printing shipping label…')
    try:
        if zpl_label and carrier != 'pickup':
            printer_svc.print_zpl(zpl_label, printer='large')
        yield _sse_event(number, 5, 'ok', 'Shipping label printed')
    except Exception as exc:
        yield _sse_event(number, 5, 'error', f'Label print failed: {exc}')
        # Non-fatal — continue

    # ── Step 6: Fulfill on Shopify / WooCommerce ───────────────────────────────
    yield _sse_event(number, 6, 'running', 'Fulfilling order on Shopify / WooCommerce…')
    try:
        shopify_id = order.get('shopify_id')
        if not shopify_id:
            shopify_id = shopify_svc.get_order_id(number, order.get('buyerName'))
            if shopify_id:
                database.update_order_shopify_id(number, shopify_id, db_path)

        if shopify_id and carrier != 'pickup':
            fulfillment_orders = shopify_svc.get_fulfillment_orders(shopify_id)
            if fulfillment_orders:
                fo_id = str(fulfillment_orders[0]['id'])
                shopify_svc.fulfill_order(
                    shopify_id, fo_id,
                    tracking_number=tracking_number,
                    carrier=carrier.upper(),
                )

        # Also try WooCommerce (order number may be in WC as well)
        try:
            wc_order_id = number.lstrip('S')
            if wc_order_id.isdigit() and carrier != 'pickup':
                wc_svc.mark_completed(wc_order_id)
                if tracking_number and tracking_number != 'OSOBNO':
                    wc_svc.add_tracking_note(wc_order_id, tracking_number, carrier.upper())
        except Exception:
            pass  # WC update is best-effort

        database.update_order_status(number, 'CompletedDelivery', db_path)
        yield _sse_event(number, 6, 'ok', 'Order fulfilled')

    except Exception as exc:
        yield _sse_event(number, 6, 'error', f'Fulfillment failed: {exc}')
        return


# ── Routes ─────────────────────────────────────────────────────────────────────

@orders_bp.route('/')
@login_required
def index_redirect():
    from app.db import get_all_users, get_all_settings
    from app.routes.settings import ALL_SETTINGS_FIELDS
    db_path = current_app.config['DATABASE_PATH']
    cfg = current_app.config

    all_keys = ALL_SETTINGS_FIELDS
    settings_data = {k: cfg.get(k, '') or '' for k in all_keys}

    users = get_all_users(db_path)
    return render_template('index.html', settings=settings_data, users=users)


@orders_bp.route('/orders')
def orders_list():
    db_path = current_app.config['DATABASE_PATH']
    orders = database.get_all_orders(db_path)
    # Parse items for display
    for o in orders:
        if isinstance(o.get('items'), str):
            try:
                o['items'] = json.loads(o['items'])
            except Exception:
                o['items'] = []
    processing_orders = database.get_processing_orders(db_path)
    return render_template(
        'orders/index.html',
        orders=orders,
        processing_orders=processing_orders,
    )


@orders_bp.route('/orders/refresh', methods=['POST'])
def orders_refresh():
    """Fetch fresh orders from e-računi, upsert to DB, return HTMX partial."""
    db_path = current_app.config['DATABASE_PATH']
    try:
        date_from = (datetime.utcnow() - timedelta(days=90)).strftime('%Y-%m-%d')
        raw_orders = eracuni.get_orders(date_from)
        database.upsert_orders(raw_orders, db_path)
    except Exception as exc:
        # Return an error row so HTMX partial still renders
        return Response(
            f'<tr><td colspan="7" class="text-danger">Refresh failed: {exc}</td></tr>',
            status=200,
        )

    orders = database.get_all_orders(db_path)
    for o in orders:
        if isinstance(o.get('items'), str):
            try:
                o['items'] = json.loads(o['items'])
            except Exception:
                o['items'] = []
    return render_template('partials/order_table.html', orders=orders)


@orders_bp.route('/orders/queue')
def orders_queue():
    """HTMX partial: current processing queue."""
    db_path = current_app.config['DATABASE_PATH']
    processing_orders = database.get_processing_orders(db_path)
    return render_template('partials/queue.html', processing_orders=processing_orders)


@orders_bp.route('/orders/<number>')
def order_detail(number):
    db_path = current_app.config['DATABASE_PATH']
    order = database.get_order(number, db_path)
    if not order:
        flash(f'Order {number} not found.', 'warning')
        return redirect(url_for('orders.orders_list'))

    if isinstance(order.get('items'), str):
        try:
            order['items'] = json.loads(order['items'])
        except Exception:
            order['items'] = []
    if isinstance(order.get('address'), str):
        try:
            order['address'] = json.loads(order['address'])
        except Exception:
            order['address'] = {}

    return render_template('orders/process.html', order=order)


@orders_bp.route('/orders/<number>/skip', methods=['POST'])
def order_skip(number):
    db_path = current_app.config['DATABASE_PATH']
    database.skip_order(number, db_path)
    flash(f'Order {number} skipped.', 'info')
    return redirect(url_for('orders.orders_list'))


@orders_bp.route('/orders/<number>/unskip', methods=['POST'])
def order_unskip(number):
    db_path = current_app.config['DATABASE_PATH']
    database.unskip_order(number, db_path)
    flash(f'Order {number} removed from skip list.', 'info')
    return redirect(url_for('orders.orders_list'))


@orders_bp.route('/orders/<number>/notify', methods=['POST'])
@login_required
def order_notify(number):
    """Send customer notification email via make.com for an order."""
    from app.services.notification import notify_customer
    db_path  = current_app.config['DATABASE_PATH']
    webhook  = current_app.config.get('MAKE_ORDER_NOTIFY_URL', '')
    order    = database.get_order(number, db_path)
    if not order:
        return jsonify(ok=False, error=f'Order {number} not found'), 404

    body            = request.get_json(silent=True) or {}
    tracking_number = body.get('tracking_number')

    result = notify_customer(order, webhook, tracking_number=tracking_number)
    status = 200 if result['ok'] else 502
    return jsonify(**result), status


@orders_bp.route('/orders/make-data-structure', methods=['POST'])
@login_required
def make_data_structure():
    """Send a sample multipart request to the Make.com webhook so it can auto-detect the data structure."""
    import pathlib

    webhook = current_app.config.get('MAKE_ORDER_NOTIFY_URL', '')
    if not webhook:
        return jsonify(ok=False, error='MAKE_ORDER_NOTIFY_URL is not configured'), 400

    sample_pdf_path = pathlib.Path(current_app.root_path) / 'static' / 'sample.pdf'
    invoice_bytes = sample_pdf_path.read_bytes() if sample_pdf_path.exists() else b'%PDF-1.4 sample invoice'

    data = {
        'Name':          'Test Customer',
        'Email':         'test@example.com',
        'Subject':       'Your Soldered Electronics order has been completed!',
        'html_data':     '<h1>Sample email body</h1><p>This is a structure detection request.</p>',
        'Content':       '<h1>Sample email body</h1><p>This is a structure detection request.</p>',
        'title':         'Your Soldered Electronics order has been completed!',
        'email':         'test@example.com',
    }

    try:
        import requests as req
        resp = req.post(
            webhook,
            data=data,
            files={
                'invoice': ('invoice.pdf', invoice_bytes, 'application/pdf'),
            },
            timeout=30,
        )
    except Exception as e:
        return jsonify(ok=False, error=str(e)), 502

    if resp.status_code == 200:
        return jsonify(ok=True)
    return jsonify(ok=False, error=f'make.com returned {resp.status_code}: {resp.text[:200]}'), 502


@orders_bp.route('/orders/notify-test', methods=['POST'])
@login_required
def notify_test():
    """Send test emails in all 4 templates (EN, HR, DE, Pickup) to the given address."""
    import pathlib
    from app.services.notification import notify_customer

    webhook = current_app.config.get('MAKE_ORDER_NOTIFY_URL', '')
    body    = request.get_json(silent=True) or {}
    email   = (body.get('email') or '').strip()

    if not email:
        return jsonify(ok=False, error='Email address is required'), 400
    if not webhook:
        return jsonify(ok=False, error='MAKE_ORDER_NOTIFY_URL is not configured'), 400

    sample_pdf_path = pathlib.Path(current_app.root_path) / 'static' / 'sample.pdf'
    invoice_pdf = sample_pdf_path.read_bytes() if sample_pdf_path.exists() else None

    # One dummy order per template variant
    variants = [
        {'label': 'EN', 'buyerCountry': 'US', 'carrier': 'DHL',    'number': 'TEST-EN'},
        {'label': 'HR', 'buyerCountry': 'HR', 'carrier': 'DPD',    'number': 'TEST-HR'},
        {'label': 'DE', 'buyerCountry': 'DE', 'carrier': 'DPD',    'number': 'TEST-DE'},
        {'label': 'Pickup', 'buyerCountry': 'HR', 'carrier': 'Pickup', 'number': 'TEST-PICKUP'},
    ]

    errors = []
    for v in variants:
        order = {
            'number':          v['number'],
            'buyerName':       'Soldered Electronics',
            'buyerName1':      'Test Customer',
            'buyerEMail':      email,
            'buyerStreet':     'Ulica Testna 1',
            'buyerPostalCode': '10000',
            'buyerCity':       'Zagreb',
            'buyerCountry':    v['buyerCountry'],
            'carrier':         v['carrier'],
            'deliveryMethod':  v['carrier'],
            'deliveryStreet':  '',
            'deliveryCity':    '',
        }
        result = notify_customer(order, webhook,
                                 tracking_number='TEST1234567890',
                                 invoice_pdf=invoice_pdf)
        if result['ok']:
            errors.append(f"{v['label']}: ok (make.com: {result.get('response', '')})")
        else:
            errors.append(f"{v['label']}: FAIL {result.get('error', 'unknown error')}")

    all_ok = all('FAIL' not in e for e in errors)
    if not all_ok:
        return jsonify(ok=False, error='; '.join(errors)), 502
    return jsonify(ok=True, sent=len(variants), details='; '.join(errors))


@orders_bp.route('/orders/<number>/print-labels', methods=['POST'])
def order_print_labels(number):
    """Print order summary + item labels for a single order. Returns JSON."""
    db_path = current_app.config['DATABASE_PATH']
    order = database.get_order(number, db_path)
    if not order:
        return jsonify(ok=False, error=f'Order {number} not found.')

    items = _parse_items(order)
    try:
        currency = order.get('documentCurrency') or 'EUR'
        img_summary = summary_label.generate(
            order_number=number,
            buyer_name=order.get('buyerName', ''),
            num_items=len(items),
            delivery_method=order.get('deliveryMethod', ''),
            total=float(order.get('totalAmountInDomCurr') or 0),
            currency=currency,
        )
        printer_svc.print_label(img_summary, printer='small',
                                meta={'order': number, 'label_type': 'Order Summary'},
                                also_preview=True)

        for item in items:
            r = _resolve_item(item, db_path)
            raw_price = item.get('price') or item.get('unitPrice')
            img_item = item_label.generate(
                sku=r['sku'],
                product_name=r['name'],
                quantity=r['qty'],
                order_number=number,
                warehouse_location=r['location'],
                item_price=float(raw_price) if raw_price is not None else None,
                currency=currency,
            )
            printer_svc.print_label(img_item, printer='small',
                                    meta={'order': number, 'label_type': f'Item: {r["sku"]}'},
                                    also_preview=True)

        printed = 1 + len(items)
        return jsonify(ok=True, message=f'Printed {printed} label(s) for {number}.')
    except Exception as exc:
        return jsonify(ok=False, error=str(exc))


@orders_bp.route('/orders/invoice-stream')
@login_required
def invoice_stream():
    """SSE: steps 1-3 only (create invoice, get PDF, print PDF) for a batch."""
    db_path = current_app.config['DATABASE_PATH']
    selected = [n.strip() for n in request.args.get('orders', '').split(',') if n.strip()][:4]

    if not selected:
        def _empty():
            yield _sse_event('', 0, 'error', 'No orders specified')
        return Response(stream_with_context(_empty()), mimetype='text/event-stream')

    app = current_app._get_current_object()

    def generate():
        with app.app_context():
            for number in selected:
                order = database.get_order(number, db_path)
                if not order:
                    yield _sse_event(number, 0, 'error', f'Order {number} not found')
                    continue

                # Step 1: Create invoice
                yield _sse_event(number, 1, 'running', 'Creating invoice…')
                invoice_id = None
                invoice_pdf_bytes = None
                try:
                    tax_number = order.get('buyerTaxNumber', '')
                    if tax_number:
                        eracuni.update_order(number, buyerTaxNumber=tax_number,
                                             vatTransactionType='B2B')
                    invoice_id = eracuni.create_invoice(number)
                    if not invoice_id:
                        raise ValueError('empty invoice ID')
                    invoice_data = eracuni.get_invoice(invoice_id)
                    inv_num = invoice_data.get('number', invoice_id)
                    yield _sse_event(number, 1, 'ok', f'Invoice {inv_num} created')
                except Exception as exc:
                    yield _sse_event(number, 1, 'error', str(exc))
                    continue

                # Step 2: Download PDF
                yield _sse_event(number, 2, 'running', 'Downloading invoice PDF…')
                try:
                    invoice_pdf_bytes = eracuni.get_invoice_pdf(invoice_id)
                    _save_pdf(invoice_pdf_bytes, f'{number}_invoice.pdf')
                    yield _sse_event(number, 2, 'ok', 'PDF downloaded')
                except Exception as exc:
                    yield _sse_event(number, 2, 'error', str(exc))
                    continue

                # Step 3: Print PDF
                yield _sse_event(number, 3, 'running', 'Sending PDF to printer…')
                try:
                    printer_svc.print_pdf(invoice_pdf_bytes, printer='large')
                    yield _sse_event(number, 3, 'ok', 'PDF sent to printer')
                except Exception as exc:
                    yield _sse_event(number, 3, 'error', str(exc))

            yield f"data: {json.dumps({'done': True})}\n\n"

    return Response(stream_with_context(generate()), mimetype='text/event-stream',
                    headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})


@orders_bp.route('/orders/batch-items')
@login_required
def batch_items():
    """Return enriched item data (name, SKU, location, qty) for the packing screen."""
    db_path = current_app.config['DATABASE_PATH']
    selected = [n.strip() for n in request.args.get('orders', '').split(',') if n.strip()][:4]
    result = []
    for number in selected:
        order = database.get_order(number, db_path)
        if not order:
            continue
        raw_items = _parse_items(order)
        enriched = [_resolve_item(it, db_path) for it in raw_items]
        for it, r in zip(raw_items, enriched):
            r['price'] = it.get('price') or it.get('unitPrice')
        result.append({
            'number': number,
            'buyer_name': order.get('buyerName1') or order.get('buyerName', ''),
            'delivery': order.get('deliveryMethod') or '',
            'items': enriched,
        })
    return jsonify(orders=result)


@orders_bp.route('/orders/print-batch-labels', methods=['POST'])
@login_required
def print_batch_labels():
    """Print order summary + item labels for each order in the batch, in sequence."""
    db_path = current_app.config['DATABASE_PATH']
    body = request.get_json(silent=True) or {}
    order_numbers = (body.get('orders') or [])[:4]

    total_printed = 0
    errors = []
    hash_map = {}  # order_number → barcode_id

    for i, number in enumerate(order_numbers):
        letter = LETTERS[i] if i < len(LETTERS) else str(i + 1)
        order = database.get_order(number, db_path)
        if not order:
            errors.append(f'{number} not found')
            continue
        items = _parse_items(order)
        bid = _barcode_id(number)
        hash_map[number] = bid
        try:
            currency = order.get('documentCurrency') or 'EUR'
            img = summary_label.generate(
                order_number=number,
                buyer_name=order.get('buyerName', ''),
                num_items=len(items),
                delivery_method=order.get('deliveryMethod', ''),
                total=float(order.get('totalAmountInDomCurr') or 0),
                currency=currency,
                letter=letter,
                barcode_id=bid,
            )
            printer_svc.print_label(img, printer='small',
                                    meta={'order': number, 'label_type': 'Order Summary'},
                                    also_preview=True)
            total_printed += 1
            for item in items:
                r = _resolve_item(item, db_path)
                raw_price = item.get('price') or item.get('unitPrice')
                img_item = item_label.generate(
                    sku=r['sku'],
                    product_name=r['name'],
                    quantity=r['qty'],
                    order_number=number,
                    warehouse_location=r['location'],
                    item_price=float(raw_price) if raw_price is not None else None,
                    currency=currency,
                    letter=letter,
                    barcode_id=bid,
                )
                printer_svc.print_label(img_item, printer='small',
                                        meta={'order': number, 'label_type': f'Item: {r["sku"]}'},
                                        also_preview=True)
                total_printed += 1
        except Exception as exc:
            errors.append(f'{number}: {exc}')

    return jsonify(ok=True, printed=total_printed, errors=errors, hash_map=hash_map)


@orders_bp.route('/orders/print-test-labels', methods=['POST'])
@login_required
def print_test_labels():
    """Print fake labels for the packing screen test mode."""
    fake_orders = [
        {'number': 'S10001', 'buyer': 'Marko Horvat',    'delivery': 'DHL',     'total': 29.99,
         'items': [
             {'sku': '333100', 'name': 'Arduino Uno R3',       'qty': 2, 'location': 'A1-01'},
             {'sku': '333101', 'name': 'LED Strip 1m RGB',      'qty': 1, 'location': 'B2-03'},
             {'sku': '333102', 'name': 'Resistor Kit 600pc',    'qty': 3, 'location': 'A3-07'},
         ]},
        {'number': 'S10002', 'buyer': 'Jana Kovač',     'delivery': 'DPD',     'total': 54.50,
         'items': [
             {'sku': '333200', 'name': 'Raspberry Pi 4 4GB',        'qty': 1, 'location': 'C1-02'},
             {'sku': '333201', 'name': 'Power Supply 5V 3A',        'qty': 1, 'location': 'D2-01'},
             {'sku': '333202', 'name': 'MicroSD Card 32GB',         'qty': 2, 'location': 'A1-03'},
             {'sku': '333203', 'name': 'HDMI Cable 1m',             'qty': 1, 'location': 'B3-05'},
             {'sku': '333204', 'name': 'GPIO Breakout Board',       'qty': 1, 'location': 'C2-04'},
             {'sku': '333205', 'name': 'Breadboard 830pt',          'qty': 2, 'location': 'A2-02'},
             {'sku': '333206', 'name': 'Jumper Wires M-F 40pc',     'qty': 1, 'location': 'B1-06'},
             {'sku': '333207', 'name': 'Resistor Kit 600pc',        'qty': 1, 'location': 'A3-07'},
             {'sku': '333208', 'name': 'Capacitor Kit 300pc',       'qty': 1, 'location': 'A3-08'},
             {'sku': '333209', 'name': 'LED Assortment 100pc',      'qty': 1, 'location': 'B2-09'},
             {'sku': '333210', 'name': 'Arduino Nano',              'qty': 3, 'location': 'A1-01'},
             {'sku': '333211', 'name': 'USB-C Cable 2m',            'qty': 2, 'location': 'D1-03'},
             {'sku': '333212', 'name': 'Heat Shrink Tubes Kit',     'qty': 1, 'location': 'C3-01'},
             {'sku': '333213', 'name': 'Multimeter DT830',          'qty': 1, 'location': 'D3-02'},
             {'sku': '333214', 'name': 'Soldering Iron 60W',        'qty': 1, 'location': 'D2-05'},
             {'sku': '333215', 'name': 'Solder Wire 100g',          'qty': 2, 'location': 'D2-06'},
             {'sku': '333216', 'name': 'Relay Module 5V',           'qty': 4, 'location': 'B1-07'},
             {'sku': '333217', 'name': 'HC-SR04 Ultrasonic Sensor', 'qty': 2, 'location': 'C1-08'},
             {'sku': '333218', 'name': 'L298N Motor Driver',        'qty': 1, 'location': 'C2-09'},
             {'sku': '333219', 'name': 'DS18B20 Temp Sensor',       'qty': 3, 'location': 'A2-10'},
         ]},
        {'number': 'S10003', 'buyer': 'Klaus Müller',   'delivery': 'GLS',     'total': 18.00,
         'items': [
             {'sku': '333300', 'name': 'ESP32 Dev Board',       'qty': 4, 'location': 'A2-05'},
             {'sku': '333301', 'name': 'OLED Display 0.96"',    'qty': 2, 'location': 'B1-04'},
             {'sku': '333302', 'name': 'DHT22 Temp Sensor',     'qty': 1, 'location': 'A1-08'},
         ]},
        {'number': '2026-00044', 'buyer': 'OŠ Nikola Tesla Zagreb', 'delivery': 'Osobno preuzimanje', 'total': 142.50,
         'items': [
             {'sku': '333400', 'name': 'Jumper Wires 40pc M-M', 'qty': 5,  'location': 'B3-02'},
             {'sku': '333100', 'name': 'Arduino Uno R3',         'qty': 10, 'location': 'A1-01'},
             {'sku': '333205', 'name': 'Breadboard 830pt',       'qty': 10, 'location': 'A2-02'},
         ]},
    ]
    total_printed = 0
    errors = []
    hash_map = {}

    for i, o in enumerate(fake_orders):
        letter = LETTERS[i]
        bid = _barcode_id(o['number'])
        hash_map[o['number']] = bid
        try:
            img = summary_label.generate(
                order_number=o['number'],
                buyer_name=o['buyer'],
                num_items=len(o['items']),
                delivery_method=o['delivery'],
                total=o['total'],
                letter=letter,
                barcode_id=bid,
            )
            printer_svc.print_label(img, printer='small',
                                    meta={'order': o['number'], 'label_type': 'Order Summary'},
                                    also_preview=True)
            total_printed += 1
        except Exception as exc:
            errors.append(f'{o["number"]} summary: {exc}')
        for it in o['items']:
            try:
                img_item = item_label.generate(
                    sku=it['sku'],
                    product_name=it['name'],
                    quantity=it['qty'],
                    order_number=o['number'],
                    warehouse_location=it['location'],
                    letter=letter,
                    barcode_id=bid,
                )
                printer_svc.print_label(img_item, printer='small',
                                        meta={'order': o['number'], 'label_type': f'Item: {it["sku"]}'},
                                        also_preview=True)
                total_printed += 1
            except Exception as exc:
                errors.append(f'{o["number"]} {it["sku"]}: {exc}')

    return jsonify(ok=True, printed=total_printed, errors=errors, hash_map=hash_map)


@orders_bp.route('/orders/print-shipping-label', methods=['POST'])
@login_required
def print_shipping_label():
    """Print a carrier-appropriate shipping label. Returns tracking_number (or None for pickup)."""
    from app.labels.render import new_label, load_font, draw_barcode, LARGE_LABEL_W, LARGE_LABEL_H

    body = request.get_json(silent=True) or {}
    order_number    = (body.get('order_number')    or '').strip()
    barcode_id      = (body.get('barcode_id')      or order_number).strip()
    delivery_method = (body.get('delivery_method') or '').strip()
    if not order_number:
        return jsonify(ok=False, error='order_number required'), 400

    db_path = current_app.config['DATABASE_PATH']
    order   = database.get_order(order_number, db_path)
    order   = dict(order) if order else {'number': order_number}

    # delivery_method from JS overrides DB value for test orders not in the DB
    if delivery_method and not order.get('deliveryMethod'):
        order['deliveryMethod'] = delivery_method

    country = (order.get('deliveryCountry') or order.get('buyerCountry') or 'HR').strip()
    carrier = _detect_carrier(order.get('deliveryMethod') or order.get('carrier') or '', country)
    addr    = _parse_shipping_address(order)

    tracking_number = None
    zpl_label       = None
    error_msg       = None

    # ── Carrier dispatch ────────────────────────────────────────────────────────
    try:
        if carrier in ('dhl', 'dhl_eu', 'dhl_int'):
            # TODO: replace mock with real API call once credentials are configured
            tracking_number = 'DHL' + ''.join(random.choices(string.digits, k=10))

        elif carrier == 'dpd':
            # TODO: replace mock with real API call once credentials are configured
            tracking_number = 'DPD' + ''.join(random.choices(string.digits, k=10))

        elif carrier == 'gls':
            tracking_number = f'GLS-{barcode_id}'   # scanned for confirmation

        elif carrier == 'pickup':
            tracking_number = None   # no tracking; confirm screen skips tracking slot

        else:
            error_msg = f'Carrier not recognised: "{carrier or order.get("deliveryMethod", "")}"'

    except Exception as exc:
        error_msg = str(exc)

    # ── Build PIL label ─────────────────────────────────────────────────────────
    w, h = LARGE_LABEL_W, LARGE_LABEL_H
    img, draw = new_label(w, h)

    def _addr_lines():
        lines = []
        if addr.get('name'):   lines.append(addr['name'])
        if addr.get('street'): lines.append(addr['street'])
        pc_city = f"{addr.get('postal_code','')} {addr.get('city','')}".strip()
        if pc_city: lines.append(pc_city)
        if addr.get('country'): lines.append(addr['country'])
        return lines

    if error_msg:
        # ── Error label ─────────────────────────────────────────────────────────
        y = 30
        draw.text((40, y), 'GREŠKA - SHIPPING LABEL',  fill=0, font=load_font(52, bold=True)); y += 74
        draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 20
        draw.text((40, y), f'Narudžba:  {order_number}', fill=0, font=load_font(40)); y += 56
        del_method = order.get('deliveryMethod') or ''
        if del_method:
            draw.text((40, y), f'Dostava:   {del_method}', fill=0, font=load_font(40)); y += 56
        draw.rectangle([(40, y), (w - 40, y + 3)], fill=0); y += 20
        draw.text((40, y), 'Greška:', fill=0, font=load_font(36, bold=True)); y += 46
        # Word-wrap long error messages
        words = error_msg.split()
        line = ''
        for word in words:
            test = (line + ' ' + word).strip()
            if draw.textlength(test, font=load_font(34)) > w - 80:
                draw.text((40, y), line, fill=0, font=load_font(34)); y += 46; line = word
            else:
                line = test
        if line:
            draw.text((40, y), line, fill=0, font=load_font(34)); y += 56
        draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 20
        draw.text((40, y), 'RUČNO KREIRAJTE OTPREMNICU', fill=0, font=load_font(44, bold=True))

    elif carrier in ('dhl', 'dhl_eu', 'dhl_int'):
        # ── DHL mock label ──────────────────────────────────────────────────────
        y = 30
        draw.text((40, y), 'DHL EXPRESS',   fill=0, font=load_font(70, bold=True)); y += 94
        draw.text((40, y), '[MOCK LABEL]',  fill=0, font=load_font(36)); y += 52
        draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 20
        draw.text((40, y), f'Narudžba: {order_number}', fill=0, font=load_font(42, bold=True)); y += 60
        draw.text((40, y), 'Primatelj:', fill=0, font=load_font(34, bold=True)); y += 46
        for line in _addr_lines():
            draw.text((60, y), line, fill=0, font=load_font(38)); y += 50
        y += 10
        draw.rectangle([(40, y), (w - 40, y + 3)], fill=0); y += 20
        draw.text((40, y), f'Tracking: {tracking_number}', fill=0, font=load_font(36, bold=True)); y += 52
        draw_barcode(img, tracking_number, x=40, y=y, width=w - 80, height=80)

    elif carrier == 'dpd':
        # ── DPD mock label ──────────────────────────────────────────────────────
        y = 30
        draw.text((40, y), 'DPD',          fill=0, font=load_font(70, bold=True)); y += 94
        draw.text((40, y), '[MOCK LABEL]', fill=0, font=load_font(36)); y += 52
        draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 20
        draw.text((40, y), f'Narudžba: {order_number}', fill=0, font=load_font(42, bold=True)); y += 60
        draw.text((40, y), 'Primatelj:', fill=0, font=load_font(34, bold=True)); y += 46
        for line in _addr_lines():
            draw.text((60, y), line, fill=0, font=load_font(38)); y += 50
        y += 10
        draw.rectangle([(40, y), (w - 40, y + 3)], fill=0); y += 20
        draw.text((40, y), f'Tracking: {tracking_number}', fill=0, font=load_font(36, bold=True)); y += 52
        draw_barcode(img, tracking_number, x=40, y=y, width=w - 80, height=80)

    elif carrier == 'gls':
        # ── GLS manual label ────────────────────────────────────────────────────
        gls_barcode = f'GLS{barcode_id}'
        y = 30
        draw.text((40, y), 'GLS', fill=0, font=load_font(80, bold=True)); y += 106
        draw_barcode(img, gls_barcode, x=40, y=y, width=w - 80, height=80); y += 106
        draw.text((40, y), gls_barcode, fill=0, font=load_font(30)); y += 48
        draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 20
        draw.text((40, y), f'Narudžba: {order_number}', fill=0, font=load_font(42, bold=True)); y += 62
        draw.text((40, y), 'RUČNO KREIRATI GLS OTPREMNICU', fill=0, font=load_font(40, bold=True)); y += 60
        draw.rectangle([(40, y), (w - 40, y + 3)], fill=0); y += 18
        draw.text((40, y), 'Primatelj:', fill=0, font=load_font(34, bold=True)); y += 46
        for line in _addr_lines():
            draw.text((60, y), line, fill=0, font=load_font(38)); y += 50
        del_method = order.get('deliveryMethod') or ''
        if del_method:
            y += 6
            draw.text((60, y), f'Dostava: {del_method}', fill=0, font=load_font(36)); y += 50

    elif carrier == 'pickup':
        # ── Personal pickup label ───────────────────────────────────────────────
        y = 30
        draw.text((40, y), 'OSOBNO PREUZIMANJE', fill=0, font=load_font(62, bold=True)); y += 88
        draw_barcode(img, barcode_id, x=40, y=y, width=w - 80, height=80); y += 106
        draw.text((40, y), barcode_id, fill=0, font=load_font(30)); y += 48
        draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 20
        draw.text((40, y), f'Narudžba: {order_number}', fill=0, font=load_font(42, bold=True)); y += 62
        draw.text((40, y), 'Kupac:', fill=0, font=load_font(34, bold=True)); y += 46
        for line in _addr_lines():
            draw.text((60, y), line, fill=0, font=load_font(38)); y += 50

    try:
        if zpl_label:
            printer_svc.print_zpl(zpl_label, printer='large')
        else:
            printer_svc.print_label(img, printer='large',
                                    meta={'order': order_number, 'label_type': f'{carrier} Shipping'},
                                    also_preview=True)
        return jsonify(ok=True, tracking_number=tracking_number)
    except Exception as e:
        return jsonify(ok=False, error=str(e)), 500


@orders_bp.route('/orders/cancel-packing', methods=['POST'])
@login_required
def cancel_packing():
    """Print a cancellation notice label on the large printer."""
    from app.labels.render import new_label, load_font, LARGE_LABEL_W, LARGE_LABEL_H

    body = request.get_json(silent=True) or {}
    order_number = (body.get('order_number') or '').strip()
    buyer_name   = (body.get('buyer_name')   or '').strip()
    if not order_number:
        return jsonify(ok=False, error='order_number required'), 400

    # Fetch full order details from DB for address lines
    db_path = current_app.config['DATABASE_PATH']
    order = database.get_order(order_number, db_path)
    if order:
        order = dict(order)

    # Pull extended fields when order is available
    if order:
        buyer_name1  = (order.get('buyerName1') or order.get('buyerName') or buyer_name).strip()
        buyer_street = (order.get('buyerStreet') or '').strip()
        buyer_postal = (order.get('buyerPostalCode') or '').strip()
        buyer_city   = (order.get('buyerCity') or '').strip()
        buyer_country= (order.get('buyerCountry') or '').strip()
        buyer_tax    = (order.get('buyerTaxNumber') or '').strip()
        del_name     = (order.get('deliveryName') or '').strip()
        del_street   = (order.get('deliveryStreet') or '').strip()
        del_postal   = (order.get('deliveryPostalCode') or '').strip()
        del_city     = (order.get('deliveryCity') or '').strip()
        del_country  = (order.get('deliveryCountry') or '').strip()
        delivery_method = (order.get('deliveryMethod') or '').strip()
        total        = order.get('totalAmountInDomCurr')
        currency     = (order.get('documentCurrency') or 'EUR').strip()
    else:
        buyer_name1 = buyer_name
        buyer_street = buyer_postal = buyer_city = buyer_country = buyer_tax = ''
        del_name = del_street = del_postal = del_city = del_country = delivery_method = ''
        total = None; currency = 'EUR'

    # Determine if delivery address differs from billing
    ship_differs = del_street and del_street.lower() != buyer_street.lower()

    w, h = LARGE_LABEL_W, LARGE_LABEL_H
    img, draw = new_label(w, h)

    y = 30
    draw.text((40, y), order_number, fill=0, font=load_font(90, bold=True)); y += 110
    draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 20

    # Invoice / billing info
    draw.text((40, y), 'INVOICE:', fill=0, font=load_font(34, bold=True)); y += 44
    for line in filter(None, [
        buyer_name1,
        f'Tax: {buyer_tax}' if buyer_tax else None,
        buyer_street,
        f'{buyer_postal} {buyer_city}'.strip(),
        buyer_country,
    ]):
        draw.text((60, y), line, fill=0, font=load_font(38)); y += 50
    if total is not None:
        draw.text((60, y), f'Total: {total:.2f} {currency}', fill=0, font=load_font(38)); y += 50
    y += 6
    draw.rectangle([(40, y), (w - 40, y + 3)], fill=0); y += 18

    # Shipping info
    draw.text((40, y), 'SHIPPING:', fill=0, font=load_font(34, bold=True)); y += 44
    ship_name_line = del_name if (ship_differs and del_name and del_name.lower() != buyer_name1.lower()) else buyer_name1
    ship_addr = (del_street, f'{del_postal} {del_city}'.strip(), del_country) if ship_differs else \
                (buyer_street, f'{buyer_postal} {buyer_city}'.strip(), buyer_country)
    for line in filter(None, [ship_name_line, *ship_addr, delivery_method or None]):
        draw.text((60, y), line, fill=0, font=load_font(38)); y += 50
    y += 6
    draw.rectangle([(40, y), (w - 40, y + 4)], fill=0); y += 24

    # Cancellation notice
    draw.text((40, y), 'CANCELLED PROCESSING',    fill=0, font=load_font(52, bold=True)); y += 68
    draw.text((40, y), 'INVOICE HAS BEEN MADE',   fill=0, font=load_font(44, bold=True)); y += 60
    draw.text((40, y), 'FINISH PROCESSING',        fill=0, font=load_font(44, bold=True)); y += 56
    draw.text((40, y), 'WITH E-RACUNI',            fill=0, font=load_font(44, bold=True))

    try:
        printer_svc.print_label(img, printer='large',
                                meta={'order': order_number, 'label_type': 'Cancelled'},
                                also_preview=True)
        return jsonify(ok=True)
    except Exception as e:
        return jsonify(ok=False, error=str(e)), 500


@orders_bp.route('/orders/confirm-packed', methods=['POST'])
@login_required
def confirm_packed():
    """Send shipping confirmation email after packing + shipping label scan verified."""
    from app.services.notification import notify_customer

    db_path = current_app.config['DATABASE_PATH']
    webhook  = current_app.config.get('MAKE_ORDER_NOTIFY_URL', '')
    body = request.get_json(silent=True) or {}
    order_number    = (body.get('order_number') or '').strip()
    tracking_number = (body.get('tracking_number') or '').strip() or None

    if not order_number:
        return jsonify(ok=False, error='order_number required'), 400

    test_mode  = current_app.config.get('MAKE_TEST_MODE', '') == '1'
    test_email = database.get_setting('MAKE_TEST_EMAIL', db_path) if test_mode else None

    if test_mode and not test_email:
        return jsonify(ok=False, error='Test mode is on but no test email address is set in Settings → make.com'), 400

    order = database.get_order(order_number, db_path)

    if order:
        order = dict(order)
    else:
        # Fake / test orders (not in DB) only work when test mode is on
        if not test_mode:
            return jsonify(ok=False, error=f'Order {order_number} not found'), 404
        _test_orders = {
            'S10001': {
                'buyerName': 'Horvat', 'buyerName1': 'Marko Horvat',
                'buyerStreet': 'Ilica 42', 'buyerPostalCode': '10000',
                'buyerCity': 'Zagreb', 'buyerCountry': 'HR',
                'carrier': 'DHL', 'deliveryMethod': 'DHL',
            },
            'S10002': {
                'buyerName': 'Kovač', 'buyerName1': 'Jana Kovač',
                'buyerStreet': 'Vukovarska 15', 'buyerPostalCode': '21000',
                'buyerCity': 'Split', 'buyerCountry': 'HR',
                'carrier': 'DPD', 'deliveryMethod': 'DPD',
            },
            'S10003': {
                'buyerName': 'Müller', 'buyerName1': 'Klaus Müller',
                'buyerStreet': 'Hauptstraße 8', 'buyerPostalCode': '80331',
                'buyerCity': 'München', 'buyerCountry': 'DE',
                'carrier': 'GLS', 'deliveryMethod': 'GLS',
            },
            '2026-00044': {
                'buyerName': 'OŠ Nikola Tesla Zagreb', 'buyerName1': 'OŠ Nikola Tesla Zagreb',
                'buyerStreet': 'Šegavčeva 9', 'buyerPostalCode': '10000',
                'buyerCity': 'Zagreb', 'buyerCountry': 'HR',
                'carrier': 'Pickup', 'deliveryMethod': 'Osobno preuzimanje',
            },
        }
        base = _test_orders.get(order_number, {
            'buyerName': 'Test Customer', 'buyerName1': 'Test Customer',
            'buyerStreet': 'Testna ulica 1', 'buyerPostalCode': '10000',
            'buyerCity': 'Zagreb', 'buyerCountry': 'HR',
            'carrier': 'DHL', 'deliveryMethod': 'DHL',
        })
        order = {'number': order_number, 'buyerEMail': test_email, **base}

    if test_mode:
        order['buyerEMail'] = test_email

    # Load invoice and delivery note PDFs saved during order processing.
    # In test mode, fall back to sample PDFs so Make.com always receives both files.
    import pathlib
    pdfs_dir = os.path.join(os.getcwd(), 'pdfs')
    sample_pdf_path = pathlib.Path(current_app.root_path) / 'static' / 'sample.pdf'
    sample_pdf_bytes = sample_pdf_path.read_bytes() if sample_pdf_path.exists() else None

    invoice_pdf = None
    delivery_note_pdf = None
    invoice_path = os.path.join(pdfs_dir, f'{order_number}_invoice.pdf')
    dn_path = os.path.join(pdfs_dir, f'{order_number}_delivery_note.pdf')

    if os.path.exists(invoice_path):
        with open(invoice_path, 'rb') as f:
            invoice_pdf = f.read()
    elif test_mode:
        invoice_pdf = sample_pdf_bytes

    if os.path.exists(dn_path):
        with open(dn_path, 'rb') as f:
            delivery_note_pdf = f.read()
    elif test_mode:
        delivery_note_pdf = sample_pdf_bytes

    result = notify_customer(order, webhook, tracking_number=tracking_number,
                             invoice_pdf=invoice_pdf, delivery_note_pdf=delivery_note_pdf)
    status = 200 if result['ok'] else 502
    return jsonify(**result), status


@orders_bp.route('/orders/process-stream')
def process_stream():
    """SSE endpoint: auto-process selected orders.

    Query param: orders=S1001,S1002,S1003,S1004 (comma-separated, max 4)
    """
    db_path = current_app.config['DATABASE_PATH']
    order_numbers_raw = request.args.get('orders', '')
    selected = [n.strip() for n in order_numbers_raw.split(',') if n.strip()][:4]

    if not selected:
        def empty_stream():
            yield _sse_event('', 0, 'error', 'No orders specified')
        return Response(stream_with_context(empty_stream()),
                        mimetype='text/event-stream')

    # Capture app context for the generator
    app = current_app._get_current_object()

    def generate():
        with app.app_context():
            for i, number in enumerate(selected):
                letter = LETTERS[i] if i < len(LETTERS) else str(i + 1)
                order = database.get_order(number, db_path)
                if not order:
                    yield _sse_event(number, 0, 'error', f'Order {number} not found in DB')
                    continue

                # Emit a start event
                yield _sse_event(number, 0, 'running', f'Starting order {number} ({letter})')

                # Print order labels first
                items = _parse_items(order)
                try:
                    currency = order.get('documentCurrency') or 'EUR'
                    img_summary = summary_label.generate(
                        order_number=number,
                        buyer_name=order.get('buyerName', ''),
                        num_items=len(items),
                        delivery_method=order.get('deliveryMethod', ''),
                        total=float(order.get('totalAmountInDomCurr') or 0),
                        currency=currency,
                        letter=letter,
                    )
                    printer_svc.print_label(img_summary, printer='small',
                                            meta={'order': number, 'label_type': 'Order Summary'},
                                            also_preview=True)

                    for item in items:
                        r = _resolve_item(item, db_path)
                        raw_price = item.get('price') or item.get('unitPrice')
                        img_item = item_label.generate(
                            sku=r['sku'],
                            product_name=r['name'],
                            quantity=r['qty'],
                            order_number=number,
                            warehouse_location=r['location'],
                            item_price=float(raw_price) if raw_price is not None else None,
                            currency=currency,
                            letter=letter,
                        )
                        printer_svc.print_label(img_item, printer='small',
                                                meta={'order': number, 'label_type': f'Item: {r["sku"]}'},
                                                also_preview=True)
                except Exception as exc:
                    yield _sse_event(number, 0, 'error', f'Label print failed: {exc}')
                    # Non-fatal — continue with the rest

                yield from _process_single_order(order, letter=letter)

            # Signal completion
            payload = json.dumps({'done': True})
            yield f"data: {payload}\n\n"

    return Response(
        stream_with_context(generate()),
        mimetype='text/event-stream',
        headers={
            'Cache-Control': 'no-cache',
            'X-Accel-Buffering': 'no',
        },
    )


# ── Debug: dump a raw order to a txt file ─────────────────────────────────────

@orders_bp.route('/orders/dump-sample')
@orders_bp.route('/orders/dump-sample/<number>')
@login_required
def orders_dump_sample(number=None):
    """Dump raw e-računi API response + local DB row for an order to sample_order.txt."""
    import pprint
    db_path = current_app.config['DATABASE_PATH']
    sections = []

    # 1. Raw API response (SalesOrderGet) — shows actual field names
    if number:
        try:
            raw = eracuni.get_order(number)
            sections.append('=== RAW e-računi API (SalesOrderGet) ===')
            sections.append(pprint.pformat(raw, indent=2, width=120))
        except Exception as exc:
            sections.append(f'=== RAW API ERROR: {exc} ===')

    # 2. Local DB row
    sections.append('=== LOCAL DB ROW ===')
    if number:
        order = database.get_order(number, db_path)
        if not order:
            sections.append(f'Order {number} not found in DB.')
        else:
            for field in ('items', 'address'):
                if isinstance(order.get(field), str):
                    try:
                        order[field] = json.loads(order[field])
                    except Exception:
                        pass
            sections.append(pprint.pformat(order, indent=2, width=120))
    else:
        orders = database.get_all_orders(db_path)
        if not orders:
            sections.append('No orders in DB yet. Run a sync first.')
        else:
            order = orders[0]
            for field in ('items', 'address'):
                if isinstance(order.get(field), str):
                    try:
                        order[field] = json.loads(order[field])
                    except Exception:
                        pass
            sections.append(pprint.pformat(order, indent=2, width=120))

    txt = '\n\n'.join(sections)
    out_path = os.path.join(os.getcwd(), 'sample_order.txt')
    with open(out_path, 'w', encoding='utf-8') as f:
        f.write(txt)
    return Response(txt, mimetype='text/plain')


# ── Shared: enrich orders with standardised carrier field ─────────────────────

def _enrich_orders(orders: list, db_path: str) -> list:
    """Deserialise items and add a 'carrier' field (standardised shipping name)."""
    rules_json = database.get_setting('SHIPPING_RULES_JSON', db_path)
    rules = rules_from_json(rules_json)
    for o in orders:
        if isinstance(o.get('items'), str):
            try:
                o['items'] = json.loads(o['items'])
            except Exception:
                o['items'] = []
        country = o.get('deliveryCountry') or o.get('buyerCountry', '')
        o['carrier'] = get_carrier(o.get('deliveryMethod', ''), country, rules)
    return orders


# ── Processing-Orders JSON list (for Process Orders tab) ──────────────────────

@orders_bp.route('/orders/processing')
@login_required
def orders_processing_json():
    """Return Processing orders from local DB as JSON."""
    db_path = current_app.config['DATABASE_PATH']
    orders = database.get_processing_orders(db_path)
    return jsonify(orders=_enrich_orders(orders, db_path))


# ── All-Orders JSON list (for SPA tab) ────────────────────────────────────────

@orders_bp.route('/orders/list')
@login_required
def orders_list_json():
    """Return all orders from local DB as JSON."""
    db_path = current_app.config['DATABASE_PATH']
    orders = database.get_all_orders(db_path, exclude_statuses=['Draft'])
    return jsonify(orders=_enrich_orders(orders, db_path))


@orders_bp.route('/orders/sync', methods=['POST'])
@login_required
def orders_sync():
    """SSE stream: sync orders (and optionally products) from e-računi."""
    db_path = current_app.config['DATABASE_PATH']
    body = request.get_json(silent=True) or {}
    do_products = body.get('sync_products', False)

    def generate():
        try:
            for msg in sync_svc.sync_orders(db_path):
                yield f"data: {json.dumps({'message': msg})}\n\n"

            if do_products:
                for msg in sync_svc.sync_products(db_path):
                    yield f"data: {json.dumps({'message': msg})}\n\n"

        except Exception as exc:
            yield f"data: {json.dumps({'message': f'Error: {exc}', 'error': True})}\n\n"

        yield f"data: {json.dumps({'done': True})}\n\n"

    return Response(
        stream_with_context(generate()),
        mimetype='text/event-stream',
        headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'},
    )
