#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Bangladesh Bank EXP Search Automation (Commercial RPA Edition)
Author: Izaz Ahamed
Platform: Commercial RPA
---------------------------------------------------------------------------------
Workflow:
1️⃣ Login with Bangladesh Bank credentials (manual login through browser)
2️⃣ Navigate to Search EXP Detail Information page
3️⃣ Search for each EXP record from CSV
4️⃣ Download PDF for each record
5️⃣ Rename downloaded files with meaningful names
---------------------------------------------------------------------------------
Usage:
  python bb_exp_search.py <csv_file> <output_dir> <job_id>
"""

import csv
import time
import os
import glob
import sys
import datetime
import logging
import re
import json
import shutil
import time
import requests
import subprocess as _unused_subprocess  # unused (legacy Chrome helpers removed)
# Use Electron browser instead of Selenium
try:
    from shared.electron_browser import ElectronBrowser, By, ElectronWait, EC
    from shared.server_client import ServerClient
except ImportError:
    # Fallback for direct execution
    import sys
    import os
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    try:
        from automation_scripts.shared.electron_browser import ElectronBrowser, By, ElectronWait, EC
        from automation_scripts.shared.server_client import ServerClient
    except ImportError:
        from shared.electron_browser import ElectronBrowser, By, ElectronWait, EC
        from shared.server_client import ServerClient

# Compatibility exceptions
class TimeoutException(Exception):
    pass

class NoSuchElementException(Exception):
    pass

class StaleElementReferenceException(Exception):
    pass
from functools import wraps

# Windows registry access for Chrome version detection (legacy disabled)
HAS_WINREG = False

# ------------------- Argument validation -------------------
# CLI mode - require 3 arguments (CSV_FILE, OUTPUT_DIR, JOB_ID)
# Optional 4th and 5th arguments: username, password for automated login
if len(sys.argv) < 4:
    print("=" * 80)
    print("❌ ERROR: Missing Required Arguments")
    print("=" * 80)
    print("Usage: python bb_exp_search.py <csv_file> <output_dir> <job_id> [username] [password]")
    print("")
    print("Optional arguments:")
    print("  username: Portal username for automated login")
    print("  password: Portal password for automated login")
    print("")
    print("If username/password are not provided, manual login will be required.")
    print("")
    print("=" * 80)
    sys.exit(1)

# CLI mode - use command-line arguments
CSV_FILE = sys.argv[1]
BASE_OUTPUT_DIR = sys.argv[2]
JOB_ID = sys.argv[3]
# Optional credentials for automated login
PORTAL_USERNAME = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else os.environ.get('BB_EXP_USERNAME')
PORTAL_PASSWORD = sys.argv[5] if len(sys.argv) > 5 and sys.argv[5] else os.environ.get('BB_EXP_PASSWORD')

# Validate CSV file exists and has correct extension
if not os.path.exists(CSV_FILE):
    print("=" * 80)
    print("❌ ERROR: CSV File Not Found")
    print("=" * 80)
    print(f"📁 File provided: {CSV_FILE}")
    print("")
    print("ℹ️  The CSV file does not exist at the specified path.")
    print("   Please check the file path and try again.")
    print("")
    print("=" * 80)
    sys.exit(1)

# Validate CSV file extension
if not CSV_FILE.lower().endswith('.csv'):
    print("=" * 80)
    print("⚠️  WARNING: Invalid File Type")
    print("=" * 80)
    print(f"📁 File provided: {CSV_FILE}")
    print("")
    print("ℹ️  This automation only supports CSV (Comma-Separated Values) files.")
    print("   Please ensure your input file has a .csv extension.")
    print("")
    print("=" * 80)
    sys.exit(1)

# Use isolated service-specific output directory provided by the server
OUTPUT_DIR = os.path.abspath(BASE_OUTPUT_DIR)
os.makedirs(OUTPUT_DIR, exist_ok=True)

# Configuration
LOGIN_URL = "https://exp.bb.org.bd/ords/f?p=112:LOGIN:13736275091712:::::"
# Use OUTPUT_DIR directly instead of creating downloads subfolder
# Convert to absolute path to ensure Chrome can access it properly
DOWNLOAD_DIR = os.path.abspath(OUTPUT_DIR)

# Session directory for per-job session persistence within isolated output directory
SESSION_DIR = os.path.join(OUTPUT_DIR, 'sessions', JOB_ID)
os.makedirs(SESSION_DIR, exist_ok=True)

# Results CSV file path
RESULT_CSV = os.path.join(OUTPUT_DIR, f"exp_results_{JOB_ID}.csv")

# Headless mode configuration
# During login, browser will be visible. After login, can continue in headless mode
HEADLESS_MODE_AFTER_LOGIN = True  # Set to False to keep browser visible during automation
LOGIN_HEADLESS_MODE = True  # Browser must be visible during login for monitoring/debugging

import shutil

# ------------------- Logging setup -------------------
logs_dir = os.path.join(OUTPUT_DIR, 'logs')
os.makedirs(logs_dir, exist_ok=True)
log_file_path = os.path.join(logs_dir, f"bb_exp_search_{JOB_ID}.log")

console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
console_handler.setFormatter(console_formatter)

file_handler = logging.FileHandler(log_file_path, mode='w', encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
file_handler.setFormatter(file_formatter)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[console_handler, file_handler]
)

logging.info("=" * 80)
logging.info("🚀 Bangladesh Bank EXP Search Automation Started")
logging.info("=" * 80)
logging.info(f"📁 Input CSV: {CSV_FILE}")
logging.info(f"📂 Output Directory: {OUTPUT_DIR}")
logging.info(f"🆔 Job ID: {JOB_ID}")
logging.info(f"💾 Session Directory: {SESSION_DIR}")

# DEBUG: Log arguments status
logging.info(f"🔢 Argument Count: {len(sys.argv)}")
logging.info(f"👤 Username provided: {'Yes' if PORTAL_USERNAME else 'No'}")
logging.info(f"🔑 Password provided: {'Yes' if PORTAL_PASSWORD else 'No'}")

logging.info("=" * 80)

# Prepare download preferences for Chrome (will be passed to get_uc_driver)
DOWNLOAD_PREFS = {
    "download.default_directory": DOWNLOAD_DIR,
    "download.prompt_for_download": False,
    "download.directory_upgrade": True,
    "plugins.always_open_pdf_externally": True,  # This will download PDFs instead of viewing
    "profile.default_content_setting_values.automatic_downloads": 1,  # Enable automatic downloads
    "profile.default_content_setting_values.popups": 1,  # Allow popups (needed for downloads)
    "profile.managed_default_content_settings.popups": 1,  # Managed popup settings
    "profile.default_content_settings.popups": 0,
    "profile.content_settings.exceptions.automatic_downloads.*.setting": 1,
    "safebrowsing.enabled": True
}

# Retry decorator for handling slow website responses
def retry_on_failure(max_retries=3, delay=1):
    """Retry decorator for functions that may fail due to slow website"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries - 1:
                        raise
                    logging.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay} seconds...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

