#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
ÖBB Tickets for MeeGo Harmattan
Version 3.14.0 - Fix: Dynamic clientversion from /version endpoint

Changes in 3.10.9:
- FIX: N26 erwartet POST zu "3ds-method-finish", nicht "3ds-method-start"
- FIX: N26_RISK_ENGINE_deviceId und deviceDna werden jetzt generiert
- FIX: Device-Fingerprint wird simuliert (wie Fingerprint2 Library)
- Speichere Bank-HTML für spätere Analyse in _postToBankForm
- Das sollte 3DS2 Challenge mit N26 und ähnlichen Banken fixen!

Changes in 3.10.8:
- Fix: threeDSMethodData aus URL extrahieren (nicht aus leerem HTML)

Changes in 3.10.1:
- NEW: 3DS2 Challenge Support (App-Bestätigung)
- Erkennt renderRedirectToAcs Response
- POSTet zu methodurl.psp-solutions.com
- Pollt notificationUrl auf Bestätigung
- Funktioniert mit N26, George, etc.

Changes in 3.10.0:
- CRITICAL FIX: POST to REDIRECTED URL, not original Saferpay URL!
- The browser follows redirect from /vt2/Api/SharedThreeDS/ to /ThreeDSAuthentication/
- POST must go to the final /ThreeDSAuthentication/ URL
- http_helper now tracks and saves final URL after redirects
- This should finally fix the "unexpected error" from Saferpay!

Changes in 3.9.9:
- FIX: Saferpay Cookies korrekt speichern und laden
- FIX: Mehr Logging für Cookie-Debugging
- FIX: http_helper handhabt Cookies automatisch

Changes in 3.9.8:
- DEBUG: Vollständige Saferpay GET/POST Antworten loggen
- DEBUG: Response Headers loggen
- DEBUG: Nach versteckten Form-Feldern suchen

Changes in 3.9.7:
- FIX: Nach "Ticket kaufen" automatisch zum Warenkorb navigieren
- FIX: handlePaymentFailed navigiert korrekt zum Warenkorb
- FIX: pageStack.push statt mainPageContent (existierte nicht)
- 3DS Payment: Saferpay erfordert echte JS-Ausführung (TODO)

Changes in 3.9.6:
- FIX: Passagiere und Warenkorb nach Auth-Restore laden
- FIX: Token Refresh vor dem Laden der Daten
- Jetzt werden Mitreisende auch bei gespeicherter Anmeldung angezeigt!

Changes in 3.9.5:
- FIX: Saferpay Cookies zwischen GET und POST speichern
- FIX: Bei Payment-Fehler automatisch zum Warenkorb wechseln
- Das sollte 3DS endlich zum Laufen bringen!

Changes in 3.9.4:
- Unterscheide zwischen Frictionless und Challenge
- Bei Challenge: Hinweis "Im Browser bezahlen"
- Bei Frictionless: Automatisch abschließen

Changes in 3.9.3:
- FIX: POST browser data to Saferpay (Width, Height, ColorDepth, etc.)
- This simulates what the JavaScript collectDataForm does
- No JavaScript needed!

Changes in 3.9.2:
- FIX: Call Saferpay URL BEFORE sending authorized: true
- This was the missing step for frictionless 3DS!
- Added 3DS challenge parsing (form/iframe handling)

Changes in 3.9.1:
- FIX: "Ticket kaufen" Button ruft jetzt buyTicket() auf
- Entfernt "wird in zukünftiger Version implementiert" Nachricht

Changes in 3.9.0:
- NEW: Complete in-app purchase flow (startPayment)
- NEW: Frictionless 3DS support (auto-authorize)
- NEW: Order overview after purchase
- NEW: getCartTotal, getCartId helper functions
- Falls back to browser if 3DS challenge required

Changes in 3.6.2:
- Added debug logging for cookies in http_helper and auth_helper
- Debug: shows which cookies are saved/loaded

Changes in 3.5.9:
- Added PDF request via email (requestTicketPdf)
- New pdfRequested/pdfFailed signals
- QML: PDF button on ticket details

Changes in 3.5.8:
- auth_helper: ALWAYS generate NEW support_id on login
- QML: Store and pass full station data (with extId)
- Fixed session binding after re-login

Changes in 3.5.7:
- Headers passed via environment variable (OEBB_HEADERS_FILE)
- Added missing getAuthSettings() function
- HAFAS: Full external ID with XML escaping
- Better debug logging

Changes in 3.5.6:
- Headers now passed via file (not command line)
- Fixed HAFAS connection search (correct ID format)
- Added debug logging for headers

Changes in 3.5.5:
- Fixed HAFAS station search (use XML API only)
- Fixed JWT parsing for Python 2.7 (base64 compat)
- Added debug logging in http_helper
- Better User-Agent (Firefox)

Changes in 3.5.4:
- Station search now uses HAFAS API (no auth needed)
- Connection search now uses HAFAS API (no auth needed)
- Shop API only used for tickets/purchase (when logged in)
- Added hafas_helper.py for HAFAS requests

Changes in 3.5.3:
- Fixed session binding: support_id now passed to auth_helper
- Extract user info from JWT token instead of API call (404 fix)
- Token expiry set to 15 minutes (matching JWT)
- Cookies saved for session persistence

Changes in 3.5.1:
- Added domain data loading (/api/domain/v2)
- Added discount cards selection (Vorteilscard, KlimaTicket, etc.)
- Added top 10 Austrian stations for quick selection
- Added device fingerprint support
- Added EPS banks for payment