def wait_for_element(driver, by, value, timeout=15, retry_count=3):
    """Wait for an element to be present and return it with retry"""
    for attempt in range(retry_count):
        try:
            element = ElectronWait(driver, timeout).until(
                EC.presence_of_element_located((by, value))
            )
            return element
        except (TimeoutException, StaleElementReferenceException) as e:
            if attempt == retry_count - 1:
                logging.warning(f"Timeout waiting for element: {by}={value}")
                return None
            time.sleep(0.5)
    return None

def wait_for_clickable(driver, by, value, timeout=15, retry_count=3):
    """Wait for an element to be clickable and return it with retry"""
    for attempt in range(retry_count):
        try:
            element = ElectronWait(driver, timeout).until(
                EC.element_to_be_clickable((by, value))
            )
            return element
        except (TimeoutException, StaleElementReferenceException) as e:
            if attempt == retry_count - 1:
                logging.warning(f"Timeout waiting for clickable element: {by}={value}")
                return None
            time.sleep(0.5)
    return None

def wait_for_page_load(driver, timeout=10):
    """Wait for page to be ready (not just loaded, but interactive)"""
    try:
        ElectronWait(driver, timeout).until(
            lambda d: d.execute_script("(function(){ return document.readyState; })();") == "complete"
        )
        # Additional wait for dynamic content
        try:
            ElectronWait(driver, timeout).until(
                lambda d: d.execute_script("(function(){ return (typeof jQuery != 'undefined') ? jQuery.active == 0 : true; })();")
            )
        except:
            pass # Ignore jQuery timeout
    except Exception as e:
        logging.warning(f"Page load wait timed out: {e}")

def wait_for_downloaded_file(download_start_time, timeout=30, check_interval=0.5):
    """Wait for exp_exporter.pdf to be downloaded and return its path
    Looks for files created AFTER download_start_time to avoid renaming old files
    
    Args:
        download_start_time: Timestamp when download was initiated (from time.time())
        timeout: Maximum time to wait for download
        check_interval: How often to check for the file
    
    Returns:
        str: Path to the newly downloaded file, or None if not found
    """
    logging.info("Waiting for download to complete...")
    
    # Pattern to match exp_exporter.pdf and its variants
    pattern_base = os.path.join(DOWNLOAD_DIR, "exp_exporter.pdf")
    
    # Wait for a new file to appear
    while time.time() - download_start_time < timeout:
        # Check for exp_exporter.pdf and its numbered variants
        # Chrome pattern: exp_exporter.pdf, exp_exporter (1).pdf, exp_exporter (2).pdf, etc.
        potential_files = []
        
        # Check base filename
        if os.path.exists(pattern_base):
            potential_files.append(pattern_base)
        
        # Check numbered variants (Chrome adds numbers in parentheses)
        # Pattern: exp_exporter (1).pdf, exp_exporter (2).pdf, etc.
        # Check variants efficiently (limit to first 50 to avoid too many checks)
        for i in range(1, 51):
            variant_file = os.path.join(DOWNLOAD_DIR, f"exp_exporter ({i}).pdf")
            if os.path.exists(variant_file):
                potential_files.append(variant_file)
            # Don't break on gaps - continue checking all variants
        
        # Filter files that were created/modified AFTER download_start_time
        new_files = []
        for file_path in potential_files:
            try:
                file_mtime = os.path.getmtime(file_path)
                # File must be newer than download start time (with 2 second buffer for timing)
                if file_mtime >= (download_start_time - 2):
                    new_files.append((file_path, file_mtime))
            except OSError:
                continue
        
        if new_files:
            # Get the most recently modified file
            latest_file, latest_mtime = max(new_files, key=lambda x: x[1])
            
            # Verify file is complete (size stable)
            try:
                size1 = os.path.getsize(latest_file)
                time.sleep(0.5)
                size2 = os.path.getsize(latest_file)
                
                # If file size is stable and file is not empty, download is complete
                if size1 == size2 and size1 > 0:
                    # Wait additional time to ensure Chrome releases the file lock
                    # Check multiple times to ensure file is accessible
                    file_ready = False
                    for _ in range(5):  # Check up to 5 times with 0.5s delay
                        try:
                            # Try to open file to verify it's not locked
                            with open(latest_file, 'rb') as f:
                                f.read(1)  # Read 1 byte to test access
                            file_ready = True
                            break
                        except (OSError, IOError, PermissionError):
                            # File is still locked, wait a bit more
                            time.sleep(0.5)
                            continue
                    
                    if file_ready:
                        logging.info(f"✓ Download completed: {os.path.basename(latest_file)} ({size1} bytes)")
                        return latest_file
                    else:
                        logging.info(f"File {os.path.basename(latest_file)} found but still locked, waiting...")
            except OSError:
                pass
        
        time.sleep(check_interval)
    
    # Fallback: Look for ANY PDF file created after download_start_time
    logging.info("Checking for any recently created PDF files...")
    pdf_files = glob.glob(os.path.join(DOWNLOAD_DIR, "*.pdf"))
    new_pdf_files = []
    for pdf_file in pdf_files:
        try:
            file_mtime = os.path.getmtime(pdf_file)
            if file_mtime >= (download_start_time - 2):
                new_pdf_files.append((pdf_file, file_mtime))
        except OSError:
            continue
    
    if new_pdf_files:
        # Get the most recently modified PDF file
        latest_file, latest_mtime = max(new_pdf_files, key=lambda x: x[1])
        logging.info(f"✓ Found recently created PDF file: {os.path.basename(latest_file)}")
        return latest_file
    
    logging.warning("No newly downloaded PDF file found")
    return None

def verify_login_success(driver):
    """Verify that login was successful"""
    try:
        # Check if we're no longer on login page (URL changed or login elements are gone)
        current_url = driver.current_url
        if "LOGIN" not in current_url.upper():
            return True
        
        # Check if login fields are gone (indicating successful login)
        try:
            driver.find_element(By.ID, "P101_USERNAME")
            return False  # Still on login page
        except NoSuchElementException:
            return True  # Login fields gone, likely logged in
    except:
        return False

def check_session_valid(driver):
    """Check if existing session is still valid
    
    Args:
        driver: Selenium WebDriver instance
        
    Returns:
        bool: True if session is valid, False if login is required
    """
    try:
        logging.info("Checking if existing session is valid...")
        # Navigate to main page (after login, should redirect to dashboard)
        # Try accessing a protected page
        driver.get("https://exp.bb.org.bd/ords/f?p=112:1::::::")
        wait_for_page_load(driver, timeout=5)
        
        # Check if we're redirected to login page
        current_url = driver.current_url
        if "LOGIN" in current_url.upper():
            logging.info("Session invalid - redirected to login page")
            return False
        
        # Check if login form is present
        try:
            driver.find_element(By.ID, "P101_USERNAME")
            logging.info("Session invalid - login form detected")
            return False
        except NoSuchElementException:
            logging.info("✓ Session is valid")
            return True
    except Exception as e:
        logging.warning(f"Error checking session validity: {e}")
        return False