Changes in 3.5.0:
- Added Payment initialization flow (/api/payment/v1/init)
- Added Shopping Cart support (/api/shoppingcart/v1, /api/order/v5/shoppingCart)
- Fixed Web API headers based on HAR analysis (Channel, ClientId, isoffernew)
- Improved ticket display with infocardType parameter
- Added ticket details view with QR code support
"""

import sys
import os
import json
import subprocess
import threading
import re
import uuid
import urllib
import hashlib
import base64
from datetime import datetime, timedelta

reload(sys)
sys.setdefaultencoding('utf-8')

from PySide.QtCore import QObject, QUrl, Slot, Signal, Property, QTimer, QDate, QTime
from PySide.QtGui import QApplication
from PySide.QtDeclarative import QDeclarativeView

# Paths
PYTHON311 = "/opt/wunderw/bin/python3.11"
HTTP_HELPER = "/opt/oebb-tickets/http_helper.py"
AUTH_HELPER = "/opt/oebb-tickets/auth_helper.py"
HAFAS_HELPER = "/opt/oebb-tickets/hafas_helper.py"
CONFIG_DIR = os.path.expanduser("~/.config/oebb-tickets")
AUTH_FILE = os.path.join(CONFIG_DIR, "auth.json")
FAVORITES_FILE = os.path.join(CONFIG_DIR, "favorites.json")
DEVICE_FILE = os.path.join(CONFIG_DIR, "device_id")
DOWNLOADED_FILE = os.path.join(CONFIG_DIR, "downloaded_pdfs.json")

# =====================================================
# API Configuration from HAR Analysis (Dec 2025)
# =====================================================
API_BASE = "https://shop.oebbtickets.at"
AUTH_REALM = "/auth/realms/customer/protocol/openid-connect"
TOKEN_URL = API_BASE + AUTH_REALM + "/token"
AUTH_URL = API_BASE + AUTH_REALM + "/auth"
LOGOUT_URL = API_BASE + AUTH_REALM + "/logout"
USERINFO_URL = API_BASE + AUTH_REALM + "/userinfo"

# Web API endpoints
TICKETS_URL = API_BASE + "/api/order/v1/infocards"
STATIONS_URL = API_BASE + "/api/hafas/v1/stations"
TIMETABLE_URL = API_BASE + "/api/hafas/v4/timetable"
PRICES_URL = API_BASE + "/api/offer/v1/prices"
OFFERS_URL = API_BASE + "/api/offer/v6/offers"
TRAVEL_ACTIONS_URL = API_BASE + "/api/offer/v2/travelActions"
RESERVATION_DETAILS_URL = API_BASE + "/api/offer/v1/reservation-details"
GENERAL_INFO_URL = API_BASE + "/api/domain/v2/generalInfo"
CART_COUNT_URL = API_BASE + "/api/order/v1/shoppingCart/count"

# NEW: Shopping Cart & Payment endpoints from HAR
SHOPPING_CART_URL = API_BASE + "/api/shoppingcart/v1"
SHOPPING_CART_ADD_URL = API_BASE + "/api/order/v5/shoppingCart"
SHOPPING_CART_DELETE_URL = API_BASE + "/api/order/v4/shoppingCart"
PAYMENT_INIT_URL = API_BASE + "/api/payment/v1/init"
PAYMENT_PAY_URL = API_BASE + "/api/payment/v1/pay"
PASSENGERS_URL = API_BASE + "/api/userdata/v2/passengers"

# Domain data endpoint (contains cards, top stations, config)
DOMAIN_DATA_URL = API_BASE + "/api/domain/v2"
FINGERPRINT_URL = API_BASE + "/api/domain/v1/fingerprint"
VERSION_URL = API_BASE + "/version"

# Common discount card IDs from domain data
DISCOUNT_CARDS = {
    'vorteilscard_classic': 108,
    'vorteilscard_senior': 118,
    'vorteilscard_jugend': 120,
    'vorteilscard_family': 7341162,
    'vorteilscard_comfort': 125,
    'vorteilscard_66': 9097862,
    'klimaticket_classic': 100000040,
    'klimaticket_jugend': 100000042,
    'klimaticket_senior': 100000048,
    'studentenausweis': 9097845,
    'bahncard_25_2kl': 127,
    'bahncard_50_2kl': 129,
}

# Top 10 Austrian stations (from domain data)
TOP_STATIONS = [
    {"name": "Wien Hbf (U)", "number": 1290401, "longitude": 16375325, "latitude": 48185507},
    {"name": "Innsbruck", "number": 1170101, "longitude": 11400973, "latitude": 47263771},
    {"name": "Salzburg", "number": 1150101, "longitude": 13045784, "latitude": 47814369},
    {"name": "Linz", "number": 1140101, "longitude": 14291509, "latitude": 48290240},
    {"name": "Graz", "number": 1160100, "longitude": 15417093, "latitude": 47071815},
    {"name": "St.Pölten", "number": 1130201, "longitude": 15623099, "latitude": 48208178},
    {"name": "Klagenfurt", "number": 1120101, "longitude": 14313766, "latitude": 46615820},
    {"name": "Wels", "number": 1140301, "longitude": 14028179, "latitude": 48166288},
    {"name": "Villach", "number": 1120201, "longitude": 13848484, "latitude": 46618390},
    {"name": "Wr.Neustadt", "number": 1130401, "longitude": 16233808, "latitude": 47811690},
]

# Client configuration from HAR
CLIENT_ID = "usermanagement"
CLIENT_VERSION = "2.4.11709-TSPNEU-153089-2"  # Fallback; overridden dynamically from /version
WEB_CLIENT_ID = "63"  # Updated to match browser

# PKCE toggle
USE_PKCE = True

# HAFAS for connection search
HAFAS_URL = "https://fahrplan.oebb.at/bin/query.exe"

def ensure_config_dir():
    if not os.path.exists(CONFIG_DIR):
        os.makedirs(CONFIG_DIR)

def get_device_id():
    ensure_config_dir()
    if os.path.exists(DEVICE_FILE):
        with open(DEVICE_FILE, 'r') as f:
            return f.read().strip()
    device_id = str(uuid.uuid4())
    with open(DEVICE_FILE, 'w') as f:
        f.write(device_id)
    return device_id

def generate_code_verifier():
    """Generate PKCE code verifier"""
    return base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode('ascii')

def generate_code_challenge(verifier):
    """Generate PKCE code challenge from verifier"""
    digest = hashlib.sha256(verifier.encode('ascii')).digest()
    return base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')

def generate_support_id():
    """Generate a support ID like WEB_OEBB_xxx_xxx (from HAR)"""
    import random
    import string
    chars = string.ascii_lowercase + string.digits
    part1 = ''.join(random.choice(chars) for _ in range(8))
    part2 = ''.join(random.choice(chars) for _ in range(8))
    return "WEB_OEBB_%s_%s" % (part1, part2)


class OEBBController(QObject):
    """Main controller for ÖBB App"""
    
    # Signals
    loadingChanged = Signal(bool)
    loginStateChanged = Signal()
    loginSuccess = Signal()
    loginFailed = Signal(unicode)
    stationsFound = Signal(unicode)
    connectionsFound = Signal(unicode)
    offersFound = Signal(unicode)
    offersLoaded = Signal(unicode)  # All offers for selection dialog
    reservationOptionsLoaded = Signal(unicode)  # Reservation options (compartments, preferences)
    ticketsFound = Signal(unicode)
    ticketDetailsFound = Signal(unicode)
    passengersFound = Signal(unicode)
    errorOccurred = Signal(unicode)
    sessionExpired = Signal()  # NEW: Session expired (HTTP 440)
    purchaseSuccess = Signal(unicode)
    purchaseFailed = Signal(unicode)
    captchaProgress = Signal(unicode, int, int)
    # NEW: Shopping Cart & Payment signals
    cartUpdated = Signal(unicode)
    paymentInitialized = Signal(unicode)
    paymentFailed = Signal(unicode)
    waiting3DS = Signal(bool)  # True = waiting, False = done
    # Domain data signals
    domainDataLoaded = Signal(unicode)
    # PDF signals (3.5.9)
    pdfRequested = Signal(unicode)  # Success message
    pdfFailed = Signal(unicode)     # Error message
    ticketItemLoaded = Signal(unicode)  # Ticket item details
    
    def __init__(self, parent=None):
        super(OEBBController, self).__init__(parent)
        self._loading = False
        self._pending = {}
        self._stationCache = {}
        self._searchTimer = None
        self._lastQuery = ""
        
        # Auth state
        self._accessToken = None
        self._refreshToken = None
        self._tokenExpiry = None
        self._userInfo = {}
        self._savedEmail = ''
        self._savedPassword = ''
        
        # Shopping cart state
        self._cartId = None
        self._cartItems = []
        self._lastReservationOptions = None  # Cached reservation options for buy
        self._reservationOnlyMode = False  # Search for reservation only (no ticket)
        
        # Passengers state (from /api/userdata/v2/passengers)
        self._passengers = []
        
        # Search filters state
        self._currentBikesFilter = False
        
        # Domain data (cards, config)
        self._domainData = {}
        self._discountCards = []
        
        # Downloaded PDFs tracking
        self._downloadedPdfs = {}  # {ticketId: filename}
        
        # PKCE state
        self._codeVerifier = None
        
        # Support ID (persistent per session)
        self._supportId = generate_support_id()
        
        # Dynamic client version (fetched from /version endpoint)
        self._clientVersion = None
        
        self._loadAuth()
        self._loadDownloadedPdfs()
    
    # ==================== Properties ====================
    
    def _get_loading(self):
        return self._loading
    
    def _set_loading(self, val):
        if self._loading != val:
            self._loading = val
            self.loadingChanged.emit(val)
    
    loading = Property(bool, _get_loading, notify=loadingChanged)
    
    @Slot(result=bool)
    def isLoggedIn(self):
        return self._accessToken is not None
    
    @Slot(result=unicode)
    def getUserEmail(self):
        return self._userInfo.get('email', '')
    
    @Slot(result=unicode)
    def getUserName(self):
        name = self._userInfo.get('name', '')
        if not name:
            name = self._userInfo.get('preferred_username', '')
        return name
    
    @Slot(result=unicode)
    def getAuthSettings(self):
        """Get authentication settings display text"""
        if self._accessToken:
            email = self._userInfo.get('email', '')
            name = self._userInfo.get('name', '')
            if name:
                return u"Eingeloggt als %s" % name
            elif email:
                return u"Eingeloggt als %s" % email
            else:
                return u"Eingeloggt"
        else:
            return u"Nicht eingeloggt - Tippen zum Anmelden"
    
    @Slot(result=unicode)
    def getSavedEmail(self):
        """Get saved email for login form pre-fill"""
        result = self._savedEmail or self._userInfo.get('email', '')
        print u"getSavedEmail() returning: %s" % result
        return result
    
    @Slot(result=unicode)
    def getSavedPassword(self):
        """Get saved password for login form pre-fill"""
        result = self._savedPassword or ''
        print u"getSavedPassword() returning: %s" % ('***' if result else '(empty)')
        return result
    
    # ==================== HTTP Helper ====================
    
    def _doRequest(self, requestId, method, url, data=None, headers=None):
        """Execute HTTP request via Python 3.11 helper"""
        try:
            data_str = data if data else ""
            headers_str = json.dumps(headers) if headers else "{}"
            
            # Write headers to temp file to avoid command line issues
            ensure_config_dir()
            headers_file = os.path.join(CONFIG_DIR, "request_headers.json")
            with open(headers_file, 'w') as f:
                f.write(headers_str)
            
            cmd = [PYTHON311, HTTP_HELPER, method, url, data_str]
            
            print u"Executing: %s %s %s" % (PYTHON311, method, url[:50])
            if headers:
                print u"Headers: AccessToken=%s, support_id=%s" % (
                    'present' if headers.get('AccessToken') else 'MISSING',
                    headers.get('x-ts-supportid', 'MISSING')
                )
            
            env = os.environ.copy()
            env['PYTHONIOENCODING'] = 'utf-8'
            env['OEBB_HEADERS_FILE'] = headers_file  # Pass via environment!
            
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
            out, err = proc.communicate()
            
            if err:
                print u"HTTP Helper stderr: %s" % err[:300]
            
            if isinstance(out, str):
                try:
                    out = out.decode('utf-8')
                except:
                    out = out.decode('iso-8859-1', 'replace')
            
            self._pending[requestId] = out
        except Exception as e:
            print u"Request error: %s" % e
            import traceback
            traceback.print_exc()
            self._pending[requestId] = json.dumps({"error": unicode(e)})
    
    def _startRequest(self, requestId, method, url, data=None, headers=None, callback=None):
        """Start async HTTP request"""
        self._set_loading(True)
        
        def runner():
            self._doRequest(requestId, method, url, data, headers)
            result = self._pending.get(requestId, '{"error": "No response"}')
            self._set_loading(False)
            if callback:
                callback(result)
        
        thread = threading.Thread(target=runner)
        thread.daemon = True
        thread.start()
    
    # ==================== Web API Headers (from HAR) ====================
    
    def _getWebApiHeaders(self, withAuth=True, contentType=None):
        """Get headers for shop.oebbtickets.at Web API based on HAR analysis"""
        headers = {
            'Accept': 'application/json, text/plain, */*',
            'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
            'Channel': 'inet',
            'Lang': 'de',
            'ClientId': WEB_CLIENT_ID,
            'clientversion': self._clientVersion or CLIENT_VERSION,
            'x-ts-supportid': self._supportId,
            'isoffernew': 'true',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0',
            'Referer': 'https://shop.oebbtickets.at/de/ticket',
            'Origin': 'https://shop.oebbtickets.at',
            'Connection': 'keep-alive',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-origin'
        }
        
        if withAuth and self._accessToken:
            # HAR shows: AccessToken header (NOT Authorization: Bearer!)
            headers['AccessToken'] = self._accessToken
        
        if contentType:
            headers['Content-Type'] = contentType
        
        return headers
    
    def _initSession(self):
        """Initialize session on server after login - REQUIRED for API access!
        
        The ÖBB server requires POST /api/domain/v1/initUserData to be called
        after login to register the support_id. Without this, API calls fail
        with HTTP 440 (session expired).
        """
        print u"=== INITIALIZING SESSION ==="
        
        url = API_BASE + "/api/domain/v1/initUserData"
        headers = self._getWebApiHeaders(withAuth=True, contentType='application/json')
        
        def worker():
            try:
                data_str = "{}"
                
                # Write headers to temp file
                ensure_config_dir()
                headers_file = os.path.join(CONFIG_DIR, "request_headers.json")
                with open(headers_file, 'w') as f:
                    f.write(json.dumps(headers))
                
                cmd = [PYTHON311, HTTP_HELPER, "POST", url, data_str]
                
                print u"Session init: POST %s" % url
                
                env = os.environ.copy()
                env['PYTHONIOENCODING'] = 'utf-8'
                env['OEBB_HEADERS_FILE'] = headers_file
                
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
                out, err = proc.communicate()
                
                if err:
                    print u"Session init stderr: %s" % err[:200]
                
                if isinstance(out, str):
                    try:
                        out = out.decode('utf-8')
                    except:
                        out = out.decode('iso-8859-1', 'replace')
                
                print u"Session init result: %s" % out[:200]
                
                if out:
                    try:
                        result = json.loads(out)
                        if result.get('sessionId'):
                            print u"Session initialized: %s" % result.get('sessionId')[:40]
                        elif result.get('error'):
                            print u"Session init error: %s" % result.get('error')
                    except:
                        pass
                
                # Step 2: Send fingerprint (required for session validation)
                fingerprint_url = API_BASE + "/api/domain/v1/fingerprint"
                fingerprint_data = json.dumps({
                    "timezone": "-60",
                    "video": "854x480x32",
                    "superCookies": "DOM localStorage: Yes, DOM sessionStorage: Yes, IE userData: No",
                    "cookieEnabled": "true",
                    "platform": "MeeGo",
                    "userAgent": "OeBB-Tickets/3.13 MeeGo Harmattan",
                    "javaEnabled": False
                })
                
                cmd2 = [PYTHON311, HTTP_HELPER, "POST", fingerprint_url, fingerprint_data]
                print u"Session fingerprint: POST %s" % fingerprint_url
                
                proc2 = subprocess.Popen(cmd2, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
                out2, err2 = proc2.communicate()
                
                if out2:
                    print u"Fingerprint result: %s" % out2[:100]
                        
            except Exception as e:
                print u"Session init exception: %s" % e
        
        thread = threading.Thread(target=worker)
        thread.daemon = True
        thread.start()
        # Give it a moment to complete
        thread.join(timeout=5.0)
        
        # Load passengers after session is initialized
        self.loadPassengers()
    
    def _fetchReservationDetailsSync(self, connectionId, offerId, sectionId, compartmentId):
        """Fetch compartment details from reservation-details API synchronously.
        
        Returns dict with:
        - title (de/en)
        - accommodationType (SEAT/COUCHETTE/SLEEPER)
        - compartmentGroup
        Or None on error.
        """
        try:
            headers = self._getWebApiHeaders(withAuth=True, contentType='application/json')
            
            # Build request body matching HAR structure
            request_body = {
                "selection": {
                    "connectionId": connectionId,
                    "offerSections": [{
                        "class": "2",
                        "id": str(sectionId),
                        "offerId": offerId,
                        "reservation": {
                            "isSelected": True,
                            "sections": [],
                            "compartmentId": int(compartmentId)
                        }
                    }]
                }
            }
            
            # Write headers to temp file
            ensure_config_dir()
            headers_file = os.path.join(CONFIG_DIR, "request_headers.json")
            with open(headers_file, 'w') as f:
                f.write(json.dumps(headers))
            
            data_str = json.dumps(request_body)
            cmd = [PYTHON311, HTTP_HELPER, "POST", RESERVATION_DETAILS_URL, data_str]
            
            print u"Fetching reservation details for compartment %s" % compartmentId
            
            env = os.environ.copy()
            env['PYTHONIOENCODING'] = 'utf-8'
            env['OEBB_HEADERS_FILE'] = headers_file
            
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
            out, err = proc.communicate(timeout=10)
            
            if isinstance(out, str):
                try:
                    out = out.decode('utf-8')
                except:
                    out = out.decode('iso-8859-1', 'replace')
            
            if out:
                result = json.loads(out)
                # Navigate to compartment details
                for section in result.get('sections', []):
                    rd = section.get('reservationDetails', {})
                    for comp in rd.get('compartments', []):
                        if str(comp.get('id')) == str(compartmentId):
                            title = comp.get('title', {})
                            return {
                                'id': str(comp.get('id')),
                                'title_de': title.get('de', ''),
                                'title_en': title.get('en', ''),
                                'accommodationType': comp.get('accommodationType', ''),
                                'compartmentGroup': comp.get('compartmentGroup', '')
                            }
            return None
        except Exception as e:
            print u"Error fetching reservation details: %s" % e
            return None
    
    @Slot(unicode, unicode, unicode, unicode, unicode)
    def getReservationOptions(self, connectionId, offerId, sectionId="0", travelClass="2", compartmentId=""):
        """Fetch available reservation options for a connection/offer.
        
        Calls /api/offer/v1/reservation-details and returns the available
        compartments and seat preferences as JSON to the UI.
        
        Args:
            connectionId: The connection ID
            offerId: The offer ID
            sectionId: The offer section ID (default "0")
            travelClass: Travel class (1 or 2)
            compartmentId: Optional compartment ID for nightjet
        """
        if not self._accessToken:
            self.errorOccurred.emit(u"Bitte zuerst anmelden")
            return
        
        print u"=== GET RESERVATION OPTIONS ==="
        print u"ConnectionId: %s" % connectionId[:50]
        print u"OfferId: %s" % offerId[:30]
        print u"SectionId: %s, Class: %s" % (sectionId, travelClass)
        if compartmentId:
            print u"CompartmentId: %s" % compartmentId
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(withAuth=True, contentType='application/json')
        
        # Build reservation request
        reservation_obj = {"isSelected": True}
        if compartmentId:
            reservation_obj["sections"] = []
            reservation_obj["compartmentId"] = int(compartmentId)
        
        request_body = {
            "selection": {
                "connectionId": connectionId,
                "offerSections": [{
                    "class": travelClass,
                    "id": str(sectionId),
                    "offerId": offerId,
                    "reservation": reservation_obj
                }]
            }
        }
        
        def onReservationResult(result):
            try:
                data = json.loads(result)
                print u"Reservation details response: %s" % result[:500]
                
                # Build UI-friendly result
                ui_sections = []
                for section in data.get('sections', []):
                    rd = section.get('reservationDetails')
                    if not rd:
                        continue
                    
                    sec_from = section.get('from', {}).get('name', '')
                    sec_to = section.get('to', {}).get('name', '')
                    ride = section.get('ride', {})
                    ride_name = u"%s %s" % (ride.get('shortName', ''), ride.get('number', ''))
                    
                    price = rd.get('price', {})
                    base_price = price.get('base', 0) if isinstance(price, dict) else price
                    
                    ui_compartments = []
                    for comp in rd.get('compartments', []):
                        title = comp.get('title', {})
                        title_str = title.get('de', '') if isinstance(title, dict) else str(title)
                        
                        ui_prefs = []
                        for pref in comp.get('preferences', []):
                            pref_title = pref.get('title', {})
                            pref_str = pref_title.get('de', '') if isinstance(pref_title, dict) else str(pref_title)
                            alloc = pref.get('allocation', {})
                            ui_prefs.append({
                                'id': pref.get('id', ''),
                                'name': pref_str,
                                'isSelected': pref.get('isSelected', False),
                                'isChangeable': pref.get('isChangeable', True),
                                'position': alloc.get('positionOfPlaces', '')
                            })
                        
                        # Gender variations
                        ui_variations = []
                        for var in comp.get('variations', []):
                            ui_variations.append({
                                'id': var.get('id', ''),
                                'isSelected': var.get('isSelected', False),
                                'gender': var.get('genderDesignation', '')
                            })
                        
                        ui_compartments.append({
                            'id': str(comp.get('id', '')),
                            'name': title_str,
                            'accommodationType': comp.get('accommodationType', ''),
                            'isSelected': comp.get('isSelected', False),
                            'preferences': ui_prefs,
                            'variations': ui_variations
                        })
                    
                    ui_sections.append({
                        'from': sec_from,
                        'to': sec_to,
                        'ride': ride_name.strip(),
                        'price': base_price,
                        'isChangeable': rd.get('isChangeable', True),
                        'compartments': ui_compartments
                    })
                
                print u"Reservation options: %d sections" % len(ui_sections)
                self._lastReservationOptions = ui_sections
                self.reservationOptionsLoaded.emit(json.dumps(ui_sections))
                
            except Exception as e:
                print u"getReservationOptions error: %s" % e
                import traceback
                traceback.print_exc()
                self.errorOccurred.emit(u"Fehler beim Laden der Reservierungsoptionen")
        
        self._startRequest("get_reservation_options", "POST", RESERVATION_DETAILS_URL,
                          json.dumps(request_body), headers=headers, callback=onReservationResult)
    
    # ==================== Auth State ====================
    
    def _saveAuth(self, email=None, password=None):
        """Save auth state to file"""
        ensure_config_dir()
        data = {
            'access_token': self._accessToken,
            'refresh_token': self._refreshToken,
            'token_expiry': self._tokenExpiry.isoformat() if self._tokenExpiry else None,
            'user_info': self._userInfo,
            'support_id': self._supportId  # Save support_id for session binding
        }
        # Save credentials if provided (base64 encoded for minimal obfuscation)
        # Use parameter if provided, otherwise use instance variable
        email_to_save = email if email else self._savedEmail
        password_to_save = password if password else self._savedPassword
        
        if email_to_save:
            data['saved_email'] = email_to_save.encode('utf-8').encode('base64').strip()
            print u"Saving email: %s" % email_to_save
        if password_to_save:
            data['saved_password'] = password_to_save.encode('utf-8').encode('base64').strip()
            print u"Saving password: ***"
        
        with open(AUTH_FILE, 'w') as f:
            json.dump(data, f)
        print u"Auth saved to %s" % AUTH_FILE
    
    def _loadAuth(self):
        """Load auth state from file"""
        if os.path.exists(AUTH_FILE):
            try:
                with open(AUTH_FILE, 'r') as f:
                    data = json.load(f)
                print u"Loaded auth file, keys: %s" % data.keys()
                self._accessToken = data.get('access_token')
                self._refreshToken = data.get('refresh_token')
                # Load support_id from auth file to maintain session binding
                if data.get('support_id'):
                    self._supportId = data.get('support_id')
                if data.get('token_expiry'):
                    try:
                        self._tokenExpiry = datetime.fromisoformat(data['token_expiry'])
                    except:
                        self._tokenExpiry = datetime.strptime(data['token_expiry'], "%Y-%m-%dT%H:%M:%S.%f")
                self._userInfo = data.get('user_info', {})
                
                # Load saved credentials
                if data.get('saved_email'):
                    try:
                        self._savedEmail = data['saved_email'].decode('base64').decode('utf-8')
                        print u"Loaded saved email: %s" % self._savedEmail
                    except Exception as e:
                        print u"Failed to decode saved email: %s" % e
                        self._savedEmail = ''
                else:
                    print u"No saved_email in auth file"
                if data.get('saved_password'):
                    try:
                        self._savedPassword = data['saved_password'].decode('base64').decode('utf-8')
                        print u"Loaded saved password: ***"
                    except Exception as e:
                        print u"Failed to decode saved password: %s" % e
                        self._savedPassword = ''
                else:
                    print u"No saved_password in auth file"
                
                # Notify QML about login state
                if self._accessToken:
                    print u"Loaded saved auth token"
                    # Emit signal after a short delay to ensure QML is ready
                    QTimer.singleShot(100, self._emitLoginState)
            except Exception as e:
                print u"Failed to load auth: %s" % e
        else:
            print u"Auth file does not exist: %s" % AUTH_FILE
    
    def _emitLoginState(self):
        """Emit login state signal and load data after restored auth"""
        self.loginStateChanged.emit()
        
        # If we have a valid token, refresh if needed and load data
        if self._accessToken:
            print u"Auth restored - refreshing token if needed..."
            self.refreshTokenIfNeeded()
            print u"Auth restored - loading passengers and cart..."
            self.loadPassengers()
            self.loadCart()
    
    def _loadDownloadedPdfs(self):
        """Load list of downloaded PDFs from file"""
        if os.path.exists(DOWNLOADED_FILE):
            try:
                with open(DOWNLOADED_FILE, 'r') as f:
                    self._downloadedPdfs = json.load(f)
                print u"Loaded %d downloaded PDF records" % len(self._downloadedPdfs)
            except Exception as e:
                print u"Failed to load downloaded PDFs: %s" % e
                self._downloadedPdfs = {}
    
    def _saveDownloadedPdfs(self):
        """Save list of downloaded PDFs to file"""
        ensure_config_dir()
        try:
            with open(DOWNLOADED_FILE, 'w') as f:
                json.dump(self._downloadedPdfs, f)
        except Exception as e:
            print u"Failed to save downloaded PDFs: %s" % e
    
    def _markPdfDownloaded(self, ticketId, filename):
        """Mark a ticket as having its PDF downloaded"""
        self._downloadedPdfs[ticketId] = filename
        self._saveDownloadedPdfs()
        print u"Marked ticket %s as downloaded: %s" % (ticketId, filename)
    
    def _getPdfFilename(self, ticketId):
        """Get the filename of a downloaded PDF, or empty string if not downloaded"""
        return self._downloadedPdfs.get(ticketId, '')
    
    def _clearAuth(self):
        """Clear auth state but KEEP saved credentials for re-login"""
        # Save credentials before clearing
        saved_email = self._savedEmail
        saved_password = self._savedPassword
        
        self._accessToken = None
        self._refreshToken = None
        self._tokenExpiry = None
        self._userInfo = {}
        self._cartId = None
        self._cartItems = []
        
        # Don't delete auth file - instead rewrite it with just credentials
        if saved_email or saved_password:
            ensure_config_dir()
            data = {}
            if saved_email:
                data['saved_email'] = saved_email.encode('utf-8').encode('base64').strip()
            if saved_password:
                data['saved_password'] = saved_password.encode('utf-8').encode('base64').strip()
            try:
                with open(AUTH_FILE, 'w') as f:
                    json.dump(data, f)
                print u"Cleared auth but kept credentials"
            except Exception as e:
                print u"Failed to save credentials during clear: %s" % e
        else:
            # No credentials to keep, delete the file
            if os.path.exists(AUTH_FILE):
                try:
                    os.remove(AUTH_FILE)
                except:
                    pass
    
    # ==================== Login Methods ====================
    
    @Slot(unicode, unicode)
    def login(self, email, password):
        """Login with email and password using Resource Owner Password Grant"""
        print u"=== LOGIN ATTEMPT (Password Grant) ==="
        print u"Email: %s" % email
        print u"Client ID: %s" % CLIENT_ID
        
        if not email or not password:
            self.loginFailed.emit(u"E-Mail und Passwort erforderlich")
            return
        
        data = "grant_type=password"
        data += "&client_id=%s" % CLIENT_ID
        data += "&username=%s" % urllib.quote(email.encode('utf-8'), safe='')
        data += "&password=%s" % urllib.quote(password.encode('utf-8'), safe='')
        data += "&scope=openid%20offline_access"
        
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json',
            'User-Agent': 'OeBBTickets/5.147.0 (Android)',
            'X-App-Domain': 'oebb'
        }
        
        print u"URL: %s" % TOKEN_URL
        
        def onResult(result):
            print u"=== LOGIN RESPONSE ==="
            print u"Raw result (first 500 chars): %s" % result[:500]
            
            try:
                if result.startswith('{"error"') or '"error"' in result[:100]:
                    try:
                        response = json.loads(result)
                        if 'error' in response:
                            error = response.get('error_description', response.get('error', 'Login fehlgeschlagen'))
                            error_code = response.get('error', '')
                            
                            if 'HTTP 403' in error or '403' in error_code:
                                self.loginFailed.emit(u"Direkter Login blockiert - Browser-Login verwenden")
                                return
                            
                            if error_code == 'unauthorized_client' or 'direct access grants' in error.lower():
                                self.loginFailed.emit(u"Bitte Browser-Login verwenden")
                            elif error_code == 'invalid_grant':
                                self.loginFailed.emit(u"Ungültige Anmeldedaten")
                            else:
                                self.loginFailed.emit(unicode(error))
                            return
                    except:
                        pass
                
                response = json.loads(result)
                
                if 'error' in response:
                    error = response.get('error_description', response.get('error', 'Login fehlgeschlagen'))
                    self.loginFailed.emit(unicode(error))
                    return
                
                if 'access_token' not in response:
                    self.loginFailed.emit(u"Ungültige Server-Antwort")
                    return
                
                self._accessToken = response.get('access_token')
                self._refreshToken = response.get('refresh_token')
                
                expires_in = response.get('expires_in', 3600)
                self._tokenExpiry = datetime.now() + timedelta(seconds=expires_in)
                
                print u"Login successful! Token expires in %d seconds" % expires_in
                
                self._fetchUserInfo()
                
            except ValueError as e:
                print u"JSON parse error: %s" % e
                self.loginFailed.emit(u"Server-Antwort ungültig")
            except Exception as e:
                print u"Login exception: %s" % e
                self.loginFailed.emit(u"Login-Fehler: %s" % e)
        
        self._startRequest("login", "POST", TOKEN_URL, data, headers, callback=onResult)
    
    
    @Slot(unicode, unicode)
    def loginWithCaptcha(self, email, password):
        """Login with automatic FriendlyCaptcha solving via auth_helper.py"""
        print u"=== CAPTCHA LOGIN ATTEMPT ==="
        print u"Email: %s" % email
        print u"Support ID: %s" % self._supportId
        
        if not email or not password:
            self.loginFailed.emit(u"E-Mail und Passwort erforderlich")
            return
        
        self._set_loading(True)
        self.captchaProgress.emit(u"Starte Login...", 0, 0)
        
        # Capture support_id to pass to auth_helper
        support_id = self._supportId
        
        def worker():
            try:
                import subprocess
                # Pass support_id to auth_helper for session binding
                cmd = [PYTHON311, AUTH_HELPER, "login", email, password, support_id]
                
                env = os.environ.copy()
                env['PYTHONIOENCODING'] = 'utf-8'
                
                proc = subprocess.Popen(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    env=env,
                    bufsize=1
                )
                
                import re
                for line in iter(proc.stderr.readline, b''):
                    line = line.decode('utf-8', 'ignore').strip()
                    if not line:
                        continue
                    
                    print u"Captcha progress: %s" % line
                    
                    m = re.search(r'\[(\d+)/(\d+)\]', line)
                    if m:
                        current = int(m.group(1))
                        total = int(m.group(2))
                        self.captchaProgress.emit(u"Löse Captcha %d/%d" % (current, total), current, total)
                    else:
                        self.captchaProgress.emit(line, 0, 0)
                
                proc.wait()
                
                out = proc.stdout.read().decode('utf-8', 'ignore').strip()
                print u"Captcha login result: %s" % out[:200]
                
                self._set_loading(False)
                
                if out:
                    result = json.loads(out)
                    if result.get('success'):
                        self._accessToken = result.get('access_token')
                        self._refreshToken = result.get('refresh_token')
                        # Update support_id from auth_helper result
                        if result.get('support_id'):
                            self._supportId = result.get('support_id')
                        self._tokenExpiry = datetime.now() + timedelta(minutes=15)
                        # Save credentials for re-login
                        self._savedEmail = email
                        self._savedPassword = password
                        self._saveAuth(email, password)
                        # CRITICAL: Initialize session on server before any API calls!
                        self._initSession()
                        self._fetchUserInfo()
                    else:
                        self.loginFailed.emit(result.get('error', u'Login fehlgeschlagen'))
                else:
                    self.loginFailed.emit(u"Keine Antwort vom Auth-Helper")
                    
            except Exception as e:
                print u"Captcha login error: %s" % e
                self._set_loading(False)
                self.loginFailed.emit(u"Fehler: %s" % e)
        
        thread = threading.Thread(target=worker)
        thread.daemon = True
        thread.start()

    @Slot(result=unicode)
    def getAuthUrl(self):
        """Get OAuth2 Authorization URL for WebView-based login"""
        url = AUTH_URL
        url += "?client_id=%s" % CLIENT_ID
        url += "&response_type=code"
        url += "&scope=openid%20email"
        url += "&redirect_uri=%s" % urllib.quote("https://shop.oebbtickets.at/de/account/login", safe='')
        
        if USE_PKCE:
            self._codeVerifier = generate_code_verifier()
            challenge = generate_code_challenge(self._codeVerifier)
            url += "&code_challenge=%s" % challenge
            url += "&code_challenge_method=S256"
        
        return url

    @Slot(unicode)
    def handleAuthCode(self, code):
        """Exchange authorization code for tokens"""
        print u"=== EXCHANGING AUTH CODE ==="
        
        data = "grant_type=authorization_code"
        data += "&client_id=%s" % CLIENT_ID
        data += "&code=%s" % code
        data += "&redirect_uri=%s" % urllib.quote("https://shop.oebbtickets.at/de/account/login", safe='')
        
        if USE_PKCE and self._codeVerifier:
            data += "&code_verifier=%s" % self._codeVerifier
        
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json'
        }
        
        def onResult(result):
            try:
                response = json.loads(result)
                if 'access_token' in response:
                    self._accessToken = response['access_token']
                    self._refreshToken = response.get('refresh_token')
                    expires_in = response.get('expires_in', 3600)
                    self._tokenExpiry = datetime.now() + timedelta(seconds=expires_in)
                    # CRITICAL: Initialize session on server before any API calls!
                    self._initSession()
                    self._fetchUserInfo()
                else:
                    error = response.get('error_description', response.get('error', 'Token-Austausch fehlgeschlagen'))
                    self.loginFailed.emit(unicode(error))
            except Exception as e:
                self.loginFailed.emit(u"Fehler: %s" % e)
        
        self._startRequest("token_exchange", "POST", TOKEN_URL, data, headers, callback=onResult)

    def _fetchUserInfo(self):
        """Extract user info from JWT token (no API call needed)"""
        try:
            if not self._accessToken:
                self._userInfo = {}
            else:
                # JWT format: header.payload.signature
                parts = self._accessToken.split('.')
                if len(parts) >= 2:
                    # Decode payload (base64url)
                    payload_b64 = str(parts[1])
                    # Add padding if needed
                    padding = 4 - len(payload_b64) % 4
                    if padding != 4:
                        payload_b64 += '=' * padding
                    # Replace URL-safe chars with standard base64
                    payload_b64 = payload_b64.replace('-', '+').replace('_', '/')
                    payload_json = base64.b64decode(payload_b64)
                    payload = json.loads(payload_json)
                    
                    # Extract user info from token
                    self._userInfo = {
                        'email': payload.get('email', ''),
                        'name': payload.get('name', ''),
                        'given_name': payload.get('given_name', ''),
                        'family_name': payload.get('family_name', ''),
                        'preferred_username': payload.get('preferred_username', ''),
                        'sub': payload.get('sub', ''),
                        'email_verified': payload.get('email_verified', False)
                    }
                    print u"User info from JWT: %s" % self._userInfo
                else:
                    self._userInfo = {}
        except Exception as e:
            print u"Failed to parse JWT: %s" % e
            self._userInfo = {}
        
        self._saveAuth()
        self.loginStateChanged.emit()
        self.loginSuccess.emit()

    @Slot()
    def logout(self):
        """Logout user"""
        self._clearAuth()
        self.loginStateChanged.emit()

    # === N26 Fingerprint Management ===
    
    def _getN26FingerprintPath(self):
        """Get path to N26 fingerprint file"""
        return os.path.expanduser("~/.config/oebb-tickets/n26_fingerprint.txt")
    
    @Slot(result=unicode)
    def getN26Fingerprint(self):
        """Get current N26 fingerprint or empty string"""
        fp_file = self._getN26FingerprintPath()
        if os.path.exists(fp_file):
            try:
                with open(fp_file, 'r') as f:
                    fp = f.read().strip()
                if len(fp) == 32:
                    return fp
            except:
                pass
        return u""
    
    @Slot(result=bool)
    def hasN26Fingerprint(self):
        """Check if N26 fingerprint is configured"""
        return len(self.getN26Fingerprint()) == 32
    
    @Slot(unicode, result=bool)
    def setN26Fingerprint(self, fingerprint):
        """Save N26 fingerprint to persistent storage"""
        fingerprint = fingerprint.strip().lower()
        
        # Validate: must be 32 hex characters
        if len(fingerprint) != 32:
            print u"Invalid fingerprint length: %d (must be 32)" % len(fingerprint)
            return False
        
        import re
        if not re.match(r'^[a-f0-9]{32}$', fingerprint):
            print u"Invalid fingerprint format (must be hex)"
            return False
        
        fp_file = self._getN26FingerprintPath()
        try:
            # Ensure directory exists
            fp_dir = os.path.dirname(fp_file)
            if not os.path.exists(fp_dir):
                os.makedirs(fp_dir)
            
            with open(fp_file, 'w') as f:
                f.write(fingerprint)
            print u"N26 fingerprint saved: %s..." % fingerprint[:16]
            return True
        except Exception as e:
            print u"Failed to save fingerprint: %s" % str(e)
            return False
    
    @Slot(result=bool)
    def clearN26Fingerprint(self):
        """Delete N26 fingerprint"""
        fp_file = self._getN26FingerprintPath()
        try:
            if os.path.exists(fp_file):
                os.remove(fp_file)
                print u"N26 fingerprint deleted"
            return True
        except Exception as e:
            print u"Failed to delete fingerprint: %s" % str(e)
            return False

    @Slot()
    def refreshTokenIfNeeded(self):
        """Refresh token if expired or about to expire"""
        if not self._refreshToken:
            return
        
        if self._tokenExpiry and datetime.now() < self._tokenExpiry - timedelta(minutes=5):
            return
        
        print u"=== REFRESHING TOKEN ==="
        
        # HAR shows refresh uses form-urlencoded
        data = "grant_type=refresh_token"
        data += "&client_id=%s" % CLIENT_ID
        data += "&refresh_token=%s" % self._refreshToken
        
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json',
            'Channel': 'inet',
            'Lang': 'de',
            'x-ts-supportid': self._supportId,
            'ClientId': WEB_CLIENT_ID,
            'clientversion': self._clientVersion or CLIENT_VERSION
        }
        
        def onResult(result):
            try:
                response = json.loads(result)
                if 'access_token' in response:
                    self._accessToken = response['access_token']
                    self._refreshToken = response.get('refresh_token', self._refreshToken)
                    expires_in = response.get('expires_in', 3600)
                    self._tokenExpiry = datetime.now() + timedelta(seconds=expires_in)
                    self._saveAuth()
                    print u"Token refreshed successfully"
                else:
                    print u"Token refresh failed"
                    self.logout()
            except Exception as e:
                print u"Token refresh error: %s" % e
        
        self._startRequest("refresh", "POST", TOKEN_URL, data, headers, callback=onResult)
    
    # ==================== Tickets ====================
    
    @Slot()
    def loadTickets(self):
        """Load user's tickets from Web API (both current and past journeys)"""
        if not self._accessToken:
            self.ticketsFound.emit(json.dumps([]))
            return
        
        self.refreshTokenIfNeeded()
        
        headers = self._getWebApiHeaders()
        
        # We need to query both CurrentJourneys AND PastJourneys
        all_tickets = []
        pending = [0]  # Use list to allow modification in nested function
        
        def combineResults():
            pending[0] -= 1
            if pending[0] <= 0:
                print u"Total tickets found: %d" % len(all_tickets)
                # Format tickets to ensure 'id' is set for QML
                formatted = []
                for t in all_tickets:
                    ticket = dict(t)
                    # Set id from infocardId if not present
                    if not ticket.get('id') and ticket.get('infocardId'):
                        ticket['id'] = ticket['infocardId']
                    # Also set orderPartId for PDF download compatibility
                    if not ticket.get('orderPartId') and ticket.get('infocardId'):
                        ticket['orderPartId'] = ticket['infocardId']
                    # Add PDF filename if already downloaded
                    ticket_id = ticket.get('infocardId') or ticket.get('id') or ''
                    pdf_filename = self._getPdfFilename(ticket_id)
                    if pdf_filename:
                        ticket['pdfFileName'] = pdf_filename
                    formatted.append(ticket)
                self.ticketsFound.emit(json.dumps(formatted))
        
        def onCurrentResult(result):
            try:
                print u"Current tickets result: %s" % result[:300]
                data = json.loads(result)
                # Check for session expired (HTTP 440)
                if data.get('error') == 'HTTP 440':
                    print u"Session expired - need to re-login"
                    self.sessionExpired.emit()
                    combineResults()
                    return
                if 'error' not in data:
                    infocards = data.get('t1Infocards', data.get('infocards', []))
                    if isinstance(data, list):
                        infocards = data
                    print u"Found %d current tickets" % len(infocards)
                    all_tickets.extend(infocards)
            except Exception as e:
                print u"Current tickets parse error: %s" % e
            combineResults()
        
        def onPastResult(result):
            try:
                print u"Past tickets result: %s" % result[:300]
                data = json.loads(result)
                # Check for session expired (HTTP 440)
                if data.get('error') == 'HTTP 440':
                    print u"Session expired - need to re-login"
                    self.sessionExpired.emit()
                    combineResults()
                    return
                if 'error' not in data:
                    infocards = data.get('t1Infocards', data.get('infocards', []))
                    if isinstance(data, list):
                        infocards = data
                    print u"Found %d past tickets" % len(infocards)
                    all_tickets.extend(infocards)
            except Exception as e:
                print u"Past tickets parse error: %s" % e
            combineResults()
        
        # Query both types
        pending[0] = 2
        
        url_current = TICKETS_URL + "?infocardType=CurrentJourneys&pageSize=20&pageNumber=0"
        url_past = TICKETS_URL + "?infocardType=PastJourneys&pageSize=20&pageNumber=0"
        
        self._startRequest("tickets_current", "GET", url_current, headers=headers, callback=onCurrentResult)
        self._startRequest("tickets_past", "GET", url_past, headers=headers, callback=onPastResult)
    
    @Slot(unicode)
    def loadTicketDetails(self, ticketId):
        """Load details for a specific ticket"""
        if not self._accessToken or not ticketId:
            self.ticketDetailsFound.emit(json.dumps({}))
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders()
        
        # Try to get ticket details
        url = TICKETS_URL + "/%s" % ticketId
        
        def onResult(result):
            try:
                data = json.loads(result)
                self.ticketDetailsFound.emit(json.dumps(data))
            except Exception as e:
                print u"Ticket details error: %s" % e
                self.ticketDetailsFound.emit(json.dumps({}))
        
        self._startRequest("ticket_details", "GET", url, headers=headers, callback=onResult)
    
    # ==================== PDF Download (NEW in 3.5.9) ====================
    
    @Slot(unicode)
    def loadTicketItem(self, orderPartId):
        """Load ticket item details (for PDF request)
        Endpoint: GET /api/order/v6/item?orderpartId={id}
        """
        if not self._accessToken or not orderPartId:
            self.ticketItemLoaded.emit(json.dumps({}))
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders()
        
        url = API_BASE + "/api/order/v6/item?orderpartId=" + str(orderPartId)
        
        def onResult(result):
            try:
                print u"Ticket item result: %s" % result[:500]
                data = json.loads(result)
                self.ticketItemLoaded.emit(json.dumps(data))
            except Exception as e:
                print u"Ticket item error: %s" % e
                self.ticketItemLoaded.emit(json.dumps({}))
        
        self._startRequest("ticket_item", "GET", url, headers=headers, callback=onResult)
    
    @Slot(unicode, unicode)
    def requestTicketPdf(self, orderPartId, email):
        """Request PDF ticket via email
        Endpoint: POST /api/order/v1/orderparts/acquisition/pdf
        Body: {"orderPartIds":["id"],"email":"email@example.com"}
        Response: 204 No Content (success) or error
        """
        if not self._accessToken:
            self.pdfFailed.emit(u"Nicht angemeldet")
            return
        
        if not orderPartId:
            self.pdfFailed.emit(u"Keine Ticket-ID")
            return
        
        if not email:
            self.pdfFailed.emit(u"Keine E-Mail-Adresse")
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(contentType='application/json')
        
        url = API_BASE + "/api/order/v1/orderparts/acquisition/pdf"
        
        # Build request body
        body = json.dumps({
            "orderPartIds": [str(orderPartId)],
            "email": str(email)
        })
        
        def onResult(result):
            try:
                print u"PDF request result: %s" % result[:200]
                # 204 No Content means success (empty result)
                if not result or result.strip() == '':
                    self.pdfRequested.emit(u"PDF wird an %s gesendet" % email)
                else:
                    # Check for error in response
                    data = json.loads(result)
                    if 'error' in data:
                        self.pdfFailed.emit(data['error'].get('message', u'Fehler beim PDF-Versand'))
                    else:
                        self.pdfRequested.emit(u"PDF wird an %s gesendet" % email)
            except Exception as e:
                # Empty response or parse error usually means success
                if not result or result.strip() == '':
                    self.pdfRequested.emit(u"PDF wird an %s gesendet" % email)
                else:
                    print u"PDF request error: %s" % e
                    self.pdfFailed.emit(unicode(e))
        
        self._startRequest("pdf_request", "POST", url, body, headers=headers, callback=onResult)
    
    @Slot(unicode)
    def downloadTicketPdf(self, orderPartId):
        """Download PDF ticket directly to device storage
        Endpoint: GET /api/order/v2/pdf?orderPartIds[]=<uuid>
        Response: {"filename": "OEBBTicket-xxx.pdf", "content": "<base64>"}
        Saves to: /home/user/MyDocs/Downloads/
        """
        if not self._accessToken:
            self.pdfFailed.emit(u"Nicht angemeldet")
            return
        
        if not orderPartId:
            self.pdfFailed.emit(u"Keine Ticket-ID")
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders()
        
        # Build URL with orderPartIds[] parameter
        url = API_BASE + "/api/order/v2/pdf?orderPartIds[]=" + str(orderPartId)
        
        print u"=== PDF DOWNLOAD ==="
        print u"URL: %s" % url
        
        def onResult(result):
            try:
                print u"PDF download result length: %d" % len(result)
                data = json.loads(result)
                
                if 'error' in data:
                    self.pdfFailed.emit(data.get('error', u'Fehler beim PDF-Download'))
                    return
                
                filename = data.get('filename', 'OEBBTicket.pdf')
                content_b64 = data.get('content', '')
                
                if not content_b64:
                    self.pdfFailed.emit(u"Keine PDF-Daten erhalten")
                    return
                
                # Decode base64 to binary PDF
                try:
                    pdf_data = base64.b64decode(content_b64)
                except Exception as e:
                    print u"Base64 decode error: %s" % e
                    self.pdfFailed.emit(u"PDF-Dekodierung fehlgeschlagen")
                    return
                
                # Save to Downloads folder
                downloads_dir = "/home/user/MyDocs/Downloads"
                if not os.path.exists(downloads_dir):
                    try:
                        os.makedirs(downloads_dir)
                    except Exception as e:
                        print u"Cannot create downloads dir: %s" % e
                        downloads_dir = os.path.expanduser("~")
                
                filepath = os.path.join(downloads_dir, filename)
                
                # Avoid overwriting - add number if file exists
                if os.path.exists(filepath):
                    base, ext = os.path.splitext(filename)
                    counter = 1
                    while os.path.exists(filepath):
                        filepath = os.path.join(downloads_dir, "%s_%d%s" % (base, counter, ext))
                        counter += 1
                
                # Write PDF file
                try:
                    with open(filepath, 'wb') as f:
                        f.write(pdf_data)
                    print u"PDF saved to: %s" % filepath
                    # Track this download
                    self._markPdfDownloaded(str(orderPartId), os.path.basename(filepath))
                    self.pdfRequested.emit(u"PDF gespeichert: %s" % os.path.basename(filepath))
                    # Reload tickets to update the list with PDF info
                    self.loadTickets()
                except Exception as e:
                    print u"PDF write error: %s" % e
                    self.pdfFailed.emit(u"Speichern fehlgeschlagen: %s" % e)
                    
            except Exception as e:
                print u"PDF download error: %s" % e
                import traceback
                traceback.print_exc()
                self.pdfFailed.emit(u"Download-Fehler: %s" % e)
        
        self._startRequest("pdf_download", "GET", url, headers=headers, callback=onResult)
    
    @Slot()
    def loadPassengers(self):
        """Load user's passengers/travelers from Web API"""
        if not self._accessToken:
            self.passengersFound.emit(json.dumps([]))
            return
        
        self.refreshTokenIfNeeded()
        
        headers = self._getWebApiHeaders()
        
        def onResult(result):
            try:
                print u"Passengers result: %s" % result[:500]
                data = json.loads(result)
                if 'error' in data:
                    print u"Passengers error: %s" % data.get('error')
                    self.passengersFound.emit(json.dumps([]))
                else:
                    passengers = data.get('passengers', [])
                    # Store passengers for later use in timetable requests
                    self._passengers = passengers
                    print u"Found %d passengers" % len(passengers)
                    
                    # DEBUG: Print cards for each passenger
                    for p in passengers:
                        p_name = p.get('firstName', '') + ' ' + p.get('lastName', '')
                        p_cards = p.get('cards', [])
                        print u"  Passenger %s: %d cards" % (p_name, len(p_cards))
                        for c in p_cards:
                            print u"    Card: %s (id=%s)" % (c.get('name', 'unknown'), c.get('cardId', c.get('id', '?')))
                    
                    # Format for QML display
                    formatted = []
                    for p in passengers:
                        # Calculate age from birthDate
                        age = 30  # default
                        if p.get('birthDate'):
                            try:
                                bd = datetime.strptime(p['birthDate'], '%Y-%m-%d')
                                today = datetime.now()
                                age = today.year - bd.year - ((today.month, today.day) < (bd.month, bd.day))
                            except:
                                pass
                        
                        # Find all cards (discount and regular)
                        cards = []
                        for c in p.get('cards', []):
                            cards.append(c.get('name', 'Karte'))
                        
                        formatted.append({
                            'id': p.get('id', 0),
                            'kdbId': p.get('kdbId', 0),
                            'firstName': p.get('firstName', ''),
                            'lastName': p.get('lastName', ''),
                            'type': p.get('type', 'ADULT'),
                            'me': p.get('me', False),
                            'age': age,
                            'birthDate': p.get('birthDate', ''),
                            'discountCards': cards,  # Renamed for QML compatibility
                            'fullData': p  # Store full data for timetable request
                        })
                    
                    self.passengersFound.emit(json.dumps(formatted))
            except Exception as e:
                print u"Passengers parse error: %s" % e
                self.passengersFound.emit(json.dumps([]))
        
        self._startRequest("passengers", "GET", PASSENGERS_URL, headers=headers, callback=onResult)
    
    # ==================== Shopping Cart (NEW from HAR) ====================
    
    @Slot()
    def loadCart(self):
        """Load shopping cart contents"""
        if not self._accessToken:
            self.cartUpdated.emit(json.dumps({"orderItems": []}))
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders()
        
        # From HAR: /api/shoppingcart/v1?error=false&cancel=false
        url = SHOPPING_CART_URL + "?error=false&cancel=false"
        
        def onResult(result):
            try:
                print u"Cart result: %s" % result[:500]
                data = json.loads(result)
                if 'id' in data:
                    self._cartId = data['id']
                self._cartItems = data.get('orderItems', [])
                self.cartUpdated.emit(json.dumps(data))
            except Exception as e:
                print u"Cart parse error: %s" % e
                self.cartUpdated.emit(json.dumps({"orderItems": []}))
        
        self._startRequest("cart", "GET", url, headers=headers, callback=onResult)
    
    @Slot(unicode, unicode, unicode)
    def addToCart(self, connectionId, offerId, passengerJson):
        """Add offer to shopping cart"""
        if not self._accessToken:
            self.errorOccurred.emit(u"Bitte zuerst anmelden")
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(contentType='application/json')
        
        try:
            passengers = json.loads(passengerJson) if passengerJson else []
        except:
            passengers = []
        
        # From HAR: Shopping cart add request structure
        request_data = {
            "selection": {
                "connectionId": connectionId,
                "offerSections": [{
                    "class": "2",
                    "id": "0",
                    "offerId": offerId,
                    "options": []
                }]
            },
            "passengers": passengers if passengers else [{
                "me": True,
                "remembered": True,
                "markedForDeath": False,
                "challengedFlags": {
                    "hasHandicappedPass": False,
                    "hasAssistanceDog": False,
                    "hasWheelchair": False,
                    "hasAttendant": False
                },
                "cards": []
            }]
        }
        
        def onResult(result):
            try:
                print u"Add to cart result: %s" % result[:300]
                data = json.loads(result)
                if 'error' in data:
                    self.errorOccurred.emit(u"Fehler: %s" % data.get('error'))
                else:
                    # Reload cart after adding
                    self.loadCart()
            except Exception as e:
                print u"Add to cart error: %s" % e
                self.errorOccurred.emit(u"Fehler beim Hinzufügen")
        
        self._startRequest("add_cart", "POST", SHOPPING_CART_ADD_URL, 
                          json.dumps(request_data), headers=headers, callback=onResult)
    
    @Slot(unicode)
    def removeFromCart(self, itemId):
        """Remove item from shopping cart"""
        if not self._accessToken or not itemId:
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders()
        
        # From HAR: DELETE with itemId and lastChangeDate
        now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
        url = SHOPPING_CART_DELETE_URL + "?itemId=%s&lastChangeDate=%s" % (itemId, now)
        
        def onResult(result):
            print u"Remove from cart result: %s" % result[:200]
            self.loadCart()
        
        self._startRequest("remove_cart", "DELETE", url, headers=headers, callback=onResult)
    
    @Slot(unicode, unicode)
    def getOffers(self, connectionId, selectedPassengerIdsJson=""):
        """Get all available offers for a connection (for selection dialog)
        
        Args:
            connectionId: The connection to get offers for
            selectedPassengerIdsJson: JSON array of passenger IDs
        
        Emits offersLoaded signal with JSON of all offers
        """
        selectedPassengerIds = None
        if selectedPassengerIdsJson and selectedPassengerIdsJson.strip():
            try:
                selectedPassengerIds = json.loads(selectedPassengerIdsJson)
            except:
                pass
        
        if not self._accessToken:
            self.errorOccurred.emit(u"Bitte zuerst anmelden")
            return
        
        if not connectionId:
            self.errorOccurred.emit(u"Keine Verbindung ausgewählt")
            return
        
        print u"=== GET OFFERS ==="
        print u"ConnectionId: %s" % connectionId[:50]
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(contentType='application/json')
        
        # Build passengers array from stored passengers
        passengers = self._buildPassengersArray(selectedPassengerIds)
        
        offers_request = {
            "selection": {
                "connectionId": connectionId,
                "offerSections": []
            },
            "ignoreSelectionOnOfferChange": False,
            "passengers": passengers
        }
        
        def onOffersResult(result):
            try:
                print u"Offers result: %s" % result[:800]
                data = json.loads(result)
                
                if 'error' in data:
                    self.errorOccurred.emit(u"Fehler: %s" % data.get('error', {}).get('message', 'Unbekannt'))
                    return
                
                # Check connection sections for intermediate stops
                connection = data.get('connection', {})
                conn_sections = connection.get('sections', [])
                if conn_sections:
                    print u"  Offers connection has %d sections" % len(conn_sections)
                    for i, cs in enumerate(conn_sections[:2]):  # Only first 2 for debug
                        print u"    Section %d keys: %s" % (i, cs.keys())
                        if 'passingPoints' in cs or 'stops' in cs or 'passlist' in cs:
                            pp = cs.get('passingPoints', cs.get('stops', cs.get('passlist', [])))
                            print u"    Section %d has %d stops!" % (i, len(pp))
                
                offer_sections = data.get('offerSections', [])
                if not offer_sections:
                    self.offersLoaded.emit(json.dumps([]))
                    return
                
                # Collect all offers from all sections and classes
                all_offers = []
                for section_idx, section in enumerate(offer_sections):
                    section_from = section.get('from', {}).get('name', '')
                    section_to = section.get('to', {}).get('name', '')
                    ride_names = section.get('rideNames', [])
                    ride_str = ', '.join(ride_names) if ride_names else ''
                    has_integrated_reservation = section.get('hasIntegratedReservation', False)
                    
                    # Build compartment lookup from media.compartments
                    # This contains the full info (name, accommodationType) for each compartment ID
                    media_compartment_map = {}
                    media = section.get('media', {})
                    for mc in media.get('compartments', []):
                        mc_id = str(mc.get('id', ''))
                        mc_name = mc.get('name', {})
                        mc_name_str = mc_name.get('de', '') if isinstance(mc_name, dict) else str(mc_name)
                        mc_type = mc.get('accommodationType', '')
                        media_compartment_map[mc_id] = {
                            'name': mc_name_str,
                            'accommodationType': mc_type,
                            'infotexts': mc.get('infotexts', [])
                        }
                    if media_compartment_map:
                        print u"  Section %d media compartments: %s" % (section_idx, list(media_compartment_map.keys()))
                    
                    for tc in section.get('travelClasses', []):
                        tc_class = tc.get('class', '2')
                        class_name = '1. Klasse' if tc_class == '1' else '2. Klasse'
                        
                        for offer in tc.get('offers', []):
                            offer_id = offer.get('id', '')
                            
                            # Extract price - can be a dict or a number
                            price_data = offer.get('price', 0)
                            if isinstance(price_data, dict):
                                price = price_data.get('amount', price_data.get('total', 0))
                            else:
                                price = price_data
                            
                            # If offer price is 0, try to get from compartment (Nightjet)
                            if not price:
                                reservation = offer.get('reservation', {})
                                compartments = reservation.get('compartments', [])
                                if compartments:
                                    # Use first compartment's price
                                    comp_price = compartments[0].get('price', 0)
                                    if comp_price:
                                        price = comp_price
                                        print u"  Offer %s using compartment price: %.2f" % (offer_id[:20], price)
                            
                            # If still no price, try products
                            if not price:
                                products_list = offer.get('products', [])
                                print u"  Offer %s products count: %d" % (offer_id[:20], len(products_list))
                                for pi, prod in enumerate(products_list):
                                    print u"    Product %d keys: %s" % (pi, prod.keys())
                                    prod_price = prod.get('price', 0)
                                    print u"    Product %d price value: %s (type: %s)" % (pi, prod_price, type(prod_price).__name__)
                                    if prod_price:
                                        price = prod_price
                                        print u"  Offer %s using product price: %.2f" % (offer_id[:20], price)
                                        break
                                    # Try passengerPriceDetails
                                    ppd = prod.get('passengerPriceDetails', [])
                                    if ppd:
                                        print u"    Product %d has %d passengerPriceDetails" % (pi, len(ppd))
                                        for pdi, pd in enumerate(ppd):
                                            print u"      PPD %d: %s" % (pdi, pd)
                                            pd_price = pd.get('price', 0)
                                            if pd_price:
                                                price = pd_price
                                                print u"  Offer %s using passengerPriceDetails: %.2f" % (offer_id[:20], price)
                                                break
                                        if price:
                                            break
                            
                            # Debug price
                            print u"  Offer %s price: %s -> %.2f" % (offer_id[:20], type(price_data).__name__, price if price else 0)
                            
                            flexibility = offer.get('flexibility', {})
                            flex_name = flexibility.get('de', '') if isinstance(flexibility, dict) else str(flexibility)
                            
                            # Debug: show all offer keys
                            print u"  Offer keys: %s" % offer.keys()
                            
                            # Get product names and scopes
                            products = offer.get('products', [])
                            product_names = []
                            for prod in products:
                                pname = prod.get('name', {})
                                if isinstance(pname, dict):
                                    product_names.append(pname.get('de', ''))
                                else:
                                    product_names.append(str(pname))
                                # Debug scopes
                                prod_scopes = prod.get('scopes', [])
                                if prod_scopes:
                                    for psc in prod_scopes:
                                        print u"    Product scope: %s → %s" % (psc.get('from', {}).get('name', '?'), psc.get('to', {}).get('name', '?'))
                            
                            # Check for compartment options (Nightjet)
                            compartments = []
                            reservation = offer.get('reservation')
                            marketing_cat = offer.get('marketingCategory', {})
                            if reservation:
                                print u"  Offer %s has reservation: %s" % (offer_id[:20], json.dumps(reservation)[:300])
                                print u"  MarketingCategory: %s" % json.dumps(marketing_cat)[:200] if marketing_cat else ""
                                for comp in reservation.get('compartments', []):
                                    print u"    Compartment keys: %s" % comp.keys()
                                    
                                    comp_id = str(comp.get('id', ''))
                                    comp_name_str = ""
                                    accommodation_type = ""
                                    
                                    # FIRST: Try media.compartments lookup (most reliable)
                                    if comp_id in media_compartment_map:
                                        media_info = media_compartment_map[comp_id]
                                        comp_name_str = media_info.get('name', '')
                                        accommodation_type = media_info.get('accommodationType', '')
                                        print u"    ID %s from media: %s (%s)" % (comp_id, comp_name_str, accommodation_type)
                                    
                                    # SECOND: Static ID-based mapping (fallback)
                                    if not comp_name_str:
                                        id_map = {
                                            # Sitzwagen (SEAT)
                                            '5000134': ('Vorrangplatz', 'SEAT'),
                                            '5000120': ('Sitzplatz Comfort 2. Kl.', 'SEAT'),
                                            '5000121': ('Sitzwagen', 'SEAT'),
                                            '5000133': ('Sitzwagen', 'SEAT'),
                                            '5000010': ('Großraumwagen Ruhezone', 'SEAT'),
                                            '3756270': ('Großraumwagen 2. Kl.', 'SEAT'),
                                            # Liegewagen (COUCHETTE)
                                            '5000111': ('Mini Cabin', 'COUCHETTE'),
                                            '5000112': ('Liegewagen', 'COUCHETTE'),
                                            '5000113': ('Liegewagen Comfort', 'COUCHETTE'),
                                            '5000127': ('Liegewagen 4er', 'COUCHETTE'),
                                            # Schlafwagen (SLEEPER)
                                            '5000170': ('Schlafwagen', 'SLEEPER'),
                                            '5000171': ('Schlafwagen', 'SLEEPER'),
                                            '5000172': ('Schlafwagen', 'SLEEPER'),
                                            '5000174': ('Schlafwagen', 'SLEEPER'),
                                            '5000115': ('Schlafwagen Deluxe', 'SLEEPER'),
                                            '5000114': ('Schlafwagen Suite', 'SLEEPER'),
                                            '5000135': ('Schlafwagen Single', 'SLEEPER'),
                                            '5000136': ('Schlafwagen Double', 'SLEEPER'),
                                        }
                                        
                                        if comp_id in id_map:
                                            comp_name_str, accommodation_type = id_map[comp_id]
                                            print u"    ID %s from id_map: %s (%s)" % (comp_id, comp_name_str, accommodation_type)
                                    
                                    # THIRD: Try reservation-details API (expensive)
                                    if not comp_name_str:
                                        print u"    Unknown compartment ID %s - fetching from API..." % comp_id
                                        details = self._fetchReservationDetailsSync(
                                            connectionId, offer_id, str(section_idx), comp_id
                                        )
                                        if details:
                                            comp_name_str = details.get('title_de', '') or details.get('title_en', '')
                                            accommodation_type = details.get('accommodationType', '')
                                            print u"    API returned: %s (%s)" % (comp_name_str, accommodation_type)
                                    
                                    # Fallback: use ID if still no name
                                    if not comp_name_str:
                                        comp_name_str = "Abteil %s" % comp_id
                                    
                                    # Get scope info
                                    scopes = comp.get('scopes', [])
                                    scope_info = ""
                                    if scopes:
                                        scope = scopes[0]
                                        scope_from = scope.get('stationFrom', '')
                                        scope_to = scope.get('stationTo', '')
                                        if scope_from and scope_to:
                                            scope_info = u"%s → %s" % (scope_from, scope_to)
                                    
                                    # Check for gender designation
                                    variations = comp.get('variations', [])
                                    has_gender = any(v.get('genderDesignation') for v in variations)
                                    
                                    compartments.append({
                                        'id': comp_id,
                                        'name': comp_name_str,
                                        'price': comp.get('price', 0),
                                        'scopeInfo': scope_info,
                                        'accommodationType': accommodation_type,
                                        'hasGenderSeparation': has_gender
                                    })
                                    print u"    Compartment ID=%s: %s (€%.2f) type=%s %s" % (
                                        comp_id, comp_name_str, comp.get('price', 0), 
                                        accommodation_type, scope_info)
                            
                            # Also check for sleeper/couchette in products
                            for prod in products:
                                prod_type = prod.get('type', '')
                                if prod_type in ['SLEEPER', 'COUCHETTE', 'NIGHTJET']:
                                    print u"  Found sleeper product: %s" % prod
                            
                            # Extract optional reservation info (for non-nightjet trains)
                            optional_reservation = None
                            if reservation and not has_integrated_reservation:
                                # This is an optional reservation (normal trains)
                                res_price = reservation.get('price', 0)
                                res_selected = reservation.get('isSelected', False)
                                res_changeable = reservation.get('isChangeable', True)
                                res_details = reservation.get('details', [])
                                
                                # Get scope from details
                                res_scope = ""
                                if res_details:
                                    for rd in res_details:
                                        for scope in rd.get('scopes', []):
                                            res_scope = u"%s → %s" % (scope.get('stationFrom', ''), scope.get('stationTo', ''))
                                            break
                                        if res_scope:
                                            break
                                
                                optional_reservation = {
                                    'price': res_price,
                                    'isSelected': res_selected,
                                    'isChangeable': res_changeable,
                                    'scope': res_scope
                                }
                                print u"  Offer %s has optional reservation: €%.2f, selected=%s, scope=%s" % (
                                    offer_id[:20], res_price, res_selected, res_scope)
                            
                            all_offers.append({
                                'id': offer_id,
                                'sectionIdx': section_idx,
                                'sectionFrom': section_from,
                                'sectionTo': section_to,
                                'rideName': ride_str,
                                'class': tc_class,
                                'className': class_name,
                                'flexibility': flex_name,
                                'price': price,
                                'products': product_names,
                                'compartments': compartments,
                                'hasIntegratedReservation': has_integrated_reservation,
                                'optionalReservation': optional_reservation,
                                'isSelected': offer.get('isSelected', False)
                            })
                            print u"  -> Added offer %s: %s → %s, %d compartments, price=%.2f" % (offer_id[:20], section_from, section_to, len(compartments), price)
                
                print u"Found %d offers total" % len(all_offers)
                
                # Also check for reservation-only offers (separate purchase without ticket)
                for section_idx, section in enumerate(offer_sections):
                    res_only = section.get('reservationOnly')
                    if res_only and res_only.get('isSelected'):
                        section_id = section.get('id', str(section_idx))
                        # Get ride info from notes
                        notes = res_only.get('notes', [])
                        scope_text = ''
                        for note in notes:
                            if note.get('type') == 'REDUCED_SCOPE':
                                lines = note.get('textLines', [])
                                if lines:
                                    scope_text = lines[0].get('de', '')
                        
                        for ro_offer in res_only.get('offers', []):
                            ro_class = ro_offer.get('class', '2')
                            if ro_class == '1':
                                class_name = '1. Klasse'
                            elif ro_class == 'B':
                                class_name = 'Business'
                            else:
                                class_name = '2. Klasse'
                            
                            ro_price = ro_offer.get('price', 0)
                            
                            # Get properties
                            props = ro_offer.get('properties', [])
                            validity_text = ''
                            rides_text = ''
                            for prop in props:
                                ptype = prop.get('type', '')
                                ptxt = prop.get('text', {}).get('de', '')
                                if ptype == 'VALIDITY':
                                    validity_text = ptxt
                                elif ptype == 'RIDES':
                                    rides_text = ptxt
                            
                            all_offers.append({
                                'id': ro_offer.get('id', ''),
                                'sectionIdx': int(section_id) if section_id.isdigit() else section_idx,
                                'sectionFrom': '',
                                'sectionTo': '',
                                'rideName': rides_text or scope_text,
                                'class': ro_class,
                                'className': class_name,
                                'flexibility': u'Nicht stornierbar',
                                'price': ro_price,
                                'products': [u'Nur Reservierung'],
                                'compartments': [],
                                'hasIntegratedReservation': False,
                                'optionalReservation': None,
                                'isSelected': ro_offer.get('isSelected', False),
                                'reservationOnly': True,
                                'validityText': validity_text
                            })
                            print u"  -> Added reservation-only offer %s: %s €%.2f" % (ro_offer.get('id', '')[:20], class_name, ro_price)
                
                print u"Total offers (incl. reservation-only): %d" % len(all_offers)
                self.offersLoaded.emit(json.dumps(all_offers))
                
            except Exception as e:
                print u"getOffers error: %s" % e
                import traceback
                traceback.print_exc()
                self.errorOccurred.emit(u"Fehler beim Laden der Angebote")
        
        self._startRequest("get_all_offers", "POST", OFFERS_URL,
                          json.dumps(offers_request), headers=headers, callback=onOffersResult)
    
    def _buildPassengersArray(self, selectedPassengerIds=None):
        """Build passengers array for API requests"""
        passengers = []
        print u"=== BUILD PASSENGERS ARRAY ==="
        print u"Selected IDs: %s" % selectedPassengerIds
        print u"Stored passengers: %d" % len(self._passengers) if self._passengers else 0
        
        # Treat empty list same as None (select only "me")
        if selectedPassengerIds is not None and len(selectedPassengerIds) == 0:
            selectedPassengerIds = None
            print u"  Empty list -> treating as None (select 'me' only)"
        
        if self._passengers:
            for p in self._passengers:
                p_id = p.get('id')
                is_me = p.get('me', False)
                p_name = p.get('firstName', '') + ' ' + p.get('lastName', '')
                
                if selectedPassengerIds is None:
                    if not is_me:
                        continue
                else:
                    if p_id not in selectedPassengerIds and str(p_id) not in [str(x) for x in selectedPassengerIds]:
                        continue
                
                age = 30
                if p.get('birthDate'):
                    try:
                        bd = datetime.strptime(p['birthDate'], '%Y-%m-%d')
                        today = datetime.now()
                        age = today.year - bd.year - ((today.month, today.day) < (bd.month, bd.day))
                    except:
                        pass
                
                cards = []
                raw_cards = p.get('cards', [])
                print u"  Passenger %s (id=%s): %d raw cards" % (p_name, p_id, len(raw_cards))
                for c in raw_cards:
                    card = dict(c)
                    card['isSelected'] = False
                    card['isValidated'] = False
                    if 'person' not in card:
                        card['person'] = {}
                    cards.append(card)
                    print u"    Added card: %s" % c.get('name', c.get('cardId', 'unknown'))
                
                passengers.append({
                    "me": is_me,
                    "remembered": True,
                    "markedForDeath": False,
                    "challengedFlags": p.get('challengedFlags', {
                        "hasHandicappedPass": False,
                        "hasAssistanceDog": False,
                        "hasWheelchair": False,
                        "hasAttendant": False
                    }),
                    "cards": cards,
                    "relations": p.get('relations', []),
                    "age": age,
                    "id": p.get('id', 1),
                    "type": p.get('type', 'ADULT'),
                    "kdbId": p.get('kdbId', 0),
                    "firstName": p.get('firstName', ''),
                    "lastName": p.get('lastName', ''),
                    "colorId": p.get('colorId', 'person1'),
                    "birthDate": p.get('birthDate', ''),
                    "customAttributes": p.get('customAttributes', [])
                })
                print u"  Added passenger: %s with %d cards" % (p_name, len(cards))
        
        if not passengers:
            passengers = [{
                "me": True,
                "remembered": False,
                "markedForDeath": False,
                "challengedFlags": {
                    "hasHandicappedPass": False,
                    "hasAssistanceDog": False,
                    "hasWheelchair": False,
                    "hasAttendant": False
                },
                "cards": [],
                "age": 30,
                "id": 1,
                "type": "ADULT"
            }]
        
        return passengers
    
    @Slot(unicode, unicode, int, unicode)
    def buyTicket(self, connectionId, selectedPassengerIdsJson="", bikeCount=0, compartmentId=""):
        """Buy ticket: Get offers, extract offerId, add to cart (uses first/cheapest offer)
        
        Args:
            connectionId: The connection to buy
            selectedPassengerIdsJson: JSON array of passenger IDs (empty string = just me)
            bikeCount: Number of bike tickets to add
            compartmentId: Nightjet compartment ID for sleeper/couchette
        """
        selectedPassengerIds = None
        if selectedPassengerIdsJson and selectedPassengerIdsJson.strip():
            try:
                selectedPassengerIds = json.loads(selectedPassengerIdsJson)
            except:
                pass
        if not self._accessToken:
            self.errorOccurred.emit(u"Bitte zuerst anmelden")
            return
        
        if not connectionId:
            self.errorOccurred.emit(u"Keine Verbindung ausgewählt")
            return
        
        print u"=== BUY TICKET (auto-select) ==="
        print u"ConnectionId: %s" % connectionId[:50]
        print u"Bike count: %d" % bikeCount
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(contentType='application/json')
        passengers = self._buildPassengersArray(selectedPassengerIds)
        
        # Step 1: Get offers to extract offerId
        offers_request = {
            "selection": {
                "connectionId": connectionId,
                "offerSections": []
            },
            "ignoreSelectionOnOfferChange": False,
            "passengers": passengers
        }
        
        def onOffersResult(result):
            try:
                print u"Offers result: %s" % result[:800]
                data = json.loads(result)
                
                if 'error' in data:
                    self.purchaseFailed.emit(u"Fehler: %s" % data.get('error', {}).get('message', 'Unbekannt'))
                    return
                
                # Extract offerId from response
                offer_sections = data.get('offerSections', [])
                if not offer_sections:
                    self.purchaseFailed.emit(u"Keine Angebote verfügbar")
                    return
                
                # Get first offer from first section, second class
                travel_classes = offer_sections[0].get('travelClasses', [])
                offers = []
                for tc in travel_classes:
                    if tc.get('class') == '2':  # 2. Klasse
                        offers = tc.get('offers', [])
                        break
                
                if not offers:
                    # Try first available class
                    if travel_classes:
                        offers = travel_classes[0].get('offers', [])
                
                if not offers:
                    self.purchaseFailed.emit(u"Keine Tickets verfügbar")
                    return
                
                offer = offers[0]
                offer_id = offer.get('id', '')
                price = offer.get('price', 0)
                
                print u"Found offer: %s, Price: %.2f" % (offer_id[:30], price)
                
                # Check for bike tickets in products
                bike_offer_id = None
                if bikeCount > 0:
                    for product in offer.get('products', []):
                        name = product.get('name', {})
                        if isinstance(name, dict):
                            name_de = name.get('de', '')
                        else:
                            name_de = str(name)
                        if 'Fahrrad' in name_de or 'bike' in name_de.lower():
                            # Found bike product
                            print u"Found bike product in offer"
                            break
                
                # Check for compartment options (Nightjet)
                compartment_option = None
                if compartmentId and offer.get('reservation'):
                    compartments = offer.get('reservation', {}).get('compartments', [])
                    for comp in compartments:
                        if str(comp.get('id')) == str(compartmentId):
                            compartment_option = comp
                            print u"Selected compartment: %s" % compartmentId
                            break
                
                # Build offer sections
                offer_sections = [{
                    "class": "2",
                    "id": "0",
                    "offerId": offer_id,
                    "options": []
                }]
                
                # Add compartment selection if available
                if compartment_option:
                    offer_sections[0]["compartmentId"] = compartmentId
                
                # Generate cref timestamp
                import datetime
                cref_timestamp = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+01:00")
                
                # Step 2: Add to shopping cart
                cart_request = {
                    "selection": {
                        "connectionId": connectionId,
                        "offerSections": offer_sections
                    },
                    "passengers": passengers,
                    "cref": "oebb-header",
                    "crefCreatedAt": cref_timestamp
                }
                
                # Add bike count if requested (legacy - bikes now handled separately)
                if bikeCount > 0:
                    cart_request["bikeCount"] = bikeCount
                
                def onCartResult(cart_result):
                    try:
                        print u"Add to cart result: %s" % cart_result[:300]
                        cart_data = json.loads(cart_result) if cart_result.strip() else {}
                        
                        if 'error' in cart_data:
                            self.purchaseFailed.emit(u"Fehler: %s" % cart_data.get('error'))
                        else:
                            # Success! Reload cart and notify
                            self.loadCart()
                            self.purchaseSuccess.emit(u"Ticket zum Warenkorb hinzugefügt! Preis: € %.2f" % price)
                    except Exception as e:
                        print u"Cart add error: %s" % e
                        # Even empty response {} is success
                        self.loadCart()
                        self.purchaseSuccess.emit(u"Ticket zum Warenkorb hinzugefügt!")
                
                self._startRequest("add_to_cart", "POST", SHOPPING_CART_ADD_URL,
                                  json.dumps(cart_request), headers=headers, callback=onCartResult)
                
            except Exception as e:
                print u"Offers error: %s" % e
                import traceback
                traceback.print_exc()
                self.purchaseFailed.emit(u"Fehler beim Laden der Angebote")
        
        self._startRequest("get_offers", "POST", OFFERS_URL,
                          json.dumps(offers_request), headers=headers, callback=onOffersResult)
    
    @Slot(unicode, unicode, unicode, int, unicode, unicode, unicode, unicode, bool, unicode, unicode)
    def buyWithOffer(self, connectionId, offerId, selectedPassengerIdsJson="", bikeCount=0, travelClass="2", compartmentId="", allOffersJson="", phoneNumber="", wantReservation=False, reservationCompId="", reservationPrefId=""):
        """Buy ticket with a specific offer ID (from getOffers selection)
        
        Args:
            connectionId: The connection to buy
            offerId: The specific offer ID to purchase (for section with compartment)
            selectedPassengerIdsJson: JSON array of passenger IDs
            bikeCount: Number of bike tickets to add
            travelClass: Travel class (1 or 2)
            compartmentId: Nightjet compartment ID for sleeper/couchette
            allOffersJson: JSON array of all offers from getOffers (for multi-section)
            phoneNumber: Contact phone number for Nightjet bookings
            wantReservation: If True, add optional seat reservation
            reservationCompId: Selected compartment ID from reservation dialog
            reservationPrefId: Selected preference ID from reservation dialog
        """
        selectedPassengerIds = None
        if selectedPassengerIdsJson and selectedPassengerIdsJson.strip():
            try:
                selectedPassengerIds = json.loads(selectedPassengerIdsJson)
            except:
                pass
        
        # Parse all offers for multi-section handling
        allOffers = []
        if allOffersJson and allOffersJson.strip():
            try:
                allOffers = json.loads(allOffersJson)
            except:
                pass
        
        if not self._accessToken:
            self.errorOccurred.emit(u"Bitte zuerst anmelden")
            return
        
        if not connectionId or not offerId:
            self.errorOccurred.emit(u"Keine Verbindung oder Angebot ausgewählt")
            return
        
        print u"=== BUY WITH OFFER ==="
        print u"ConnectionId: %s" % connectionId[:50]
        print u"OfferId: %s" % offerId[:50]
        print u"Bike count: %d" % bikeCount
        print u"Class: %s" % travelClass
        if compartmentId:
            print u"CompartmentId: %s" % compartmentId
        print u"All offers count: %d" % len(allOffers)
        if phoneNumber:
            print u"Phone number: %s" % phoneNumber
        print u"Want reservation: %s" % wantReservation
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(contentType='application/json')
        passengers = self._buildPassengersArray(selectedPassengerIds)
        
        # Add phone number to first passenger for Nightjet (required for sleeper/couchette)
        if phoneNumber and compartmentId and passengers:
            passengers[0]["customAttributes"] = [{
                "id": "kfzPhoneNumber",
                "type": "phoneNumber",
                "label": {
                    "de": "Telefonnummer (z.B. +431234567)",
                    "en": "Phone number (e.g. +431234567)",
                    "it": "Numero telefonico (p.e. +431234567)"
                },
                "subtext": {
                    "de": u"Bitte geben Sie eine Telefonnummer (inkl. Ländervorwahl, z.B. +431234567) an, unter der wir Sie im Fall von Unregelmäßigkeiten kontaktieren können.",
                    "en": "Please specify a telephone number (incl. country code, e.g. +431234567), so we can contact you in case of any irregularities.",
                    "it": u"La preghiamo di inserire un numero telefonico (ivi incluso il prefisso nazionale, p.s. +431234567) su cui potremo contattarla in caso di irregolarità"
                },
                "value": phoneNumber
            }]
            print u"Added phone number to first passenger: %s" % phoneNumber
        
        # Build offer sections - need one per connection section
        # Group offers by sectionIdx and pick first available for each
        offer_sections = []
        
        if allOffers:
            # Group by sectionIdx
            sections_map = {}
            for offer in allOffers:
                sidx = offer.get('sectionIdx', 0)
                if sidx not in sections_map:
                    sections_map[sidx] = []
                sections_map[sidx].append(offer)
            
            print u"Found %d sections in offers" % len(sections_map)
            
            # Build offerSections for each connection section
            for sidx in sorted(sections_map.keys()):
                section_offers = sections_map[sidx]
                
                # Find the selected offer or first one with matching offerId
                selected_offer = None
                for o in section_offers:
                    if o.get('id') == offerId:
                        selected_offer = o
                        break
                
                # If not found in this section, use first offer of this section
                if not selected_offer:
                    # Find first non-zero price offer, or just first
                    for o in section_offers:
                        if o.get('price', 0) >= 0:
                            selected_offer = o
                            break
                    if not selected_offer:
                        selected_offer = section_offers[0]
                
                section_data = {
                    "class": travelClass,
                    "id": str(sidx),
                    "offerId": selected_offer.get('id', ''),
                    "options": []
                }
                
                # Check if this is a reservation-only purchase
                if selected_offer.get('reservationOnly') and selected_offer.get('id') == offerId:
                    ro_class = selected_offer.get('class', travelClass)
                    ro_obj = {
                        "isSelected": True,
                        "offerId": selected_offer.get('id', ''),
                        "class": ro_class
                    }
                    # Add compartment/preference if selected
                    if reservationCompId:
                        pref_position = "no_preference"
                        for rsec in self._lastReservationOptions or []:
                            for rcomp in rsec.get('compartments', []):
                                if rcomp.get('id') == reservationCompId:
                                    for rpref in rcomp.get('preferences', []):
                                        if rpref.get('id') == reservationPrefId:
                                            pref_position = rpref.get('position', 'no_preference')
                                            break
                                    break
                        ro_obj["sections"] = [{
                            "index": sidx,
                            "compartment": {
                                "id": int(reservationCompId),
                                "allocation": {
                                    "positionOfPlaces": pref_position,
                                    "necessary": False
                                }
                            }
                        }]
                        print u"Reservation-only with comp=%s pref=%s" % (reservationCompId, pref_position)
                    # Use reservationOnly instead of offerId
                    del section_data["offerId"]
                    section_data["reservationOnly"] = ro_obj
                    print u"Using reservationOnly section for offer %s" % selected_offer.get('id', '')[:20]
                
                # Check if this section has the compartment (Nightjet)
                elif selected_offer.get('id') == offerId and compartmentId:
                    # Nightjet reservation format from HAR - needs sections with allocation
                    # index must be the section index (sidx), not 0!
                    section_data["reservation"] = {
                        "isSelected": True,
                        "sections": [
                            {
                                "index": sidx,  # Must match section index!
                                "compartment": {
                                    "id": int(compartmentId),
                                    "allocation": {
                                        "positionOfPlaces": "no_preference",
                                        "necessary": False,
                                        "placeAlignment": "side_by_side"
                                    }
                                }
                            }
                        ],
                        "compartmentId": int(compartmentId)
                    }
                    print u"Added reservation to section %d: compartmentId=%s" % (sidx, compartmentId)
                # Check if user wants optional seat reservation (normal trains)
                elif selected_offer.get('id') == offerId and wantReservation:
                    opt_res = selected_offer.get('optionalReservation', {})
                    if opt_res:
                        res_obj = {"isSelected": True}
                        # Add seat preference if user selected one
                        if reservationCompId:
                            # Find the preference position
                            pref_position = "no_preference"
                            for rsec in self._lastReservationOptions or []:
                                for rcomp in rsec.get('compartments', []):
                                    if rcomp.get('id') == reservationCompId:
                                        for rpref in rcomp.get('preferences', []):
                                            if rpref.get('id') == reservationPrefId:
                                                pref_position = rpref.get('position', 'no_preference')
                                                break
                                        break
                            res_obj["sections"] = [{
                                "index": sidx,
                                "compartment": {
                                    "id": int(reservationCompId),
                                    "allocation": {
                                        "positionOfPlaces": pref_position,
                                        "necessary": False
                                    }
                                }
                            }]
                            print u"Added reservation with comp=%s pref=%s (%s) to section %d" % (
                                reservationCompId, reservationPrefId, pref_position, sidx)
                        else:
                            print u"Added optional seat reservation to section %d" % sidx
                        section_data["reservation"] = res_obj
                
                offer_sections.append(section_data)
                print u"Section %d: offer=%s price=%.2f" % (sidx, selected_offer.get('id', '')[:20], selected_offer.get('price', 0))
        else:
            # Fallback: single section (old behavior)
            section_data = {
                "class": travelClass,
                "id": "0",
                "offerId": offerId,
                "options": []
            }
            
            # Check if reservation-only mode
            if self._reservationOnlyMode:
                ro_class = travelClass
                # Find the offer to get its class
                for o in allOffers:
                    if o.get('id') == offerId and o.get('reservationOnly'):
                        ro_class = o.get('class', travelClass)
                        break
                del section_data["offerId"]
                ro_obj = {
                    "isSelected": True,
                    "offerId": offerId,
                    "class": ro_class
                }
                if reservationCompId:
                    pref_position = "no_preference"
                    for rsec in self._lastReservationOptions or []:
                        for rcomp in rsec.get('compartments', []):
                            if rcomp.get('id') == reservationCompId:
                                for rpref in rcomp.get('preferences', []):
                                    if rpref.get('id') == reservationPrefId:
                                        pref_position = rpref.get('position', 'no_preference')
                                        break
                                break
                    ro_obj["sections"] = [{
                        "index": 0,
                        "compartment": {
                            "id": int(reservationCompId),
                            "allocation": {
                                "positionOfPlaces": pref_position,
                                "necessary": False
                            }
                        }
                    }]
                section_data["reservationOnly"] = ro_obj
                print u"Fallback reservation-only: offer=%s class=%s" % (offerId[:20], ro_class)
            elif compartmentId:
                section_data["reservation"] = {
                    "isSelected": True,
                    "sections": [
                        {
                            "index": 0,
                            "compartment": {
                                "id": int(compartmentId),
                                "allocation": {
                                    "positionOfPlaces": "no_preference",
                                    "necessary": False
                                }
                            }
                        }
                    ],
                    "compartmentId": int(compartmentId)
                }
                print u"Added reservation: compartmentId=%s" % compartmentId
            elif wantReservation:
                # Optional seat reservation for normal trains
                res_obj = {"isSelected": True}
                if reservationCompId:
                    pref_position = "no_preference"
                    for rsec in self._lastReservationOptions or []:
                        for rcomp in rsec.get('compartments', []):
                            if rcomp.get('id') == reservationCompId:
                                for rpref in rcomp.get('preferences', []):
                                    if rpref.get('id') == reservationPrefId:
                                        pref_position = rpref.get('position', 'no_preference')
                                        break
                                break
                    res_obj["sections"] = [{
                        "index": 0,
                        "compartment": {
                            "id": int(reservationCompId),
                            "allocation": {
                                "positionOfPlaces": pref_position,
                                "necessary": False
                            }
                        }
                    }]
                    print u"Added reservation with comp=%s pref=%s" % (reservationCompId, pref_position)
                else:
                    print u"Added optional seat reservation"
                section_data["reservation"] = res_obj
            
            offer_sections.append(section_data)
        
        print u"Final offerSections: %s" % json.dumps(offer_sections)
        
        # Generate cref timestamp
        import datetime
        cref_timestamp = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+01:00")
        
        # Add to shopping cart directly (bikes are added separately)
        cart_request = {
            "selection": {
                "connectionId": connectionId,
                "offerSections": offer_sections
            },
            "passengers": passengers,
            "cref": "oebb-header",
            "crefCreatedAt": cref_timestamp
        }
        
        # Note: bikeCount is handled separately via _addBikeTickets
        
        def onCartResult(cart_result):
            try:
                print u"Add to cart result: %s" % cart_result[:300]
                cart_data = json.loads(cart_result) if cart_result.strip() else {}
                
                if 'error' in cart_data:
                    self.purchaseFailed.emit(u"Fehler: %s" % cart_data.get('error'))
                else:
                    # If bikes requested, add them separately
                    if bikeCount > 0:
                        print u"Adding %d bike tickets..." % bikeCount
                        self._addBikeTickets(connectionId, bikeCount, travelClass, headers)
                    else:
                        self.loadCart()
                        self.purchaseSuccess.emit(u"Ticket zum Warenkorb hinzugefügt!")
            except Exception as e:
                print u"Cart add error: %s" % e
                self.loadCart()
                self.purchaseSuccess.emit(u"Ticket zum Warenkorb hinzugefügt!")
        
        self._startRequest("add_to_cart_offer", "POST", SHOPPING_CART_ADD_URL,
                          json.dumps(cart_request), headers=headers, callback=onCartResult)
    
    def _addBikeTickets(self, connectionId, bikeCount, travelClass, headers):
        """Add bike tickets separately (bikes are booked as separate passengers)"""
        import time
        import datetime
        
        bikes_added = [0]  # Use list to allow modification in nested function
        
        def addNextBike():
            if bikes_added[0] >= bikeCount:
                # All bikes added
                self.loadCart()
                self.purchaseSuccess.emit(u"Ticket + %d Fahrrad zum Warenkorb hinzugefügt!" % bikeCount)
                return
            
            bike_idx = bikes_added[0] + 1
            print u"Adding bike %d of %d..." % (bike_idx, bikeCount)
            
            # Generate bike passenger ID (10-digit timestamp like ÖBB website)
            # Website uses something like current unix timestamp in seconds + small offset
            bike_id = int(time.time()) + bike_idx
            
            bike_passenger = {
                "me": False,
                "remembered": False,
                "markedForDeath": False,
                "challengedFlags": {
                    "hasHandicappedPass": False,
                    "hasAssistanceDog": False,
                    "hasWheelchair": False,
                    "hasAttendant": False
                },
                "cards": [],
                "relations": [],
                "id": bike_id,
                "type": "BIKE",
                "birthdateChangeable": True,
                "birthdateDeletable": True,
                "nameChangeable": True,
                "passengerDeletable": True
            }
            
            # Get current timestamp for cref and datetime
            now = datetime.datetime.now()
            cref_timestamp = now.strftime("%Y-%m-%dT%H:%M:%S+01:00")
            datetime_str = now.strftime("%Y-%m-%dT%H:%M:%S")
            
            # Step 1: Get bike offers (with all required fields)
            offers_request = {
                "selection": {
                    "connectionId": connectionId,
                    "offerSections": []
                },
                "ignoreSelectionOnOfferChange": False,
                "passengers": [bike_passenger],
                "debugFilter": {
                    "noAggregationFilter": False,
                    "noEqclassFilter": False,
                    "noNrtpathFilter": False,
                    "noPaymentFilter": False,
                    "useTripartFilter": False,
                    "noVbxFilter": False,
                    "noCategoriesFilter": False
                },
                "datetime": datetime_str
            }
            
            def onBikeOffersResult(result):
                try:
                    print u"Bike offers result: %s" % result[:300]
                    data = json.loads(result)
                    
                    if 'error' in data:
                        print u"Bike offers error: %s" % data.get('error')
                        bikes_added[0] = bikeCount  # Stop trying
                        self.loadCart()
                        self.purchaseSuccess.emit(u"Ticket hinzugefügt (Fahrrad-Fehler)")
                        return
                    
                    # Find bike offer
                    bike_offer_id = None
                    for section in data.get('offerSections', []):
                        for tc in section.get('travelClasses', []):
                            if tc.get('class') == travelClass:
                                for offer in tc.get('offers', []):
                                    # Check if this is a bike offer
                                    for prod in offer.get('products', []):
                                        name = prod.get('name', {}).get('de', '')
                                        if 'Fahrrad' in name:
                                            bike_offer_id = offer.get('id')
                                            print u"Found bike offer: %s (€%.2f)" % (bike_offer_id[:20], offer.get('price', 0))
                                            break
                                    if bike_offer_id:
                                        break
                            if bike_offer_id:
                                break
                        if bike_offer_id:
                            break
                    
                    if not bike_offer_id:
                        print u"No bike offer found!"
                        bikes_added[0] = bikeCount
                        self.loadCart()
                        self.purchaseSuccess.emit(u"Ticket hinzugefügt (kein Fahrrad-Angebot)")
                        return
                    
                    # Step 2: Add bike to cart (with cref fields like website)
                    bike_cart_request = {
                        "selection": {
                            "connectionId": connectionId,
                            "offerSections": [{
                                "class": travelClass,
                                "id": "0",
                                "offerId": bike_offer_id,
                                "options": []
                            }]
                        },
                        "passengers": [bike_passenger],
                        "cref": "oebb-header",
                        "crefCreatedAt": cref_timestamp
                    }
                    
                    print u"Bike cart request: %s" % json.dumps(bike_cart_request)[:300]
                    
                    def onBikeCartResult(cart_result):
                        try:
                            print u"Bike cart result: %s" % cart_result[:200]
                            bikes_added[0] += 1
                            # Add next bike directly (no delay needed)
                            addNextBike()
                        except Exception as e:
                            print u"Bike cart error: %s" % e
                            bikes_added[0] = bikeCount
                            self.loadCart()
                            self.purchaseSuccess.emit(u"Ticket hinzugefügt")
                    
                    self._startRequest("add_bike_to_cart", "POST", SHOPPING_CART_ADD_URL,
                                      json.dumps(bike_cart_request), headers=headers, callback=onBikeCartResult)
                    
                except Exception as e:
                    print u"Bike offers error: %s" % e
                    bikes_added[0] = bikeCount
                    self.loadCart()
                    self.purchaseSuccess.emit(u"Ticket hinzugefügt")
            
            self._startRequest("get_bike_offers", "POST", OFFERS_URL,
                              json.dumps(offers_request), headers=headers, callback=onBikeOffersResult)
        
        # Start adding bikes
        addNextBike()
    
    # ==================== Payment (NEW from HAR) ====================
    
    @Slot(unicode, float)
    def initPayment(self, paymentMethod, amount):
        """Initialize payment process"""
        if not self._accessToken or not self._cartId:
            self.paymentFailed.emit(u"Kein Warenkorb vorhanden")
            return
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(contentType='application/json')
        
        # From HAR: Payment init request
        request_data = {
            "paymentMethod": paymentMethod,  # e.g. "CREDITCARD_WC"
            "favoriteNumber": "0",
            "amount": amount,
            "orderId": self._cartId
        }
        
        def onResult(result):
            try:
                print u"Payment init result: %s" % result[:300]
                data = json.loads(result)
                if 'error' in data:
                    self.paymentFailed.emit(u"Zahlung fehlgeschlagen: %s" % data.get('error'))
                elif 'paymentId' in data:
                    # Success - return payment info for redirect
                    self.paymentInitialized.emit(json.dumps({
                        "paymentId": data.get('paymentId'),
                        "transactionNumber": data.get('transactionNumber'),
                        "orderId": self._cartId
                    }))
                else:
                    self.paymentFailed.emit(u"Unerwartete Antwort")
            except Exception as e:
                print u"Payment init error: %s" % e
                self.paymentFailed.emit(u"Fehler: %s" % e)
        
        self._startRequest("payment_init", "POST", PAYMENT_INIT_URL,
                          json.dumps(request_data), headers=headers, callback=onResult)
    
    @Slot(result=unicode)
    def getPaymentMethods(self):
        """Get available payment methods"""
        return json.dumps([
            {"id": "CREDITCARD_WC", "name": "Kreditkarte", "icon": "credit-card"},
            {"id": "SOFORT", "name": "Sofort/Klarna", "icon": "bank"},
            {"id": "EPS", "name": "eps Überweisung", "icon": "bank"},
            {"id": "PAYPAL", "name": "PayPal", "icon": "paypal"}
        ])
    
    @Slot(unicode)
    def openPaymentPage(self, paymentId):
        """Open payment page in browser"""
        url = "https://shop.oebbtickets.at/de/ticket/payment"
        self.openInBrowser(url)
    
    # ==================== FULL PAYMENT FLOW (3.9.0) ====================
    
    @Slot(float)
    def startPayment(self, amount):
        """Start complete payment flow with frictionless 3DS
        
        Flow from HAR:
        1. POST /api/payment/v1/init -> paymentId, transactionNumber
        2. POST /api/payment/v1/pay (with order details) -> redirectUrl
        3. For frictionless: POST /api/payment/v1/pay (authorized: true) -> orderBookingCode
        """
        if not self._accessToken:
            self.paymentFailed.emit(u"Bitte zuerst anmelden")
            return
        
        if not self._cartId:
            self.paymentFailed.emit(u"Kein Warenkorb vorhanden")
            return
        
        print u"=== START PAYMENT ==="
        print u"Amount: %.2f, CartId: %s" % (amount, self._cartId)
        
        self.refreshTokenIfNeeded()
        headers = self._getWebApiHeaders(contentType='application/json')
        
        # Step 1: Init payment
        init_data = {
            "paymentMethod": "CREDITCARD_WC",
            "favoriteNumber": "0",
            "amount": amount,
            "orderId": self._cartId
        }
        
        def onInitResult(result):
            try:
                print u"Payment init result: %s" % result[:300]
                data = json.loads(result)
                
                if 'error' in data:
                    self.paymentFailed.emit(u"Zahlung fehlgeschlagen: %s" % data.get('error', {}).get('message', 'Unbekannt'))
                    return
                
                payment_id = data.get('paymentId', '')
                transaction_number = data.get('transactionNumber', '')
                
                if not payment_id:
                    self.paymentFailed.emit(u"Keine PaymentId erhalten")
                    return
                
                print u"PaymentId: %s" % payment_id
                
                # Step 2: Start payment (get redirect URL)
                self._startPaymentPay(payment_id, amount, headers)
                
            except Exception as e:
                print u"Payment init error: %s" % e
                import traceback
                traceback.print_exc()
                self.paymentFailed.emit(u"Fehler: %s" % unicode(e))
        
        self._startRequest("payment_init", "POST", PAYMENT_INIT_URL,
                          json.dumps(init_data), headers=headers, callback=onInitResult)
    
    def _startPaymentPay(self, paymentId, amount, headers):
        """Step 2: Start payment and get redirect URL"""
        now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000")
        
        pay_data = {
            "lastChangeDate": now,
            "amount": amount,
            "scAmount": amount,
            "orderState": "SHOPCART",
            "payment": {
                "method": "CREDITCARD_WC",
                "paymentId": paymentId,
                "favoriteNumber": "0",
                "favorite": True
            },
            "newsletter": False,
            "orderId": self._cartId,
            "createFavorite": False,
            "email": self._userInfo.get('email', ''),
            "customerPermissions": {
                "personalNewsletter": False
            }
        }
        
        def onPayResult(result):
            try:
                print u"Payment pay result: %s" % result[:500]
                data = json.loads(result)
                
                if 'error' in data:
                    self.paymentFailed.emit(u"Zahlung fehlgeschlagen: %s" % data.get('error', {}).get('message', 'Unbekannt'))
                    return
                
                redirect_url = data.get('redirectUrl', '')
                
                if redirect_url:
                    print u"Got redirect URL: %s" % redirect_url[:80]
                    # Must call Saferpay URL first to trigger 3DS verification
                    self._callSaferpayThenComplete(redirect_url, paymentId, headers)
                elif 'orderBookingCode' in data:
                    # Already completed!
                    self._handlePaymentSuccess(data)
                else:
                    self.paymentFailed.emit(u"Unerwartete Antwort vom Server")
                
            except Exception as e:
                print u"Payment pay error: %s" % e
                import traceback
                traceback.print_exc()
                self.paymentFailed.emit(u"Fehler: %s" % unicode(e))
        
        self._startRequest("payment_pay1", "POST", PAYMENT_PAY_URL,
                          json.dumps(pay_data), headers=headers, callback=onPayResult)
    
    def _callSaferpayThenComplete(self, saferpayUrl, paymentId, oebbHeaders):
        """Simulate 3DS browser data collection
        
        CRITICAL FIX (v3.10.0): The browser does:
        1. GET /vt2/Api/SharedThreeDS/... -> REDIRECTS to /ThreeDSAuthentication/...
        2. POST to /ThreeDSAuthentication/... (the REDIRECTED URL, not original!)
        
        We MUST use the final URL after redirects for the POST!
        http_helper now saves the final URL to saferpay_final_url.txt
        """
        print u"=== 3DS BROWSER DATA COLLECTION ==="
        print u"Original URL: %s" % saferpayUrl
        
        # Browser-like headers
        saferpay_headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'de-AT,de;q=0.9,en;q=0.8',
            'User-Agent': 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
            'Referer': 'https://shop.oebbtickets.at/',
            'Connection': 'keep-alive'
        }
        
        def onSaferpayGet(result):
            print u"Saferpay GET response (%d chars)" % len(result)
            
            # Save to file for analysis - use fixed path
            try:
                log_dir = "/home/user/.config/oebb-tickets"
                if not os.path.exists(log_dir):
                    os.makedirs(log_dir)
                log_file = os.path.join(log_dir, "saferpay_get.html")
                with open(log_file, 'w') as f:
                    f.write(result.encode('utf-8') if isinstance(result, unicode) else result)
                print u"Saved GET response to: %s" % log_file
            except Exception as e:
                print u"Failed to save GET response: %s" % e
            
            # CRITICAL: Load the final URL after redirects!
            final_url = self._loadSaferpayFinalUrl()
            if final_url and final_url != saferpayUrl:
                print u"Using REDIRECTED URL for POST: %s" % final_url[:100]
                post_url = final_url
            else:
                print u"WARNING: No redirect detected, using original URL"
                post_url = saferpayUrl
            
            # Now POST browser data to the FINAL URL
            self._postBrowserDataToSaferpay(post_url, paymentId, oebbHeaders)
        
        self._startRequest("saferpay_get", "GET", saferpayUrl,
                          headers=saferpay_headers, callback=onSaferpayGet)
    
    def _loadSaferpayFinalUrl(self):
        """Load the final URL saved by http_helper after following redirects"""
        url_file = "/home/user/.config/oebb-tickets/saferpay_final_url.txt"
        try:
            if os.path.exists(url_file):
                with open(url_file, 'r') as f:
                    url = f.read().strip()
                if url:
                    print u"Loaded final URL: %s" % url[:100]
                    return url
        except Exception as e:
            print u"Failed to load final URL: %s" % e
        return None
    
    def _postBrowserDataToSaferpay(self, saferpayUrl, paymentId, oebbHeaders):
        """POST browser data to Saferpay - cookies are handled by http_helper"""
        print u"=== POSTING BROWSER DATA TO SAFERPAY ==="
        
        # Simulate N9 browser data
        browser_data = {
            'Width': '854',
            'Height': '480', 
            'ColorDepth': '32',
            'JavascriptEnabled': 'true',
            'JavaEnabled': 'false',
            'TimezoneOffset': '-60'  # CET
        }
        
        post_data = urllib.urlencode(browser_data)
        print u"POST data: %s" % post_data
        
        # Headers - cookies will be added by http_helper automatically
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'de-AT,de;q=0.9,en;q=0.8',
            'User-Agent': 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
            'Origin': 'https://www.saferpay.com',
            'Referer': saferpayUrl
        }
        
        def onPostResult(result):
            print u"Saferpay POST response (%d chars)" % len(result)
            
            # Save to file for analysis - use fixed path
            try:
                log_dir = "/home/user/.config/oebb-tickets"
                if not os.path.exists(log_dir):
                    os.makedirs(log_dir)
                log_file = os.path.join(log_dir, "saferpay_post.html")
                with open(log_file, 'w') as f:
                    f.write(result.encode('utf-8') if isinstance(result, unicode) else result)
                print u"Saved POST response to: %s" % log_file
            except Exception as e:
                print u"Failed to save POST response: %s" % e
            
            result_lower = result.lower()
            
            # Check for error
            if 'unexpected error' in result_lower or ('error' in result_lower and 'occurred' in result_lower):
                print u"Saferpay returned error!"
                self.paymentFailed.emit(u"3DS Fehler - bitte später erneut versuchen")
                return
            
            # Check for 3DS2 Challenge (App confirmation required)
            # Look for renderRedirectToAcs which contains the 3DS method data
            if 'renderRedirectToAcs' in result or 'redirectToAcs' in result_lower:
                print u"=== 3DS2 CHALLENGE DETECTED ==="
                self._handle3DS2Challenge(result, paymentId, oebbHeaders)
                return
            
            # Legacy check for password/TAN forms
            if 'password' in result_lower or 'passwort' in result_lower or 'tan' in result_lower:
                print u"Legacy 3DS Challenge - not supported"
                self.paymentFailed.emit(u"3DS-Bestätigung nicht unterstützt")
                return
            
            # Frictionless - complete payment
            print u"=== FRICTIONLESS 3DS - Completing payment ==="
            self._completePaymentAfterSaferpay(paymentId, oebbHeaders)
        
        self._startRequest("saferpay_post", "POST", saferpayUrl,
                          headers=headers, data=post_data, callback=onPostResult)
    
    def _handle3DS2Challenge(self, saferpayHtml, paymentId, oebbHeaders):
        """Handle 3DS2 Challenge with App confirmation (N26, George, etc.)
        
        The response contains:
        - action: URL to POST to (methodurl.psp-solutions.com)
        - fields: [["methodUrl", "..."], ["threeDSMethodData", "..."]]
        - notificationUrl: URL to poll for completion
        
        Flow:
        1. POST fields to action URL -> triggers push to banking app
        2. Poll notificationUrl until user confirms
        3. Complete payment
        """
        print u"=== HANDLING 3DS2 CHALLENGE ==="
        
        import re
        
        # Find the start of the renderRedirectToAcs call
        start_marker = 'renderRedirectToAcs("threeDs-redirectToAcs-container", '
        start_idx = saferpayHtml.find(start_marker)
        if start_idx == -1:
            print u"Could not find renderRedirectToAcs marker"
            self.paymentFailed.emit(u"3DS-Daten konnten nicht gelesen werden")
            return
        
        # Find the JSON start (after the marker)
        json_start = start_idx + len(start_marker)
        
        # Find matching closing brace by counting braces
        brace_count = 0
        json_end = json_start
        in_string = False
        escape_next = False
        
        for i, char in enumerate(saferpayHtml[json_start:]):
            if escape_next:
                escape_next = False
                continue
            if char == '\\':
                escape_next = True
                continue
            if char == '"' and not escape_next:
                in_string = not in_string
                continue
            if in_string:
                continue
            if char == '{':
                brace_count += 1
            elif char == '}':
                brace_count -= 1
                if brace_count == 0:
                    json_end = json_start + i + 1
                    break
        
        try:
            json_str = saferpayHtml[json_start:json_end]
            print u"Extracted JSON (%d chars)" % len(json_str)
            
            challenge_data = json.loads(json_str)
            
            action_url = challenge_data.get('action', '')
            fields = challenge_data.get('fields', [])
            notification_url = challenge_data.get('notificationUrl', '')
            
            print u"Action URL: %s" % action_url[:80]
            print u"Notification URL: %s" % notification_url[:80]
            print u"Fields: %d" % len(fields)
            
            if not action_url or not notification_url:
                print u"Missing action or notification URL"
                self.paymentFailed.emit(u"3DS-URLs fehlen")
                return
            
            # Store for polling
            self._3dsNotificationUrl = notification_url
            self._3dsPaymentId = paymentId
            self._3dsHeaders = oebbHeaders
            
            # Build form data from fields
            form_data = {}
            for field in fields:
                if len(field) >= 2:
                    form_data[field[0]] = field[1]
            
            print u"Form data keys: %s" % form_data.keys()
            
            # POST to methodurl to trigger bank push notification
            self._post3DSMethod(action_url, form_data)
            
        except Exception as e:
            print u"Error parsing 3DS2 challenge: %s" % str(e)
            print u"JSON string: %s" % saferpayHtml[json_start:json_start+200]
            self.paymentFailed.emit(u"3DS-Parsing Fehler: %s" % str(e))
    
    def _post3DSMethod(self, actionUrl, formData):
        """POST to 3DS method URL to trigger bank push notification"""
        print u"=== POSTING TO 3DS METHOD URL ==="
        print u"URL: %s" % actionUrl
        
        post_data = urllib.urlencode(formData)
        print u"POST data length: %d" % len(post_data)
        
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0',
            'Origin': 'https://www.saferpay.com',
            'Referer': 'https://www.saferpay.com/'
        }
        
        def onMethodResult(result):
            print u"3DS Method POST response (%d chars)" % len(result)
            
            # Save for debugging
            try:
                log_file = "/home/user/.config/oebb-tickets/3ds_method.html"
                with open(log_file, 'w') as f:
                    f.write(result.encode('utf-8') if isinstance(result, unicode) else result)
                print u"Saved 3DS method response to: %s" % log_file
            except Exception as e:
                print u"Failed to save: %s" % e
            
            # Check if response contains iframe to bank - that means we need to follow it
            if '3ds-challenge' in result or 'iframe' in result.lower():
                print u"Response contains bank redirect - extracting..."
                self._handle3DSMethodResponse(result)
            else:
                # No iframe, start polling directly
                print u"No bank iframe - starting polling..."
                self._emitStartPolling()
        
        self._startRequest("3ds_method", "POST", actionUrl, post_data,
                          headers=headers, callback=onMethodResult)
    
    def _handle3DSMethodResponse(self, html):
        """Handle the methodurl response - may contain iframe to bank"""
        import re
        
        # Look for iframe src to bank
        iframe_match = re.search(r'<iframe[^>]*src=["\']([^"\']+)["\']', html, re.IGNORECASE)
        if iframe_match:
            bank_url = iframe_match.group(1)
            # Decode HTML entities
            bank_url = bank_url.replace('&amp;', '&')
            print u"Found bank iframe: %s" % bank_url[:80]
            
            # We need to GET this URL to trigger the bank push
            self._triggerBankPush(bank_url)
        else:
            # Look for form action
            form_match = re.search(r'<form[^>]*action=["\']([^"\']+)["\']', html, re.IGNORECASE)
            if form_match:
                form_url = form_match.group(1)
                print u"Found form action: %s" % form_url[:80]
                
                # Extract hidden fields
                inputs = re.findall(r'<input[^>]*name=["\']([^"\']+)["\'][^>]*value=["\']([^"\']*)["\']', html, re.IGNORECASE)
                form_data = dict(inputs)
                
                if form_data:
                    self._submitBankForm(form_url, form_data)
                else:
                    self._triggerBankPush(form_url)
            else:
                print u"No iframe or form found - starting polling"
                self._emitStartPolling()
    
    def _triggerBankPush(self, bankUrl):
        """GET the bank URL to trigger push notification"""
        print u"=== TRIGGERING BANK PUSH ==="
        print u"URL: %s" % bankUrl
        
        # IMPORTANT: Extract threeDSMethodData from the URL - it's not in the HTML form!
        import re
        threeDSMethodData = None
        method_data_match = re.search(r'threeDSMethodData=([^&]+)', bankUrl)
        if method_data_match:
            threeDSMethodData = method_data_match.group(1)
            # Decode HTML entities and URL encoding
            threeDSMethodData = threeDSMethodData.replace('&amp;', '&')
            threeDSMethodData = urllib.unquote(threeDSMethodData)
            print u"Extracted threeDSMethodData from URL (%d chars)" % len(threeDSMethodData)
        
        headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
        }
        
        def onBankResult(result):
            print u"Bank response (%d chars)" % len(result)
            
            # SAVE for later access in _postToBankForm (needed for N26 Risk-Engine)
            self._last_bank_html = result
            
            # Save for debugging
            try:
                log_file = "/home/user/.config/oebb-tickets/3ds_bank.html"
                with open(log_file, 'w') as f:
                    f.write(result.encode('utf-8') if isinstance(result, unicode) else result)
                print u"Saved bank response to: %s" % log_file
            except:
                pass
            
            # Check if response contains a form that needs to be POSTed
            form_match = re.search(r'<form[^>]*action=["\']([^"\']+)["\'][^>]*method=["\']POST["\']', result, re.IGNORECASE)
            if not form_match:
                form_match = re.search(r'<form[^>]*method=["\']POST["\'][^>]*action=["\']([^"\']+)["\']', result, re.IGNORECASE)
            
            if form_match:
                form_url = form_match.group(1)
                print u"Found POST form to: %s" % form_url[:80]
                
                # Use the threeDSMethodData from the URL, not from the empty HTML input!
                form_data = {}
                if threeDSMethodData:
                    form_data['threeDSMethodData'] = threeDSMethodData
                    print u"Using threeDSMethodData from URL"
                else:
                    # Fallback: try to extract from HTML (probably empty)
                    inputs = re.findall(r'<input[^>]*name=["\']([^"\']+)["\'][^>]*(?:value=["\']([^"\']*)["\'])?', result, re.IGNORECASE)
                    for name, value in inputs:
                        form_data[name] = value if value else ''
                    print u"WARNING: Using form data from HTML (may be empty)"
                
                print u"Form data keys: %s" % form_data.keys()
                self._postToBankForm(form_url, form_data)
            else:
                # No form to POST, start polling
                print u"No POST form - starting polling"
                self.waiting3DS.emit(True)  # Show persistent banner
                self._emitStartPolling()
        
        self._startRequest("3ds_bank", "GET", bankUrl, headers=headers, callback=onBankResult)
    
    def _postToBankForm(self, formUrl, formData):
        """POST the form to the bank to trigger push notification
        
        FIXED in 3.10.9: Handle N26 3DS-method-finish flow with Risk-Engine data
        """
        print u"=== POSTING TO BANK FORM ==="
        print u"URL: %s" % formUrl
        
        # Check if this is N26's 3DS method - detect by URL
        if '3ds-challenge.n26.com' in formUrl or 'n26.com/3ds-method' in formUrl:
            print u"Detected N26 3DS flow - applying special handling"
            
            # N26 expects POST to 3ds-method-finish, not 3ds-method-start
            if '3ds-method-start' in formUrl:
                formUrl = formUrl.replace('3ds-method-start', '3ds-method-finish')
                # Remove query params (org=NSZ etc) - they're for GET, not POST
                if '?' in formUrl:
                    formUrl = formUrl.split('?')[0]
                print u"Redirecting to 3ds-method-finish: %s" % formUrl
            
            # Add N26 Risk-Engine data
            device_id = None
            
            # Load fingerprint from persistent storage
            fp_file = os.path.expanduser("~/.config/oebb-tickets/n26_fingerprint.txt")
            if os.path.exists(fp_file):
                try:
                    with open(fp_file, 'r') as f:
                        device_id = f.read().strip()
                    if device_id and len(device_id) == 32:
                        print u"Using stored N26 fingerprint: %s..." % device_id[:16]
                    else:
                        device_id = None
                        print u"Invalid fingerprint in file, ignoring"
                except Exception as e:
                    print u"Could not load fingerprint: %s" % str(e)
            
            if not device_id:
                # No fingerprint stored - payment will likely fail
                print u"WARNING: No N26 fingerprint configured!"
                print u"Generate one at: https://anthropic.com/n26-fingerprint or use the app settings"
                # Use a fallback that will probably fail but at least attempts the flow
                import hashlib
                device_id = hashlib.md5("oebb-tickets-n26-fallback").hexdigest()
                print u"Using fallback fingerprint (will likely fail): %s..." % device_id[:16]
            
            formData['N26_RISK_ENGINE_deviceId'] = device_id
            formData['N26_RISK_ENGINE_deviceDna'] = 'N26RiskEngineDeviceDna'
            print u"Added N26 Risk-Engine data"
            
            # Add required hidden fields for N26
            # Extract threeDSServerTransID from the threeDSMethodData if present
            if 'threeDSMethodData' in formData:
                try:
                    import base64
                    method_data = formData['threeDSMethodData']
                    # Fix Base64 padding - add missing = characters
                    padding_needed = 4 - (len(method_data) % 4)
                    if padding_needed != 4:
                        method_data = method_data + ('=' * padding_needed)
                    decoded = base64.b64decode(method_data)
                    method_json = json.loads(decoded)
                    if 'threeDSServerTransID' in method_json:
                        formData['threeDSServerTransID'] = method_json['threeDSServerTransID']
                        print u"Added threeDSServerTransID: %s" % method_json['threeDSServerTransID']
                    if 'threeDSMethodNotificationURL' in method_json:
                        formData['threeDSMethodNotificationURL'] = method_json['threeDSMethodNotificationURL']
                        print u"Added threeDSMethodNotificationURL"
                    # Add org parameter from the original URL
                    formData['org'] = 'NSZ'
                    print u"Added org: NSZ"
                    # Remove the raw Base64 blob - N26 wants the extracted fields
                    del formData['threeDSMethodData']
                    print u"Removed raw threeDSMethodData"
                except Exception as e:
                    print u"Could not decode threeDSMethodData: %s" % str(e)
        
        # Log data lengths for debugging
        for key, value in formData.items():
            print u"Form field '%s': %d chars" % (key, len(value) if value else 0)
        
        post_data = urllib.urlencode(formData)
        print u"POST data length: %d" % len(post_data)
        
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
        }
        
        def onBankPostResult(result):
            print u"Bank POST response (%d chars)" % len(result)
            
            # Save for debugging
            try:
                log_file = "/home/user/.config/oebb-tickets/3ds_bank_post.html"
                with open(log_file, 'w') as f:
                    f.write(result.encode('utf-8') if isinstance(result, unicode) else result)
            except:
                pass
            
            # Check if N26 returned a callback form that needs to be submitted
            # N26 responds with JavaScript that creates a form and submits it to the PSP callback
            callback_match = re.search(
                r'form\.setAttribute\(["\']action["\']\s*,\s*["\']([^"\']+)["\']',
                result, re.IGNORECASE
            )
            callback_data_match = re.search(
                r'addParameter\s*\(\s*form\s*,\s*["\']threeDSMethodData["\']\s*,\s*["\']([^"\']+)["\']',
                result, re.IGNORECASE
            )
            
            if callback_match and callback_data_match:
                callback_url = callback_match.group(1).replace('\\/', '/')
                callback_data = callback_data_match.group(1).replace('\\\n', '').replace('\n', '')
                print u"=== N26 CALLBACK DETECTED ==="
                print u"Callback URL: %s" % callback_url[:80]
                print u"Callback data: %d chars" % len(callback_data)
                
                # POST the callback to the PSP
                callback_form = {'threeDSMethodData': callback_data}
                callback_post_data = urllib.urlencode(callback_form)
                
                callback_headers = {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
                }
                
                def onCallbackResult(cb_result):
                    print u"Callback POST response (%d chars)" % len(cb_result)
                    # Save for debugging
                    try:
                        cb_log = "/home/user/.config/oebb-tickets/3ds_callback.html"
                        with open(cb_log, 'w') as f:
                            f.write(cb_result.encode('utf-8') if isinstance(cb_result, unicode) else cb_result)
                    except:
                        pass
                    
                    # Check if callback contains a redirect to result URL
                    # PSP expects us to follow this to confirm 3DS method success
                    # Use unicode pattern for Python 2 compatibility
                    result_match = re.search(
                        ur'window\.open\s*\(\s*["\']([^"\']+)["\']',
                        cb_result, re.IGNORECASE | re.DOTALL
                    )
                    
                    if result_match:
                        result_url = result_match.group(1)
                        print u"Found window.open URL: %s" % result_url[:60]
                        
                        # Check if this is a success URL
                        if 'hasMethodUrlSucceeded=true' in result_url:
                            print u"=== FOLLOWING 3DS METHOD RESULT ==="
                            print u"Result URL: %s" % result_url[:80]
                            
                            result_headers = {
                                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                                'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
                            }
                            
                            def onResultUrl(result_resp):
                                print u"Result URL response (%d chars)" % len(result_resp)
                                # Save for debugging
                                try:
                                    result_log = "/home/user/.config/oebb-tickets/3ds_result.html"
                                    with open(result_log, 'w') as f:
                                        f.write(result_resp.encode('utf-8') if isinstance(result_resp, unicode) else result_resp)
                                except:
                                    pass
                                
                                # Check if this contains a frictionless redirection form
                                # PSP expects us to POST this form to complete 3DS method
                                frictionless_match = re.search(
                                    ur'<form[^>]+action=["\']([^"\']+)["\'][^>]*>',
                                    result_resp, re.IGNORECASE | re.DOTALL
                                )
                                
                                if frictionless_match and 'Verify' in frictionless_match.group(1):
                                    verify_url = frictionless_match.group(1)
                                    print u"=== FRICTIONLESS REDIRECTION DETECTED ==="
                                    print u"Verify URL: %s" % verify_url[:80]
                                    
                                    # Extract hidden fields - try both orderings
                                    frictionless_data = {}
                                    for field_match in re.finditer(
                                        ur'<input[^>]+name=["\']([^"\']+)["\'][^>]*value=["\']([^"\']*)["\']',
                                        result_resp, re.IGNORECASE
                                    ):
                                        frictionless_data[field_match.group(1)] = field_match.group(2)
                                    for field_match in re.finditer(
                                        ur'<input[^>]+value=["\']([^"\']*)["\'][^>]*name=["\']([^"\']+)["\']',
                                        result_resp, re.IGNORECASE
                                    ):
                                        frictionless_data[field_match.group(2)] = field_match.group(1)
                                    # Also match fields with name but no value (defaults to empty)
                                    for field_match in re.finditer(
                                        ur'<input[^>]+name=["\']([^"\']+)["\'][^/>]*/?>',
                                        result_resp, re.IGNORECASE
                                    ):
                                        name = field_match.group(1)
                                        if name not in frictionless_data and name != 'submitButton':
                                            frictionless_data[name] = ''
                                    
                                    print u"Frictionless fields: %s" % frictionless_data.keys()
                                    
                                    frictionless_post = urllib.urlencode(frictionless_data)
                                    frictionless_headers = {
                                        'Content-Type': 'application/x-www-form-urlencoded',
                                        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                                        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
                                    }
                                    
                                    def onFrictionlessResult(fr_result):
                                        print u"Frictionless POST response (%d chars)" % len(fr_result)
                                        try:
                                            fr_log = "/home/user/.config/oebb-tickets/3ds_frictionless.html"
                                            with open(fr_log, 'w') as f:
                                                f.write(fr_result.encode('utf-8') if isinstance(fr_result, unicode) else fr_result)
                                        except:
                                            pass
                                        print u"Frictionless completed - starting polling"
                                        self.waiting3DS.emit(True)
                                        self._emitStartPolling()
                                    
                                    self._startRequest("3ds_frictionless", "POST", verify_url, frictionless_post,
                                                      headers=frictionless_headers, callback=onFrictionlessResult)
                                    return  # Don't start polling yet
                                
                                # Now start polling
                                print u"3DS Method completed - starting polling"
                                self.waiting3DS.emit(True)
                                self._emitStartPolling()
                            
                            self._startRequest("3ds_result", "GET", result_url,
                                              headers=result_headers, callback=onResultUrl)
                            return  # Don't start polling yet
                        else:
                            print u"URL doesn't contain success flag, ignoring"
                    else:
                        print u"No window.open found in callback response"
                    
                    # Now start polling - the PSP should have received the confirmation
                    print u"Callback completed - starting polling"
                    self.waiting3DS.emit(True)
                    self._emitStartPolling()
                
                self._startRequest("3ds_callback", "POST", callback_url, callback_post_data,
                                  headers=callback_headers, callback=onCallbackResult)
                return  # Don't start polling yet - wait for callback
            
            # Now start polling - the bank should have received the request
            # and will send a push notification
            print u"Bank form posted - starting polling"
            self.waiting3DS.emit(True)  # Show persistent banner
            self._emitStartPolling()
        
        self._startRequest("3ds_bank_post", "POST", formUrl, post_data,
                          headers=headers, callback=onBankPostResult)
    
    def _submitBankForm(self, formUrl, formData):
        """Submit form to bank"""
        print u"=== SUBMITTING BANK FORM ==="
        print u"URL: %s" % formUrl
        
        post_data = urllib.urlencode(formData)
        
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
        }
        
        def onFormResult(result):
            print u"Bank form result (%d chars)" % len(result)
            
            # Check for more redirects
            if 'iframe' in result.lower() or 'form' in result.lower():
                self._handle3DSMethodResponse(result)
            else:
                print u"Bank form done - starting polling"
                self._emitStartPolling()
        
        self._startRequest("3ds_bank_form", "POST", formUrl, post_data,
                          headers=headers, callback=onFormResult)
    
    def _emitStartPolling(self):
        """Emit signal to start polling in main thread"""
        print u"Bitte in Banking-App bestätigen!"
        # The persistent banner is shown via waiting3DS signal
        
        # Start polling using a single-shot approach instead of QTimer
        # We'll poll by chaining requests
        self._3dsPollCount = 0
        self._3dsMaxPolls = 60
        self._doPoll()
    
    def _start3DSPolling(self):
        """Start polling the 3DS notification URL - DEPRECATED, use _doPoll"""
        self._emitStartPolling()
    
    def _doPoll(self):
        """Poll 3DS notification URL using chained requests instead of QTimer"""
        self._3dsPollCount += 1
        print u"=== POLLING 3DS STATUS (%d/%d) ===" % (self._3dsPollCount, self._3dsMaxPolls)
        
        if self._3dsPollCount >= self._3dsMaxPolls:
            print u"3DS polling timeout"
            self.waiting3DS.emit(False)  # Hide persistent banner
            self.paymentFailed.emit(u"Zeitüberschreitung - bitte erneut versuchen")
            return
        
        headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
        }
        
        def onPollResult(result):
            print u"3DS poll result (%d chars)" % len(result)
            
            # Save for debugging
            try:
                log_file = "/home/user/.config/oebb-tickets/3ds_poll_%d.html" % self._3dsPollCount
                with open(log_file, 'w') as f:
                    f.write(result.encode('utf-8') if isinstance(result, unicode) else result)
            except:
                pass
            
            # Check the FINAL URL after redirect - read directly from file
            try:
                final_url_file = "/home/user/.config/oebb-tickets/saferpay_final_url.txt"
                if os.path.exists(final_url_file):
                    with open(final_url_file, 'r') as f:
                        final_url = f.read().strip()
                    print u"Poll final URL: %s" % (final_url[:100] if final_url else "None")
                    
                    if final_url:
                        # IMPORTANT: Check for error FIRST before checking for pendingPayment
                        if 'error=true' in final_url or 'success=false' in final_url:
                            # This means 3DS failed - but could be timeout, keep polling
                            print u"3DS returned error - bank confirmation missing, keep polling..."
                            # Don't fail yet, keep polling - user might still confirm
                            import time
                            time.sleep(5)
                            self._doPoll()
                            return
                        
                        # Check for explicit success
                        if 'success=true' in final_url:
                            print u"=== 3DS SUCCESS (success=true in URL) ==="
                            self._completePaymentAfterSaferpay(self._3dsPaymentId, self._3dsHeaders)
                            return
                        
                        # If redirected to ÖBB shop without error, it's success
                        if 'shop.oebbtickets.at' in final_url and 'error' not in final_url:
                            print u"=== 3DS SUCCESS (redirected to ÖBB without error) ==="
                            self._completePaymentAfterSaferpay(self._3dsPaymentId, self._3dsHeaders)
                            return
            except Exception as e:
                print u"Could not check final URL: %s" % str(e)
            
            # Check if we're still on Saferpay (waiting for confirmation)
            if 'saferpay' in result.lower() and len(result) < 5000:
                # Small page from Saferpay = still waiting
                print u"Still on Saferpay - waiting... (next poll in 5s)"
                import time
                time.sleep(5)
                self._doPoll()
                return
            
            # If we got a large response (>10KB) with ÖBB content, check for error indicators
            if len(result) > 10000:
                if 'oebbtickets' in result.lower() or 'oebb' in result.lower():
                    # Got ÖBB page - but need to check if it's error or success
                    # The error page still contains the full ÖBB layout
                    # Keep polling - the final_url check above should catch success
                    print u"Got ÖBB page - checking if success or error..."
                    import time
                    time.sleep(5)
                    self._doPoll()
                    return
            
            # Still waiting - poll again after delay
            print u"Still waiting for confirmation... (next poll in 5s)"
            import time
            time.sleep(5)
            self._doPoll()
        
        self._startRequest("3ds_poll_%d" % self._3dsPollCount, "GET", 
                          self._3dsNotificationUrl, headers=headers, callback=onPollResult)
    
    def _poll3DSStatus(self):
        """DEPRECATED - use _doPoll instead"""
        pass
    
    def _handle3DSChallenge(self, saferpayHtml, saferpayUrl, paymentId, oebbHeaders):
        """Handle 3DS challenge by parsing and following the flow"""
        print u"=== HANDLING 3DS CHALLENGE ==="
        
        import re
        
        # Look for form action URL
        form_match = re.search(r'<form[^>]*action=["\']([^"\']+)["\']', saferpayHtml, re.IGNORECASE)
        iframe_match = re.search(r'<iframe[^>]*src=["\']([^"\']+)["\']', saferpayHtml, re.IGNORECASE)
        
        if form_match:
            form_url = form_match.group(1)
            print u"Found form action: %s" % form_url[:100]
            
            # Look for hidden inputs
            inputs = re.findall(r'<input[^>]*name=["\']([^"\']+)["\'][^>]*value=["\']([^"\']*)["\']', saferpayHtml, re.IGNORECASE)
            form_data = dict(inputs)
            print u"Form data: %s" % str(form_data)[:200]
            
            # Submit form
            self._submitSaferpayForm(form_url, form_data, paymentId, oebbHeaders)
            
        elif iframe_match:
            iframe_url = iframe_match.group(1)
            print u"Found iframe src: %s" % iframe_url[:100]
            # Follow iframe
            self._followSaferpayIframe(iframe_url, paymentId, oebbHeaders)
        else:
            # No form found, try completion anyway
            print u"No form/iframe found - trying completion"
            self._completePaymentAfterSaferpay(paymentId, oebbHeaders)
    
    def _submitSaferpayForm(self, formUrl, formData, paymentId, oebbHeaders):
        """Submit Saferpay form data"""
        print u"=== SUBMITTING SAFERPAY FORM ==="
        
        # URL-encode form data
        encoded_data = urllib.urlencode(formData)
        
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
        }
        
        def onFormResult(result):
            print u"Form submit result (%d chars): %s" % (len(result), result[:500])
            # After form submission, try to complete payment
            self._completePaymentAfterSaferpay(paymentId, oebbHeaders)
        
        self._startRequest("saferpay_form", "POST", formUrl, encoded_data,
                          headers=headers, callback=onFormResult)
    
    def _followSaferpayIframe(self, iframeUrl, paymentId, oebbHeaders):
        """Follow Saferpay iframe URL"""
        print u"=== FOLLOWING SAFERPAY IFRAME ==="
        
        headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
        }
        
        def onIframeResult(result):
            print u"Iframe result (%d chars): %s" % (len(result), result[:500])
            # Check for more forms or complete
            if '<form' in result.lower():
                self._handle3DSChallenge(result, iframeUrl, paymentId, oebbHeaders)
            else:
                self._completePaymentAfterSaferpay(paymentId, oebbHeaders)
        
        self._startRequest("saferpay_iframe", "GET", iframeUrl,
                          headers=headers, callback=onIframeResult)
    
    def _completePaymentAfterSaferpay(self, paymentId, headers):
        """Complete payment after Saferpay 3DS flow"""
        print u"=== COMPLETING PAYMENT AFTER SAFERPAY ==="
        self.waiting3DS.emit(False)  # Hide persistent banner
        
        complete_data = {
            "payment": {
                "method": "CREDITCARD_WC",
                "paymentId": paymentId,
                "authorized": True
            }
        }
        
        def onCompleteResult(result):
            try:
                print u"Payment complete result: %s" % result[:500]
                data = json.loads(result)
                
                if 'orderBookingCode' in data:
                    # Success!
                    self._handlePaymentSuccess(data)
                elif 'error' in data:
                    # Parse error message
                    error_body = data.get('body', '')
                    error_msg = 'Zahlung fehlgeschlagen'
                    
                    if isinstance(error_body, str) and error_body:
                        try:
                            error_json = json.loads(error_body)
                            error_msg = error_json.get('error', {}).get('message', error_msg)
                        except:
                            error_msg = error_body[:150]
                    else:
                        err = data.get('error', {})
                        if isinstance(err, dict):
                            error_msg = err.get('message', error_msg)
                        elif isinstance(err, basestring):
                            error_msg = err
                    
                    self.paymentFailed.emit(u"Zahlung fehlgeschlagen:\n%s" % error_msg)
                else:
                    self.paymentFailed.emit(u"Unerwartete Antwort vom Server")
                
            except Exception as e:
                print u"Payment complete error: %s" % e
                import traceback
                traceback.print_exc()
                self.paymentFailed.emit(u"Fehler: %s" % unicode(e))
        
        self._startRequest("payment_complete", "POST", PAYMENT_PAY_URL,
                          json.dumps(complete_data), headers=headers, callback=onCompleteResult)
    
    def _handlePaymentSuccess(self, data):
        """Handle successful payment"""
        order_id = data.get('orderId', self._cartId)
        booking_code = data.get('orderBookingCode', '')
        
        print u"=== PAYMENT SUCCESS ==="
        print u"OrderId: %s" % order_id
        print u"BookingCode: %s" % booking_code
        
        # Clear cart
        self._cartId = None
        self._cartItems = []
        
        # Emit success with booking info
        self.purchaseSuccess.emit(u"Ticket gekauft!\n\nBuchungscode: %s" % booking_code)
        
        # Load order overview for ticket
        self.loadOrderOverview(order_id)
    
    @Slot(unicode)
    def loadOrderOverview(self, orderId):
        """Load order overview to get ticket details
        
        GET /api/order/v9/overview?orderId=<uuid>
        """
        if not self._accessToken or not orderId:
            return
        
        headers = self._getWebApiHeaders()
        url = API_BASE + "/api/order/v9/overview?orderId=%s" % orderId
        
        def onResult(result):
            try:
                print u"Order overview result: %s" % result[:800]
                data = json.loads(result)
                
                if 'error' not in data and 'orderBlocks' in data:
                    # Extract ticket info
                    for block in data.get('orderBlocks', []):
                        for item in block.get('orderItems', []):
                            booking_code = item.get('bookingCode', '')
                            methods = item.get('acquisitionMethods', [])
                            print u"Ticket: %s, Methods: %s" % (booking_code, methods)
                    
                    # Refresh tickets list
                    self.loadTickets()
                    
            except Exception as e:
                print u"Order overview error: %s" % e
        
        self._startRequest("order_overview", "GET", url, headers=headers, callback=onResult)
    
    @Slot(result=float)
    def getCartTotal(self):
        """Get total amount in cart"""
        total = 0.0
        for item in self._cartItems:
            total += item.get('price', 0)
        return total
    
    @Slot(result=unicode)
    def getCartId(self):
        """Get current cart ID"""
        return self._cartId or ""
    
    # ==================== Domain Data (NEW from HAR) ====================
    
    @Slot()
    def loadDomainData(self):
        """Load domain configuration data (cards, stations, config)"""
        headers = self._getWebApiHeaders(withAuth=False)
        
        def onResult(result):
            try:
                print u"Domain data result: %s" % result[:500]
                data = json.loads(result)
                if 'error' not in data:
                    self._domainData = data
                    # Extract discount cards
                    self._discountCards = data.get('cards', [])
                    self.domainDataLoaded.emit(json.dumps({
                        'maxChildAge': data.get('maxChildAge', 15),
                        'maxYoungsterAge': data.get('maxYoungsterAge', 28),
                        'maxPassengerAmount': data.get('maxPassengerAmount', 6),
                        'maxShoppingCartItems': data.get('maxShoppingCartItems', 5),
                        'cardsCount': len(self._discountCards)
                    }))
            except Exception as e:
                print u"Domain data error: %s" % e
        
        self._startRequest("domain", "GET", DOMAIN_DATA_URL, headers=headers, callback=onResult)
    
    @Slot(result=unicode)
    def getTopStations(self):
        """Get top 10 Austrian stations for quick selection"""
        # Use cached domain data if available
        if self._domainData and 'stationInfo' in self._domainData:
            top = self._domainData['stationInfo'].get('topTen', [])
            stations = []
            for s in top:
                stations.append({
                    'name': s.get('name') or s.get('meta', ''),
                    'id': unicode(s.get('number', '')),
                    'meta': s.get('meta', s.get('name', '')),
                    'longitude': s.get('longitude', 0),
                    'latitude': s.get('latitude', 0)
                })
            return json.dumps(stations)
        # Fallback to hardcoded top stations
        return json.dumps(TOP_STATIONS)
    
    @Slot(result=unicode)
    def getDiscountCards(self):
        """Get available discount cards for passenger selection"""
        # Use cached domain data if available
        if self._discountCards:
            # Filter to selectable cards only
            selectable = [c for c in self._discountCards if c.get('isSelectable', False)]
            # Sort: Vorteilscards first, then KlimaTickets
            cards = []
            for c in selectable:
                cards.append({
                    'id': c.get('cardId'),
                    'name': c.get('name', ''),
                    'isDiscountCard': c.get('isDiscountCard', False),
                    'numberRequired': c.get('numberRequired', False),
                    'isFamily': c.get('isFamily', False)
                })
            return json.dumps(cards)
        
        # Fallback to common cards
        return json.dumps([
            {'id': 108, 'name': 'Vorteilscard Classic', 'isDiscountCard': True, 'numberRequired': True},
            {'id': 118, 'name': 'Vorteilscard Senior', 'isDiscountCard': True, 'numberRequired': True},
            {'id': 120, 'name': 'Vorteilscard Jugend', 'isDiscountCard': True, 'numberRequired': True},
            {'id': 7341162, 'name': 'Vorteilscard Family', 'isDiscountCard': True, 'numberRequired': True},
            {'id': 100000040, 'name': 'KlimaTicket Ö Classic', 'isDiscountCard': False, 'numberRequired': True},
            {'id': 9097845, 'name': 'Studentenausweis', 'isDiscountCard': False, 'numberRequired': False},
        ])
    
    @Slot()
    def sendFingerprint(self):
        """Send device fingerprint for tracking (from HAR)"""
        if not self._accessToken:
            return
        
        headers = self._getWebApiHeaders(contentType='application/json')
        
        # Minimal fingerprint data
        fingerprint_data = {
            "userAgent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13",
            "language": "de",
            "platform": "MeeGo",
            "screenWidth": 854,
            "screenHeight": 480,
            "timezoneOffset": -60
        }
        
        def onResult(result):
            print u"Fingerprint sent: %s" % result[:100]
        
        self._startRequest("fingerprint", "POST", FINGERPRINT_URL,
                          json.dumps(fingerprint_data), headers=headers, callback=onResult)
    
    @Slot()
    def checkApiVersion(self):
        """Check API version (from HAR) and update dynamic clientversion"""
        headers = self._getWebApiHeaders(withAuth=False)
        
        def onResult(result):
            try:
                data = json.loads(result)
                version = data.get('version', '')
                if version:
                    self._clientVersion = version
                    print u"API version (dynamic): %s" % version
                else:
                    print u"API version: empty, keeping fallback"
            except:
                pass
        
        self._startRequest("version", "GET", VERSION_URL, headers=headers, callback=onResult)
    
    @Slot(result=unicode)
    def getEpsBanks(self):
        """Get EPS banks for payment"""
        if self._domainData and 'epsBanks' in self._domainData:
            return json.dumps(self._domainData['epsBanks'])
        # Fallback to common banks
        return json.dumps([
            {'id': 500, 'name': 'Erste Bank und Sparkassen'},
            {'id': 531, 'name': 'Raiffeisen Bankengruppe Österreich'},
            {'id': 520, 'name': 'Bank Austria'},
            {'id': 515, 'name': 'BAWAG P.S.K. AG'},
            {'id': 518, 'name': 'Volksbank Gruppe'},
        ])
    
    # ==================== Station Search ====================
    
    @Slot(unicode)
    def searchStations(self, query):
        """Search for stations"""
        if not query or len(query) < 2:
            self.stationsFound.emit(json.dumps([]))
            return
        
        self._lastQuery = query
        
        if self._searchTimer:
            self._searchTimer.stop()
        
        self._searchTimer = QTimer()
        self._searchTimer.setSingleShot(True)
        self._searchTimer.timeout.connect(lambda: self._doStationSearch(query))
        self._searchTimer.start(300)
    
    def _doStationSearch(self, query):
        """Execute station search via HAFAS API (no auth needed!)"""
        if query != self._lastQuery:
            return
        
        def worker():
            try:
                cmd = [PYTHON311, HAFAS_HELPER, "stations", query]
                env = os.environ.copy()
                env['PYTHONIOENCODING'] = 'utf-8'
                
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
                out, err = proc.communicate()
                
                if err:
                    print u"HAFAS Helper stderr: %s" % err[:200]
                
                result = out.decode('utf-8', 'ignore').strip()
                print u"HAFAS station search result: %s" % result[:300]
                
                data = json.loads(result)
                stations = []
                
                if data.get('success'):
                    for s in data.get('stations', []):
                        name = s.get('name', '')
                        if name:
                            # Extract station number from extId if available
                            ext_id = s.get('extId', s.get('id', ''))
                            number = 0
                            import re
                            m = re.search(r'@L=0*(\d+)@', ext_id)
                            if m:
                                number = int(m.group(1))
                            
                            stations.append({
                                'name': name,
                                'id': s.get('id', ''),
                                'extId': ext_id,
                                'number': number,
                                'meta': name,
                                'longitude': s.get('longitude', 0),
                                'latitude': s.get('latitude', 0)
                            })
                
                self.stationsFound.emit(json.dumps(stations))
                
            except Exception as e:
                print u"Station search error: %s" % e
                self.stationsFound.emit(json.dumps([]))
        
        thread = threading.Thread(target=worker)
        thread.daemon = True
        thread.start()
    
    # ==================== Connection & Offer Search ====================
    
    @Slot(unicode, unicode, unicode, unicode, int, int, unicode, bool)
    @Slot(unicode, unicode, unicode, unicode, int, int, unicode, bool, bool)
    def searchConnections(self, fromStation, toStation, date_str, time_str, adults, children, passengerIdsJson="[]", bikesFilter=False, reservationOnly=False):
        """Search for connections using Web API (requires auth) or HAFAS fallback
        
        Args:
            bikesFilter: If True, only show connections that allow bike transport
            reservationOnly: If True, search for reservation-only (no ticket)
        """
        print u"=== SEARCH CONNECTIONS ==="
        print u"From: %s, To: %s" % (fromStation, toStation)
        print u"Date: %s, Time: %s" % (date_str, time_str)
        print u"Bikes filter: %s, ReservationOnly: %s" % (bikesFilter, reservationOnly)
        
        # Store for later use
        self._bikesFilter = bikesFilter
        self._reservationOnlyMode = reservationOnly
        
        # Parse passenger IDs
        try:
            selected_ids = json.loads(passengerIdsJson) if passengerIdsJson else []
        except:
            selected_ids = []
        print u"Selected passenger IDs: %s" % selected_ids
        
        if not fromStation or not toStation:
            self.errorOccurred.emit(u"Start und Ziel erforderlich")
            return
        
        # Parse from/to station info
        try:
            from_data = json.loads(fromStation) if fromStation.startswith('{') else {'name': fromStation, 'extId': fromStation}
            to_data = json.loads(toStation) if toStation.startswith('{') else {'name': toStation, 'extId': toStation}
        except:
            from_data = {'name': fromStation, 'extId': fromStation}
            to_data = {'name': toStation, 'extId': toStation}
        
        # Build datetime
        try:
            if date_str and time_str:
                dt = datetime.strptime("%s %s" % (date_str, time_str), "%d.%m.%Y %H:%M")
            else:
                dt = datetime.now()
        except:
            dt = datetime.now()
        
        # Use Web API if logged in (better results with prices)
        if self._accessToken:
            print u"Using Web API for connection search (logged in)"
            self._searchViaTravelActions(from_data, to_data, dt, adults, children, selected_ids, bikesFilter)
        else:
            # Fallback to HAFAS (no auth needed but no prices)
            print u"Using HAFAS for connection search (not logged in)"
            self._searchViaHafasHelper(from_data, to_data, dt, adults, children)
    
    def _searchViaHafasHelper(self, from_data, to_data, dt, adults, children):
        """Search connections via HAFAS helper (no auth needed!)"""
        print u"=== HAFAS CONNECTION SEARCH ==="
        
        from_id = from_data.get('extId', from_data.get('id', from_data.get('name', '')))
        to_id = to_data.get('extId', to_data.get('id', to_data.get('name', '')))
        date_str = dt.strftime("%d.%m.%Y")
        time_str = dt.strftime("%H:%M")
        
        def worker():
            try:
                cmd = [PYTHON311, HAFAS_HELPER, "connections", from_id, to_id, date_str, time_str]
                env = os.environ.copy()
                env['PYTHONIOENCODING'] = 'utf-8'
                
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
                out, err = proc.communicate()
                
                if err:
                    print u"HAFAS Helper stderr: %s" % err[:200]
                
                result = out.decode('utf-8', 'ignore').strip()
                print u"HAFAS connection result: %s" % result[:500]
                
                data = json.loads(result)
                
                if data.get('success'):
                    connections = []
                    for conn in data.get('connections', []):
                        connections.append({
                            'id': conn.get('id', ''),
                            'departure': conn.get('departure', ''),
                            'arrival': conn.get('arrival', ''),
                            'duration': conn.get('duration', ''),
                            'changes': conn.get('changes', 0),
                            'products': conn.get('products', []),
                            'sections': conn.get('sections', []),
                            'price': u'Preis anfragen' if self._accessToken else u'Einloggen für Preis',
                            'priceValue': 0
                        })
                    self.connectionsFound.emit(json.dumps(connections))
                else:
                    print u"HAFAS search failed: %s" % data.get('error', 'Unknown error')
                    self.connectionsFound.emit(json.dumps([]))
                    
            except Exception as e:
                print u"HAFAS connection search error: %s" % e
                self.connectionsFound.emit(json.dumps([]))
        
        thread = threading.Thread(target=worker)
        thread.daemon = True
        thread.start()
    
    def _searchViaTravelActions(self, from_data, to_data, dt, adults, children, selected_ids=None, bikesFilter=False):
        """Search via travelActions API then timetable (from HAR analysis)"""
        print u"=== WEB API CONNECTION SEARCH ==="
        if selected_ids is None:
            selected_ids = []
        
        # Store bikesFilter for timetable call
        self._currentBikesFilter = bikesFilter
        
        headers = self._getWebApiHeaders(contentType='application/json')
        
        # Extract station number from various formats
        def get_station_number(data):
            # First try 'number' field directly
            if data.get('number'):
                return int(data['number'])
            # Try to extract from extId (format: ...@L=001292204@...)
            ext_id = data.get('extId', data.get('id', ''))
            import re
            m = re.search(r'@L=0*(\d+)@', ext_id)
            if m:
                return int(m.group(1))
            # Try direct id if numeric
            id_val = data.get('id', '')
            if isinstance(id_val, int):
                return id_val
            if isinstance(id_val, str) and id_val.isdigit():
                return int(id_val)
            return 0
        
        from_number = get_station_number(from_data)
        to_number = get_station_number(to_data)
        from_meta = from_data.get('meta', from_data.get('name', ''))
        to_meta = to_data.get('meta', to_data.get('name', ''))
        
        print u"From: %s (number=%d)" % (from_meta, from_number)
        print u"To: %s (number=%d)" % (to_meta, to_number)
        
        # From HAR: travelActions request
        request_data = {
            "departureTime": True,
            "from": {
                "number": from_number,
                "longitude": from_data.get('longitude', 0),
                "latitude": from_data.get('latitude', 0),
                "name": from_data.get('name', ''),
                "meta": from_meta
            },
            "to": {
                "number": to_number,
                "longitude": to_data.get('longitude', 0),
                "latitude": to_data.get('latitude', 0),
                "name": to_data.get('name', ''),
                "meta": to_meta
            },
            "datetime": dt.strftime("%Y-%m-%dT%H:%M:%S.000"),
            "filter": {
                "productTypes": [],
                "history": False,
                "maxEntries": 10,
                "channel": "inet"
            }
        }
        
        # For reservation-only search
        if self._reservationOnlyMode:
            request_data["travelActionTypes"] = ["reservation_only"]
            request_data["customerVias"] = []
            print u"Using reservation_only travelAction type"
        
        def onResult(result):
            try:
                print u"TravelActions result: %s" % result[:500]
                data = json.loads(result)
                
                if 'error' in data:
                    print u"TravelActions error, falling back to HAFAS"
                    self._searchViaHafasHelper(from_data, to_data, dt, adults, children)
                    return
                
                # Process travelActions response
                actions = data.get('travelActions', [])
                if actions:
                    # Get timetable for first action
                    action_id = actions[0].get('id', '')
                    print u"Got travelActionId: %s" % action_id[:50]
                    if action_id:
                        # Pass station data to timetable
                        from_station = {
                            "latitude": from_data.get('latitude', 0),
                            "longitude": from_data.get('longitude', 0),
                            "name": from_meta,
                            "number": from_number
                        }
                        to_station = {
                            "latitude": to_data.get('latitude', 0),
                            "longitude": to_data.get('longitude', 0),
                            "name": to_meta,
                            "number": to_number
                        }
                        self._getTimetable(action_id, dt, adults, children, from_station, to_station, selected_ids)
                    else:
                        print u"No action ID, falling back to HAFAS"
                        self._searchViaHafasHelper(from_data, to_data, dt, adults, children)
                else:
                    print u"No travelActions, falling back to HAFAS"
                    self._searchViaHafasHelper(from_data, to_data, dt, adults, children)
                    
            except Exception as e:
                print u"TravelActions error: %s" % e
                self._searchViaHafasHelper(from_data, to_data, dt, adults, children)
        
        self._startRequest("travelactions_search", "POST", TRAVEL_ACTIONS_URL,
                          json.dumps(request_data), headers=headers, callback=onResult)
    
    def _searchTravelActionsOld(self, from_data, to_data, dt, adults, children):
        """Search via travelActions API (from HAR)"""
        headers = self._getWebApiHeaders(contentType='application/json')
        
        # From HAR: travelActions request for connection search
        request_data = {
            "departureTime": True,
            "from": {
                "number": int(from_data.get('id', 0)) if from_data.get('id', '').isdigit() else 0,
                "longitude": from_data.get('longitude', 0),
                "latitude": from_data.get('latitude', 0),
                "name": "",
                "meta": from_data.get('meta', from_data.get('name', ''))
            },
            "to": {
                "meta": to_data.get('meta', to_data.get('name', '')),
                "longitude": to_data.get('longitude', 0),
                "latitude": to_data.get('latitude', 0),
                "number": int(to_data.get('id', 0)) if to_data.get('id', '').isdigit() else 0,
                "name": ""
            },
            "datetime": dt.strftime("%Y-%m-%dT%H:%M:%S.000"),
            "passengers": [{
                "type": "ADULT",
                "birthdate": None,
                "discount_cards": []
            } for _ in range(adults)] + [{
                "type": "CHILD",
                "birthdate": None,
                "discount_cards": []
            } for _ in range(children)],
            "filter": {
                "productTypes": [],
                "history": False,
                "maxEntries": 10,
                "channel": "inet"
            }
        }
        
        def onResult(result):
            try:
                print u"TravelActions search result: %s" % result[:500]
                data = json.loads(result)
                
                if 'error' in data:
                    self._searchViaHafas(from_data, to_data, dt, adults, children)
                    return
                
                # Process travelActions response
                actions = data.get('travelActions', [])
                if actions:
                    # Get timetable for first action
                    action_id = actions[0].get('id', '')
                    if action_id:
                        self._getTimetable(action_id, dt, adults, children, bikesFilter=getattr(self, '_currentBikesFilter', False))
                    else:
                        self._searchViaHafas(from_data, to_data, dt, adults, children)
                else:
                    self._searchViaHafas(from_data, to_data, dt, adults, children)
                    
            except Exception as e:
                print u"TravelActions error: %s" % e
                self._searchViaHafas(from_data, to_data, dt, adults, children)
        
        self._startRequest("travelactions_search", "POST", TRAVEL_ACTIONS_URL,
                          json.dumps(request_data), headers=headers, callback=onResult)
    
    def _getTimetable(self, travelActionId, dt, adults, children, from_station=None, to_station=None, selected_ids=None, bikesFilter=False):
        """Get timetable for travel action (from HAR) - requires passengers and stations
        
        Args:
            bikesFilter: If True, only show connections with bike transport
        """
        headers = self._getWebApiHeaders(contentType='application/json')
        
        if selected_ids is None:
            selected_ids = []
        
        # Build passengers array based on selected IDs
        passengers = []
        
        def format_passenger(p):
            """Format a passenger for the API request"""
            # Calculate age
            age = 30
            if p.get('birthDate'):
                try:
                    bd = datetime.strptime(p['birthDate'], '%Y-%m-%d')
                    today = datetime.now()
                    age = today.year - bd.year - ((today.month, today.day) < (bd.month, bd.day))
                except:
                    pass
            
            # Format cards with proper structure
            cards = []
            for c in p.get('cards', []):
                card = dict(c)  # Copy the card data
                card['isSelected'] = False
                card['isValidated'] = False
                if 'person' not in card:
                    card['person'] = {}
                cards.append(card)
            
            return {
                "me": p.get('me', False),
                "remembered": p.get('remembered', True),
                "markedForDeath": False,
                "challengedFlags": p.get('challengedFlags', {
                    "hasHandicappedPass": False,
                    "hasAssistanceDog": False,
                    "hasWheelchair": False,
                    "hasAttendant": False
                }),
                "cards": cards,
                "relations": p.get('relations', []),
                "age": age,
                "id": p.get('id', 1),
                "type": p.get('type', 'ADULT'),
                "kdbId": p.get('kdbId', 0),
                "isPersonalFavourite": p.get('isPersonalFavourite', False),
                "note1": p.get('note1'),
                "numberOfAttendants": p.get('numberOfAttendants'),
                "groupSize": p.get('groupSize'),
                "atFbgType": p.get('atFbgType'),
                "atFbgPassengerIndex": p.get('atFbgPassengerIndex'),
                "firstName": p.get('firstName', ''),
                "lastName": p.get('lastName', ''),
                "colorId": p.get('colorId', 'person1'),
                "personId": p.get('personId', ''),
                "schoolcardGroup": p.get('schoolcardGroup', {"groupSize": None, "maxAttendants": None}),
                "birthDate": p.get('birthDate', ''),
                "customAttributes": p.get('customAttributes', []),
                "birthdateChangeable": p.get('birthdateChangeable', True),
                "birthdateDeletable": p.get('birthdateDeletable', True),
                "nameChangeable": p.get('nameChangeable', True),
                "passengerDeletable": p.get('passengerDeletable', True)
            }
        
        if self._passengers and selected_ids:
            # Use selected passengers
            for p in self._passengers:
                if p.get('id') in selected_ids:
                    passengers.append(format_passenger(p))
                    print u"Added selected passenger: %s %s (ID=%s)" % (p.get('firstName', ''), p.get('lastName', ''), p.get('id'))
        
        if not passengers and self._passengers:
            # Fallback: Use "me" passenger if no selection
            for p in self._passengers:
                if p.get('me'):
                    passengers.append(format_passenger(p))
                    print u"Using default 'me' passenger: %s %s" % (p.get('firstName', ''), p.get('lastName', ''))
                    break
        
        # Add extra adults (without discount cards)
        base_id = 1000
        for i in range(adults):
            passengers.append({
                "me": False,
                "remembered": False,
                "markedForDeath": False,
                "challengedFlags": {
                    "hasHandicappedPass": False,
                    "hasAssistanceDog": False,
                    "hasWheelchair": False,
                    "hasAttendant": False
                },
                "cards": [],
                "relations": [],
                "age": 30,
                "id": base_id + i,
                "type": "ADULT",
                "firstName": "",
                "lastName": "",
                "birthDate": ""
            })
            print u"Added extra adult #%d" % (i + 1)
        
        # Add extra children (without discount cards)
        for i in range(children):
            passengers.append({
                "me": False,
                "remembered": False,
                "markedForDeath": False,
                "challengedFlags": {
                    "hasHandicappedPass": False,
                    "hasAssistanceDog": False,
                    "hasWheelchair": False,
                    "hasAttendant": False
                },
                "cards": [],
                "relations": [],
                "age": 10,
                "id": base_id + adults + i,
                "type": "CHILD",
                "firstName": "",
                "lastName": "",
                "birthDate": ""
            })
            print u"Added extra child #%d" % (i + 1)
        
        # If still no passengers, create at least one adult
        if not passengers:
            for i in range(max(1, adults)):
                passengers.append({
                    "me": (i == 0),
                    "remembered": False,
                    "markedForDeath": False,
                    "challengedFlags": {
                        "hasHandicappedPass": False,
                        "hasAssistanceDog": False,
                        "hasWheelchair": False,
                        "hasAttendant": False
                    },
                    "cards": [],
                    "relations": [],
                    "age": 30,
                    "id": i + 1,
                    "type": "ADULT",
                    "firstName": "",
                    "lastName": "",
                    "birthDate": ""
                })
        
        # From HAR: timetable request with full structure
        request_data = {
            "travelActionId": travelActionId,
            "datetimeDeparture": dt.strftime("%Y-%m-%dT%H:%M:%S.000"),
            "filter": {
                "regionaltrains": False,
                "direct": False,
                "wheelchair": False,
                "bikes": bikesFilter,  # Filter for bike-capable connections
                "trains": False,
                "motorail": False,
                "connections": []
            },
            "passengers": passengers,
            "entryPointId": "timetable",
            "count": 5,
            "debugFilter": {
                "noAggregationFilter": False,
                "noEqclassFilter": False,
                "noNrtpathFilter": False,
                "noPaymentFilter": False,
                "useTripartFilter": False,
                "noVbxFilter": False,
                "noCategoriesFilter": False
            },
            "sortType": "DEPARTURE"
        }
        
        # Add from/to stations if provided
        if from_station:
            request_data["from"] = from_station
        if to_station:
            request_data["to"] = to_station
        
        print u"Timetable request with %d passengers" % len(passengers)
        for p in passengers:
            print u"  - %s %s (%s)" % (p.get('firstName', '?'), p.get('lastName', '?'), p.get('type', '?'))
        if from_station:
            print u"From: %s (number=%d)" % (from_station.get('name', ''), from_station.get('number', 0))
        if to_station:
            print u"To: %s (number=%d)" % (to_station.get('name', ''), to_station.get('number', 0))
        
        def onResult(result):
            try:
                print u"Timetable result: %s" % result[:500]
                data = json.loads(result)
                
                if 'error' in data:
                    print u"Timetable error: %s" % data.get('error', {}).get('message', 'Unknown')
                    self.connectionsFound.emit(json.dumps([]))
                    return
                
                connections = data.get('connections', [])
                if connections:
                    # Get prices for connections
                    conn_ids = [c.get('id', '') for c in connections if c.get('id')]
                    if conn_ids:
                        self._getPrices(conn_ids, connections, adults, children)
                    else:
                        self._formatConnections(connections, {})
                else:
                    print u"No connections found"
                    self.connectionsFound.emit(json.dumps([]))
                    
            except Exception as e:
                print u"Timetable error: %s" % e
                import traceback
                traceback.print_exc()
                self.connectionsFound.emit(json.dumps([]))
        
        self._startRequest("timetable", "POST", TIMETABLE_URL,
                          json.dumps(request_data), headers=headers, callback=onResult)
    
    def _getPrices(self, connectionIds, connections, adults, children):
        """Get prices for connections (from HAR)"""
        headers = self._getWebApiHeaders()
        
        # From HAR: prices endpoint with connectionIds array
        params = "&".join(["connectionIds%%5B%%5D=%s" % cid for cid in connectionIds[:5]])
        url = PRICES_URL + "?" + params
        
        print u"Requesting prices for %d connections" % len(connectionIds)
        
        def onResult(result):
            try:
                print u"Prices result: %s" % result[:500]
                data = json.loads(result)
                prices = {}
                
                # Map prices to connection IDs - API returns 'offers' array
                offers = data.get('offers', [])
                if not offers:
                    offers = data.get('prices', [])
                if isinstance(data, list):
                    offers = data
                
                print u"Found %d offers in response" % len(offers)
                
                for p in offers:
                    cid = p.get('connectionId', '')
                    price = p.get('price', 0)
                    if cid:
                        prices[cid] = p
                        print u"  Price for %s...: EUR %.2f" % (cid[:30], price)
                
                print u"Mapped prices for %d connections" % len(prices)
                self._formatConnections(connections, prices)
                
            except Exception as e:
                print u"Prices error: %s" % e
                import traceback
                traceback.print_exc()
                self._formatConnections(connections, {})
        
        self._startRequest("prices", "GET", url, headers=headers, callback=onResult)
    
    def _formatConnections(self, connections, prices):
        """Format connections for display"""
        results = []
        
        print u"_formatConnections called with %d connections and %d prices" % (len(connections), len(prices))
        
        for conn in connections:
            conn_id = conn.get('id', '')
            
            # Extract from/to data - API returns nested structure
            from_data = conn.get('from', {})
            to_data = conn.get('to', {})
            
            # Extract times from ISO format (2025-12-19T10:04:00.000)
            dep_time = from_data.get('departure', '')
            arr_time = to_data.get('arrival', '')
            
            # Format times to HH:MM and extract dates
            dep_formatted = ''
            arr_formatted = ''
            dep_date = ''
            arr_date = ''
            if dep_time and 'T' in dep_time:
                dep_formatted = dep_time.split('T')[1][:5]  # "10:04"
                dep_date = dep_time.split('T')[0]  # "2025-12-29"
            if arr_time and 'T' in arr_time:
                arr_formatted = arr_time.split('T')[1][:5]  # "10:48"
                arr_date = arr_time.split('T')[0]  # "2025-12-30"
            
            # Format dates for display (DD.MM.)
            dep_date_display = ''
            arr_date_display = ''
            if dep_date:
                try:
                    parts = dep_date.split('-')
                    dep_date_display = "%s.%s." % (parts[2], parts[1])  # "29.12."
                except:
                    pass
            if arr_date:
                try:
                    parts = arr_date.split('-')
                    arr_date_display = "%s.%s." % (parts[2], parts[1])  # "30.12."
                except:
                    pass
            
            # Check if arrival is next day
            is_overnight = dep_date != arr_date
            
            # Calculate duration from sections or estimate from times
            duration_str = ''
            sections = conn.get('sections', [])
            if sections:
                # Sum up section durations (in milliseconds)
                total_ms = sum(s.get('duration', 0) for s in sections)
                if total_ms > 0:
                    total_min = total_ms // 60000
                    hours = total_min // 60
                    mins = total_min % 60
                    if hours > 0:
                        duration_str = "%d:%02d" % (hours, mins)
                    else:
                        duration_str = "%d min" % mins
            
            # Count transfers (sections - 1, but only train sections)
            changes = max(0, len([s for s in sections if s.get('category', {}).get('train', False)]) - 1)
            
            # Extract product names (S80, REX, etc.)
            products = []
            for s in sections:
                cat = s.get('category', {})
                name = cat.get('displayName', cat.get('name', ''))
                if name and name not in products:
                    products.append(name)
            
            # Format legs with transfer times
            legs = []
            for i, s in enumerate(sections):
                cat = s.get('category', {})
                s_from = s.get('from', {})
                s_to = s.get('to', {})
                
                # Get departure/arrival times
                s_dep = s_from.get('departure', '')
                s_arr = s_to.get('arrival', '')
                
                # Extract intermediate stops (try multiple field names)
                stops = []
                passing_points = s.get('passingPoints', s.get('stops', s.get('passlist', s.get('intermediateStops', []))))
                
                # Debug: Show section keys to find stops field
                if i == 0:
                    print u"  Section keys: %s" % s.keys()
                    if passing_points:
                        print u"  Found %d passing points" % len(passing_points)
                    else:
                        # Check for nested structure
                        for key in ['journey', 'jny', 'train', 'trainInfo']:
                            if key in s:
                                sub = s[key]
                                if isinstance(sub, dict):
                                    print u"    %s keys: %s" % (key, sub.keys())
                                    for subkey in ['stops', 'passingPoints', 'passlist']:
                                        if subkey in sub:
                                            passing_points = sub[subkey]
                                            print u"    Found stops in %s.%s: %d" % (key, subkey, len(passing_points))
                                            break
                
                for pp in passing_points:
                    stop_name = pp.get('name', '')
                    stop_arr = pp.get('arrival', '')
                    stop_dep = pp.get('departure', '')
                    # Use arrival or departure time
                    stop_time = stop_arr or stop_dep
                    if stop_time and 'T' in stop_time:
                        stop_time = stop_time.split('T')[1][:5]
                    else:
                        stop_time = ''
                    if stop_name:
                        stops.append({
                            'name': stop_name,
                            'time': stop_time,
                            'platform': pp.get('platform', pp.get('arrivalPlatform', pp.get('departurePlatform', '')))
                        })
                
                leg = {
                    'depTime': s_dep.split('T')[1][:5] if s_dep and 'T' in s_dep else '',
                    'arrTime': s_arr.split('T')[1][:5] if s_arr and 'T' in s_arr else '',
                    'fromStation': s_from.get('name', ''),
                    'toStation': s_to.get('name', ''),
                    'depPlatform': s_from.get('departurePlatform', ''),
                    'arrPlatform': s_to.get('arrivalPlatform', ''),
                    'product': cat.get('displayName', cat.get('name', 'Zug')),
                    'direction': cat.get('direction', ''),
                    'number': cat.get('number', ''),
                    'isWalk': s.get('type') == 'walk' or cat.get('iconId') == 'walk',
                    'isTrain': cat.get('train', False),
                    'stops': stops  # Intermediate stops
                }
                
                # Calculate transfer time to previous leg
                if i > 0 and legs:
                    prev_arr = sections[i-1].get('to', {}).get('arrival', '')
                    curr_dep = s_dep
                    if prev_arr and curr_dep and 'T' in prev_arr and 'T' in curr_dep:
                        try:
                            prev_time = datetime.strptime(prev_arr.split('.')[0], "%Y-%m-%dT%H:%M:%S")
                            curr_time = datetime.strptime(curr_dep.split('.')[0], "%Y-%m-%dT%H:%M:%S")
                            wait_min = int((curr_time - prev_time).total_seconds() / 60)
                            if wait_min > 0:
                                leg['waitTime'] = wait_min
                                leg['waitTimeStr'] = "%d min Umstieg" % wait_min
                        except:
                            pass
                
                legs.append(leg)
            
            # Get price - API returns price in Euro (1.9 = €1.90), NOT cents!
            price_info = prices.get(conn_id, {})
            price_val = price_info.get('price', 0)
            
            # Debug: show price lookup
            if conn_id:
                print u"  Connection %s...: price_val=%.2f" % (conn_id[:30], price_val)
            
            result = {
                'id': conn_id,
                'departure': dep_formatted,
                'arrival': arr_formatted,
                'departureDate': dep_date_display,
                'arrivalDate': arr_date_display,
                'isOvernight': is_overnight,
                'duration': duration_str,
                'changes': changes,
                'products': products,
                'price': u"€ %.2f" % price_val if price_val else u"Preis anfragen",
                'priceValue': int(price_val * 100) if price_val else 0,  # Store in cents
                'fromStation': from_data.get('name', ''),
                'toStation': to_data.get('name', ''),
                'departurePlatform': from_data.get('departurePlatform', ''),
                'arrivalPlatform': to_data.get('arrivalPlatform', ''),
                'legs': legs
            }
            results.append(result)
        
        print u"Formatted %d connections" % len(results)
        if results:
            print u"First: %s -> %s, Price: %s" % (results[0]['departure'], results[0]['arrival'], results[0]['price'])
        
        self.connectionsFound.emit(json.dumps(results))
    
    def _searchViaHafas(self, from_data, to_data, dt, adults, children):
        """Fallback search via HAFAS XML API"""
        print u"=== HAFAS FALLBACK SEARCH ==="
        
        # Build HAFAS XML request
        xml = '<?xml version="1.0" encoding="UTF-8"?>'
        xml += '<ReqC ver="1.1" prod="oebbIPHONE/5.60.0" lang="DE">'
        xml += '<ConReq>'
        xml += '<Start>'
        xml += '<Station name="%s" />' % from_data.get('name', '')
        xml += '<Prod prod="1023" direct="0" sleeper="0" couchette="0" bike="0" />'
        xml += '</Start>'
        xml += '<Dest>'
        xml += '<Station name="%s" />' % to_data.get('name', '')
        xml += '</Dest>'
        xml += '<ReqT date="%s" time="%s" a="0" />' % (dt.strftime("%Y%m%d"), dt.strftime("%H:%M"))
        xml += '<RFlags b="0" f="%d" sMode="N" />' % (adults + children)
        xml += '</ConReq>'
        xml += '</ReqC>'
        
        headers = {
            'Content-Type': 'application/xml; charset=utf-8',
            'Accept': '*/*',
            'User-Agent': 'OeBBIPHONE/5.60.0 (iPhone; iOS 15.0; Scale/3.00)'
        }
        
        def onResult(result):
            try:
                # Parse HAFAS XML response
                connections = self._parseHafasResponse(result)
                self.connectionsFound.emit(json.dumps(connections))
            except Exception as e:
                print u"HAFAS error: %s" % e
                self.connectionsFound.emit(json.dumps([]))
        
        self._startRequest("hafas", "POST", HAFAS_URL + "/dn", xml, headers, callback=onResult)
    
    def _parseHafasResponse(self, xml_data):
        """Parse HAFAS XML response"""
        connections = []
        
        try:
            import re
            
            # Find all Connection blocks
            conn_blocks = re.findall(r'<Connection[^>]*>(.*?)</Connection>', xml_data, re.DOTALL)
            
            for i, block in enumerate(conn_blocks):
                # Extract departure/arrival
                dep_match = re.search(r'<Dep[^>]*><Time>(\d{2}:\d{2})', block)
                arr_match = re.search(r'<Arr[^>]*><Time>(\d{2}:\d{2})', block)
                dur_match = re.search(r'<Duration[^>]*>(\d{2}:\d{2})', block)
                
                # Count changes (ConSection blocks - 1)
                sections = re.findall(r'<ConSection', block)
                changes = max(0, len(sections) - 1)
                
                # Get products
                products = re.findall(r'<Prod[^>]*name="([^"]+)"', block)
                
                conn = {
                    'id': 'hafas_%d' % i,
                    'departure': dep_match.group(1) if dep_match else '',
                    'arrival': arr_match.group(1) if arr_match else '',
                    'duration': dur_match.group(1) if dur_match else '',
                    'changes': changes,
                    'products': products[:3],
                    'price': u'Online kaufen',
                    'priceValue': 0
                }
                connections.append(conn)
                
        except Exception as e:
            print u"HAFAS parse error: %s" % e
        
        return connections
    
    # ==================== Favorites ====================
    
    @Slot(result=unicode)
    def getFavorites(self):
        """Get saved favorites"""
        try:
            if os.path.exists(FAVORITES_FILE):
                with open(FAVORITES_FILE, 'r') as f:
                    return json.dumps(json.load(f))
        except:
            pass
        return json.dumps([])
    
    @Slot(unicode, unicode)
    def addFavorite(self, fromStation, toStation):
        """Add a favorite route"""
        try:
            favorites = []
            if os.path.exists(FAVORITES_FILE):
                with open(FAVORITES_FILE, 'r') as f:
                    favorites = json.load(f)
            
            for fav in favorites:
                if fav.get('from') == fromStation and fav.get('to') == toStation:
                    return
            
            favorites.append({
                'from': fromStation,
                'to': toStation
            })
            
            ensure_config_dir()
            with open(FAVORITES_FILE, 'w') as f:
                json.dump(favorites, f)
        except Exception as e:
            print u"Error saving favorite: %s" % e
    
    @Slot(int)
    def removeFavorite(self, index):
        """Remove a favorite"""
        try:
            if os.path.exists(FAVORITES_FILE):
                with open(FAVORITES_FILE, 'r') as f:
                    favorites = json.load(f)
                
                if 0 <= index < len(favorites):
                    favorites.pop(index)
                    with open(FAVORITES_FILE, 'w') as f:
                        json.dump(favorites, f)
        except:
            pass
    
    # ==================== Utilities ====================
    
    @Slot(result=unicode)
    def getCurrentDate(self):
        """Get current date as DD.MM.YYYY"""
        return datetime.now().strftime("%d.%m.%Y")
    
    @Slot(result=unicode)
    def getCurrentTime(self):
        """Get current time as HH:MM"""
        return datetime.now().strftime("%H:%M")
    
    @Slot(result=unicode)
    def getVersion(self):
        """Get app version"""
        return u"3.10.8"
    
    @Slot(unicode)
    def openInBrowser(self, url):
        """Open URL in external browser"""
        print u"=== OPENING BROWSER ==="
        print u"URL: %s" % url[:100]
        
        methods = [
            lambda: subprocess.Popen(['xdg-open', url]),
            lambda: subprocess.Popen(['grob', url]),
            lambda: subprocess.Popen(['invoker', '--type=m', '/usr/bin/grob', url]),
            lambda: subprocess.Popen([
                'dbus-send', '--session', '--type=method_call',
                '--dest=com.nokia.osso_browser',
                '/com/nokia/osso_browser/request',
                'com.nokia.osso_browser.open_new_window',
                'string:' + url
            ]),
        ]
        
        for i, method in enumerate(methods):
            try:
                print u"Trying method %d..." % (i + 1)
                method()
                print u"Method %d succeeded" % (i + 1)
                return
            except Exception as e:
                print u"Method %d failed: %s" % (i + 1, e)
        
        print u"All browser methods failed!"


def main():
    app = QApplication(sys.argv)
    app.setApplicationName("OeBB Tickets")
    
    controller = OEBBController()
    
    view = QDeclarativeView()
    view.setResizeMode(QDeclarativeView.SizeRootObjectToView)
    
    context = view.rootContext()
    context.setContextProperty("oebb", controller)
    
    qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "qml", "main.qml")
    view.setSource(QUrl.fromLocalFile(qml_path))
    
    view.showFullScreen()
    
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