def perform_automated_login(driver, username, password):
    """Perform automated login using provided credentials
    
    Args:
        driver: Selenium WebDriver instance
        username: Portal username
        password: Portal password
        
    Returns:
        str: Session ID extracted from URL after login
        
    Raises:
        Exception: If login fails or session ID cannot be extracted
    """
    try:
        logging.info("Performing automated login...")
        logging.info(f"Username: {username[:3]}***")  # Log partial username for security
        
        # Navigate to login page
        driver.get(LOGIN_URL)
        wait_for_page_load(driver, timeout=10)
        
        # Wait for login form to be present
        logging.info("Waiting for login form...")
        username_field = ElectronWait(driver, 10).until(
            EC.presence_of_element_located((By.ID, "P101_USERNAME"))
        )
        password_field = driver.find_element(By.ID, "P101_PASSWORD")
        
        # Fill in credentials
        logging.info("Filling in credentials...")
        username_field.clear()
        username_field.send_keys(username)
        time.sleep(0.5)  # Small delay between fields
        
        password_field.clear()
        password_field.send_keys(password)
        time.sleep(0.5)
        
        # Find and click login button
        login_button = None
        button_selectors = [
            (By.ID, "P101_LOGIN"),
            (By.CSS_SELECTOR, "button[type='submit']"),
            (By.CSS_SELECTOR, "input[type='submit']"),
            (By.CSS_SELECTOR, "a.t12Button"),
            (By.XPATH, "//button[contains(text(), 'Login')]"),
            (By.XPATH, "//input[@value='Login']"),
            (By.XPATH, "//a[contains(text(), 'Login')]")
        ]

        for selector_type, selector_value in button_selectors:
            try:
                login_button = driver.find_element(selector_type, selector_value)
                break
            except Exception:
                continue

        if login_button:
            logging.info(f"Clicking login button (found via {login_button.tag_name})...")
            try:
                login_button.click()
            except Exception as e:
                logging.warning(f"Standard click failed ({e}), trying JavaScript click...")
                driver.execute_script("arguments[0].click();", login_button)
        else:
            logging.info("Login button not found via selectors, trying apex.submit...")
            try:
                driver.execute_script("try{apex&&apex.submit?apex.submit({request:'LOGIN'}):null}catch(e){}")
            except Exception:
                pass
        
        # Wait for valid session ID in URL (login is usually fast but might take time)
        logging.info("Waiting for session ID in URL...")
        try:
            # Oracle APEX session ID is the 3rd part of the f?p query parameter
            # We wait until it's present and not '0'
            ElectronWait(driver, 30).until(
                lambda d: re.search(r'f\?p=112:[^:]+:(\d+)', d.current_url) and 
                          re.search(r'f\?p=112:[^:]+:(\d+)', d.current_url).group(1) != '0'
            )
            logging.info("✓ Session ID detected in URL")
        except TimeoutException:
            logging.warning("Timed out waiting for session ID to appear in URL")
        
        # Check current URL to see where we are
        current_url = driver.current_url
        logging.info(f"Post-login URL: {current_url}")

        # Extra check for intermediate pages like wwv_flow.accept or wwv_flow.js_messages
        # wwv_flow.js_messages usually returns JSON/JS, which might mean we need to reload or navigate manually
        try:
            if "wwv_flow.accept" in current_url or "wwv_flow.js_messages" in current_url:
                logging.info(f"On intermediate page ({current_url}), waiting for redirect or navigating to dashboard...")
                # Wait briefly to see if it redirects automatically
                try:
                    ElectronWait(driver, 5).until(
                        lambda d: "wwv_flow" not in d.current_url
                    )
                except TimeoutException:
                    logging.info("No automatic redirect detected, forcing navigation to dashboard...")
        except:
            pass

        # Wait for page to load after login
        wait_for_page_load(driver, timeout=15)
        
        # After login, the browser should already be on the dashboard (or a page with a valid session)
        # We should NOT navigate to a generic URL without session ID (like ...:1::::::) 
        # because that often triggers a re-login in Oracle APEX.
        logging.info(f"Page loaded. Checking URL for session ID...")
        logging.info(f"Current URL: {driver.current_url}")
        
        current_url = driver.current_url
        logging.info(f"Current URL after login redirect: {current_url}")
        
        # Extract session ID from the URL first
        # Note: Oracle APEX URLs may contain "LOGIN" as a page name even after successful login
        # e.g., f?p=112:LOGIN:SESSION_ID::::: where SESSION_ID indicates successful authentication
        # The presence of a valid (non-zero) session ID means login was successful
        session_match = re.search(r'f\?p=112:[^:]+:(\d+)', current_url)
        if session_match:
            session_id = session_match.group(1)
            # Check if session ID is valid (non-zero)
            if session_id and session_id != '0':
                logging.info(f"✓ Extracted session ID: {session_id}")
                logging.info("✓ Login successful - valid session ID found in URL")
                return session_id
        
        # If no valid session ID found, check if we're still on login page
        try:
            username_field = driver.find_element(By.ID, "P101_USERNAME")
            # Login form still present and no valid session ID - login failed
            raise Exception("Login failed - login form present and no valid session ID found")
        except NoSuchElementException:
            # Login form not found but also no session ID - this is unusual
            raise Exception(f"Could not extract session ID from URL after login: {current_url}")
            
    except TimeoutException as e:
        raise Exception(f"Login timeout: {str(e)}")
    except Exception as e:
        logging.error(f"Automated login error: {e}")
        raise

def login(driver, job_id, output_dir, username=None, password=None):
    """Perform login with session management
    
    First checks if existing session is valid. If not, attempts automated login
    if credentials are provided.
    
    Args:
        driver: Selenium WebDriver instance
        job_id: Job ID for signal file naming
        output_dir: Output directory for signal files
        username: Optional portal username for automated login
        password: Optional portal password for automated login
        
    Returns:
        str: Session ID extracted from URL after login
    """
    # Check if existing session is valid
    if check_session_valid(driver):
        logging.info("✓ Using existing valid session")
        # Extract session ID from current URL
        current_url = driver.current_url
        session_match = re.search(r'f\?p=112:\d+:(\d+)', current_url)
        if session_match:
            session_id = session_match.group(1)
            logging.info(f"✓ Extracted session ID: {session_id}")
            return session_id
        else:
            # If we can't extract from URL, navigate to get session ID
            driver.get("https://exp.bb.org.bd/ords/f?p=112:1::::::")
            wait_for_page_load(driver, timeout=10)
            current_url = driver.current_url
            session_match = re.search(r'f\?p=112:\d+:(\d+)', current_url)
            if session_match:
                session_id = session_match.group(1)
                logging.info(f"✓ Extracted session ID: {session_id}")
                return session_id
    
    # Session invalid or not found
    if username and password:
        logging.info("Session invalid or not found - attempting automated login...")
        return perform_automated_login(driver, username, password)
    else:
        raise Exception("Portal credentials not configured. Please configure your Bangladesh Bank credentials in the dashboard.")

# Global results storage
automation_results = []

def write_result(row_data, success, message):
    """Store result for CSV generation at the end"""
    automation_results.append({
        "row_data": row_data,
        "status": "success" if success else "error",
        "message": message,
        "timestamp": datetime.datetime.now().isoformat()
    })
    logging.info(f"📝 Result recorded: {'✅' if success else '❌'} {message}")

def generate_results_csv(output_dir, job_id):
    """Generate results CSV file"""
    if not automation_results:
        return None
    
    file_name = f"exp_results_{job_id}.csv"
    file_path = os.path.join(output_dir, file_name)
    
    try:
        with open(file_path, 'w', encoding='utf-8', newline='') as f:
            writer = csv.writer(f)
            # Write header
            header = ["ADSCODE", "EXP_SERIAL", "EXP_YEAR", "INVOICE_NO", "status", "message", "timestamp"]
            writer.writerow(header)
            
            # Write rows
            for res in automation_results:
                row = res["row_data"]
                writer.writerow([
                    row.get("ADSCODE2", ""),
                    row.get("EXP_SERIAL2", ""),
                    row.get("EXP_YEAR2", ""),
                    row.get("INVOICE_NO", ""),
                    res["status"],
                    res["message"],
                    res["timestamp"]
                ])
        
        logging.info(f"📊 Results CSV generated: {file_path}")
        return file_path
    except Exception as e:
        logging.error(f"❌ Failed to generate results CSV: {e}")
        return None

def construct_exp_number(adscode, exp_serial, exp_year):
    """Construct 18-digit EXP number from ADSCODE, EXP_SERIAL, and EXP_YEAR
    
    Format: {ADSCODE_8digits}{EXP_SERIAL_6digits}{EXP_YEAR_4digits}
    - ADSCODE: Pad with leading zeros to 8 digits
    - EXP_SERIAL: Pad with leading zeros to 6 digits
    - EXP_YEAR: Keep as 4 digits (no padding)
    
    Args:
        adscode: ADSCODE value (will be padded to 8 digits)
        exp_serial: EXP_SERIAL value (will be padded to 6 digits)
        exp_year: EXP_YEAR value (4 digits, no padding)
    
    Returns:
        str: 18-digit EXP number
    """
    # Pad ADSCODE to 8 digits with leading zeros
    adscode_padded = str(adscode).zfill(8)
    
    # Pad EXP_SERIAL to 6 digits with leading zeros
    exp_serial_padded = str(exp_serial).zfill(6)
    
    # EXP_YEAR should already be 4 digits, but ensure it's a string
    exp_year_str = str(exp_year)
    
    # Construct the 18-digit EXP number
    exp_number = f"{adscode_padded}{exp_serial_padded}{exp_year_str}"
    
    logging.info(f"Constructed EXP number: {exp_number} (from ADSCODE={adscode}, SERIAL={exp_serial}, YEAR={exp_year})")
    
    return exp_number





def download_pdf_direct(driver, url, filename):
    """
    Download PDF by fetching it as Base64 string in the browser and saving via Python.
    Uses a single async script execution to avoid global state polling issues.
    """
    logging.info(f"Downloading PDF via Base64 fetch: {filename}")
    
    # Safely escape URL for JS
    js_url = json.dumps(url)
    
    try:
        # Wrap the whole logic in an async IIFE that returns a Promise.
        # Electron's executeJavaScript will wait for this Promise to resolve.
        script = f"""
        (async function() {{
            console.log("Starting async fetch for:", {js_url});
            try {{
                const response = await fetch({js_url});
                if (!response.ok) {{
                    throw new Error('Network status: ' + response.status);
                }}
                
                const blob = await response.blob();
                console.log("Blob received:", blob.size);
                
                return await new Promise((resolve, reject) => {{
                    const reader = new FileReader();
                    reader.onloadend = () => {{
                        resolve({{ status: 'success', data: reader.result }});
                    }};
                    reader.onerror = () => {{
                        resolve({{ status: 'error', error: 'Failed to read blob' }});
                    }};
                    reader.readAsDataURL(blob);
                }});
            }} catch (err) {{
                console.error("Fetch error:", err);
                return {{ status: 'error', error: err.message }};
            }}
        }})();
        """
        
        # This call will BLOCK until the Promise resolves
        result = driver.execute_script(script)
        
        # Check result
        if result and isinstance(result, dict):
            if result.get('status') == 'success':
                data_url = result.get('data')
                
                if data_url and ',' in data_url:
                    try:
                        base64_data = data_url.split(',')[1]
                        import base64
                        pdf_bytes = base64.b64decode(base64_data)
                        
                        # Save to file
                        full_path = os.path.join(DOWNLOAD_DIR, filename)
                        with open(full_path, 'wb') as f:
                            f.write(pdf_bytes)
                            
                        logging.info(f"✓ PDF saved successfully: {filename}")
                        return True
                    except Exception as e:
                        logging.error(f"Error decoding/saving PDF: {e}")
                        return False
                else:
                    logging.error("Invalid data URL format in response")
                    return False
            else:
                logging.error(f"Browser reported error: {result.get('error')}")
                return False
        else:
            logging.error(f"Unexpected result from browser: {result}")
            return False
            
    except Exception as e:
        logging.error(f"Error executing async download script: {e}")
        return False



def process_csv(driver, session_id):
    """Read CSV and process each row using direct URL navigation for faster downloads
    
    Args:
        driver: Selenium WebDriver instance
        session_id: Session ID extracted from login
    """
    success_count = 0
    failed_count = 0
    failed_rows = []
    
    # Validate CSV file exists before processing
    if not os.path.exists(CSV_FILE):
        logging.error(f"❌ CSV file not found: {CSV_FILE}")
        return
    
    with open(CSV_FILE, 'r', encoding='utf-8') as file:
        reader = csv.DictReader(file)
        rows = list(reader)
        
        # Filter out empty rows
        rows = [row for row in rows if any(row.values())]
        
        total_rows = len(rows)
        logging.info(f"\n{'='*60}")
        logging.info(f"Found {total_rows} rows to process")
        logging.info(f"Using optimized direct URL navigation for faster downloads")
        logging.info(f"{'='*60}\n")
        
        for idx, row in enumerate(rows, 1):
            # Check for stop signal
            stop_file = os.path.join(OUTPUT_DIR, f"stop_{JOB_ID}.txt")
            if os.path.exists(stop_file):
                logging.info("Stop signal received, stopping automation...")
                if driver:
                    try:
                        driver.quit()
                    except:
                        pass
                sys.exit(130)
            
            # Log progress: items remaining
            items_remaining = total_rows - idx + 1
            logging.info(f"📊 Progress: {idx}/{total_rows} (Remaining: {items_remaining})")
            adscode = row.get('ADSCODE2', '').strip()
            exp_serial = row.get('EXP_SERIAL2', '').strip()
            exp_year = row.get('EXP_YEAR2', '').strip()
            invoice_no = row.get('INVOICE_NO', '').strip()
            
            if not all([adscode, exp_serial, exp_year]):
                error_msg = "Missing required fields"
                logging.warning(f"⚠ Skipping row {idx}: {error_msg} (ADSCODE={adscode}, SERIAL={exp_serial}, YEAR={exp_year})")
                failed_count += 1
                failed_rows.append((idx, error_msg))
                write_result(row, False, error_msg)
                continue
            
            if not invoice_no:
                error_msg = "Missing INVOICE_NO field"
                logging.warning(f"⚠ Skipping row {idx}: {error_msg}")
                failed_count += 1
                failed_rows.append((idx, error_msg))
                write_result(row, False, error_msg)
                continue
            
            logging.info(f"\n{'='*60}")
            logging.info(f"Processing entry {idx}/{len(rows)}: EXP_{adscode}_{exp_serial}_{exp_year} (Invoice: {invoice_no})")
            logging.info(f"{'='*60}")
            
            
            try:
                # Construct EXP number from CSV data
                exp_number = construct_exp_number(adscode, exp_serial, exp_year)
                
                # Construct direect print/download URL
                # Format: https://exp.bb.org.bd/ords/f?p=112:0:{SESSION_ID}:PRINT_REPORT%3Dexp_exporter:::G_EXP_NO:{EXP_NUMBER}
                print_url = f"https://exp.bb.org.bd/ords/f?p=112:0:{session_id}:PRINT_REPORT%3Dexp_exporter:::G_EXP_NO:{exp_number}"
                
                # Desired filename
                clean_invoice = re.sub(r'[\\/*?:"<>|]', "", str(invoice_no))
                filename = f"{idx}_{clean_invoice}.pdf"
                
                # Download directly using JS injection (bypasses viewer and sets filename)
                download_success = download_pdf_direct(driver, print_url, filename)
                
                if download_success:
                    success_count += 1
                    success_msg = f"PDF downloaded successfully for Invoice {invoice_no}"
                    logging.info(f"✓ Successfully processed entry {idx}/{total_rows}: Invoice {invoice_no}")
                    logging.info(f"📥 Downloads complete: {success_count} successful, {failed_count} failed")
                    write_result(row, True, success_msg)
                else:
                    failed_count += 1
                    error_msg = f"Download failed for Invoice {invoice_no}"
                    failed_rows.append((idx, error_msg))
                    logging.error(f"✗ Failed to process entry {idx}/{total_rows}: Invoice {invoice_no}")
                    logging.info(f"📥 Downloads complete: {success_count} successful, {failed_count} failed")
                    write_result(row, False, error_msg)
                
            except Exception as e:
                failed_count += 1
                error_msg = f"Error: {str(e)}"
                failed_rows.append((idx, error_msg))
                logging.error(f"✗ Error processing entry {idx}/{total_rows}: {e}")
                logging.info(f"📥 Downloads complete: {success_count} successful, {failed_count} failed")
                write_result(row, False, error_msg)
                import traceback
                traceback.print_exc()
                
                continue
        
        # Print summary
        logging.info(f"\n{'='*60}")
        logging.info("PROCESSING SUMMARY")
        logging.info(f"{'='*60}")
        logging.info(f"Total entries: {total_rows}")
        logging.info(f"✓ Successful downloads: {success_count}")
        logging.info(f"✗ Failed downloads: {failed_count}")
        logging.info(f"📥 Total downloads completed: {success_count + failed_count}/{total_rows}")
        
        if failed_rows:
            logging.info(f"\nFailed entries:")
            for row_num, error in failed_rows:
                logging.info(f"  Row {row_num}: {error}")
        
        logging.info(f"\n📋 Detailed log saved to: {log_file_path}")
        logging.info(f"📄 Results CSV saved to: {RESULT_CSV}")
        logging.info(f"{'='*60}\n")

def setup_driver_without_incognito(headless=True, prefs=None, window_size='1920,1080', logger=None, user_data_dir=None):
    """Setup Electron browser (WebContents) with session persistence
    
    Creates an Electron-controlled browser instance. When user_data_dir is provided,
    session can be persisted by the Electron layer for future runs.
    
    Args:
        headless: If True, run browser in headless mode
        prefs: Dictionary of preferences (handled by Electron layer)
        window_size: Window size string like '1920,1080' (handled by Electron layer)
        logger: Logger instance for logging messages
        user_data_dir: Optional user data directory for session persistence
    
    Returns:
        ElectronBrowser: Configured Electron browser instance
    """
    if logger:
        logger.info(f"🔧 Headless mode: {'ENABLED' if headless else 'DISABLED'}")
        if user_data_dir:
            logger.info(f"💾 Session directory: {user_data_dir}")
            logger.info("📌 Electron browser will run in NORMAL mode with session persistence")
        else:
            logger.info("📌 Electron browser will run in NORMAL mode for automation")
    
    # Create Electron browser instance
    try:
        # Get job_id from sys.argv if available (typically 3rd argument), otherwise use default
        job_id = sys.argv[3] if len(sys.argv) > 3 else os.environ.get('JOB_ID', 'bb_exp_search')
        driver = ElectronBrowser.create(
            job_id=job_id,
            headless=headless,
            logger=logger
        )
        if logger:
            logger.info("✅ Electron browser initialized successfully")
        return driver
    except Exception as e:
        if logger:
            logger.error(f"❌ Electron browser launch failed: {e}")
        raise

def main():
    """Main function to run the automation"""
    driver = None
    login_driver = None
    
    # Parse server parameters from CLI arguments
    # Args order: 0:script, 1:csv, 2:outDir, 3:jobId, 4:user, 5:pass, 6:apiBase, 7:userId, 8:credits
    api_base_url = sys.argv[6] if len(sys.argv) > 6 else None
    user_id = sys.argv[7] if len(sys.argv) > 7 else None
    credits_used = float(sys.argv[8]) if len(sys.argv) > 8 and sys.argv[8] else 0.0
    
    # Initialize server client if parameters provided
    global server_client
    if api_base_url and user_id:
        try:
            server_client = ServerClient(api_base_url, user_id, JOB_ID)
            logging.info(f"✅ Server communication enabled: {api_base_url}")
        except Exception as e:
            logging.error(f"❌ Failed to initialize ServerClient: {e}")

    try:
        logging.info("Starting automation...")
        logging.info(f"Download directory: {DOWNLOAD_DIR}")
        logging.info(f"Session directory: {SESSION_DIR}")
        
        # Validate CSV file exists before starting
        if not os.path.exists(CSV_FILE):
            logging.error(f"❌ CSV file not found: {CSV_FILE}")
            sys.exit(1)
        
        # Step 1: Launch Electron browser in visible mode for login (with session directory)
        logging.info("Launching Electron browser for login (visible mode)...")
        login_driver = setup_driver_without_incognito(
            headless=LOGIN_HEADLESS_MODE,
            prefs=DOWNLOAD_PREFS,
            window_size='1920,1080',
            logger=logging.getLogger(),
            user_data_dir=SESSION_DIR
        )
        
        # Step 2: Login (requires configured credentials; no manual login)
        if not (PORTAL_USERNAME and PORTAL_PASSWORD):
            logging.error("Portal credentials not configured. Please configure your Bangladesh Bank credentials in the dashboard.")
            sys.exit(1)
        logging.info("Using automated login with provided credentials")
        session_id = login(login_driver, JOB_ID, OUTPUT_DIR, PORTAL_USERNAME, PORTAL_PASSWORD)
        
        # Step 3: Close login driver and launch new driver for automation (optional: can reuse same driver)
        logging.info("Login completed. Starting automation...")
        
        # Re-use the same driver
        driver = login_driver
        login_driver = None
        
        # Process CSV rows using direct URL navigation (optimized approach)
        process_csv(driver, session_id)
        
        logging.info("\n" + "="*60)
        logging.info("Automation completed successfully!")
        logging.info("="*60)
        
        # Generate and upload results
        results_csv_path = generate_results_csv(OUTPUT_DIR, JOB_ID)
        
        if server_client:
            try:
                job_data = {
                    'userId': user_id,
                    'serviceId': 'exp-search',
                    'serviceName': 'EXP Search',
                    'inputFileName': os.path.basename(CSV_FILE),
                    'inputFilePath': CSV_FILE,
                    'creditsUsed': credits_used
                }
                
                result_files = []
                if results_csv_path:
                    uploaded_path = server_client.upload_results_csv(results_csv_path, job_data)
                    if uploaded_path:
                        result_files.append(uploaded_path)
                
                # Also upload log file if it exists
                # Base log file path from logging setup
                log_file_path = os.path.join(OUTPUT_DIR, 'logs', f"bb_exp_search_{JOB_ID}.log")
                if os.path.exists(log_file_path):
                    uploaded_log = server_client.upload_log_file(log_file_path, job_data)
                    if uploaded_log:
                        result_files.append(uploaded_log)
                
                # Report final result
                success_count = len([r for r in automation_results if r['status'] == 'success'])
                server_client.report_job_result(result_files, OUTPUT_DIR, successful_count=success_count)
            except Exception as e:
                logging.error(f"❌ Error uploading results to server: {e}")
        
    except Exception as e:
        logging.error(f"Error in automation: {e}")
        if server_client:
            try:
                server_client.report_job_result([], OUTPUT_DIR, successful_count=0)
            except:
                pass
        import traceback
        traceback.print_exc()
        sys.exit(1)
    finally:
        # Close driver(s)
        if login_driver:
            logging.info("\nClosing login browser...")
            try:
                login_driver.quit()
            except:
                pass
        if driver and driver != login_driver:
            logging.info("\nClosing browser...")
            try:
                driver.quit()
            except:
                pass
        # Cleanup artifacts (sessions, logs, results CSV) after upload/log handling
        def safe_remove_dir(path, desc):
            for i in range(3):
                try:
                    if os.path.exists(path):
                        shutil.rmtree(path, ignore_errors=True)
                        if not os.path.exists(path):
                            logging.info(f"🧹 Removed {desc}: {path}")
                            return
                    break
                except Exception as e:
                    logging.warning(f"⚠️ Failed to remove {desc} (attempt {i+1}/3) {path}: {e}")
                    time.sleep(0.2)

        def safe_remove_file(path, desc):
            try:
                if os.path.exists(path):
                    os.remove(path)
                    logging.info(f"🧹 Removed {desc}: {path}")
            except Exception as e:
                logging.warning(f"⚠️ Failed to remove {desc} {path}: {e}")

        # Ensure file handlers are closed before deleting log files/dirs
        try:
            logging.shutdown()
        except Exception:
            pass

        sessions_root = os.path.join(OUTPUT_DIR, 'sessions')
        safe_remove_dir(SESSION_DIR, "session directory")
        # Also try removing the sessions root if empty
        try:
            if os.path.isdir(sessions_root) and not os.listdir(sessions_root):
                safe_remove_dir(sessions_root, "sessions root directory")
        except Exception as e:
            logging.warning(f"⚠️ Failed to inspect/remove sessions root {sessions_root}: {e}")
        safe_remove_file(RESULT_CSV, "results CSV")
        safe_remove_dir(logs_dir, "logs directory")

if __name__ == "__main__":
    main()
