#!/usr/bin/env python3
# /// script
# requires-python = ">=3.8"
# dependencies = [
#   "requests>=2.25.0",
# ]
# ///

"""
RamanBase Bulk Spectrum Uploader
Clean, refactored GUI tool for uploading multiple Raman spectra to RamanBase API
Based on official RamanBase API schema

Usage:
    # With uv (if you have uv installed):
    uv run raman_uploader.py

    # Or traditional way:
    pip install requests
    python raman_uploader.py
"""

import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext, messagebox
import requests
import time
import sqlite3
import hashlib
import threading
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass
from typing import List, Optional, Dict, Any, Tuple
from enum import Enum
import json


# Constants
API_BASE_URL = "https://api.ramanbase.org/api/v1/public"
UPLOAD_ENDPOINT = f"{API_BASE_URL}/spectra/upload"
LIST_ENDPOINT = f"{API_BASE_URL}/spectra/list"

SUPPORTED_EXTENSIONS = [
    ".txt",
    ".csv",
]

DEFAULT_DELAY = 0.0

# Database schema
DB_SCHEMA = """
CREATE TABLE IF NOT EXISTS tokens (
    id INTEGER PRIMARY KEY,
    token TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS upload_history (
    id INTEGER PRIMARY KEY,
    file_path TEXT NOT NULL,
    file_hash TEXT NOT NULL,
    uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    spectrum_id INTEGER,
    UNIQUE(file_path, file_hash)
);

CREATE TABLE IF NOT EXISTS settings (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""


class SpectroscopyType(Enum):
    """Raman spectroscopy types with their IDs"""

    RAMAN_RS = (1, "Raman spectroscopy (RS)")
    CARS = (2, "Coherent anti-Stokes Raman spectroscopy (CARS)")
    SRS = (3, "Stimulated Raman spectroscopy (SRS)")
    SERS_TERS = (4, "Surface-enhanced or tip-enhanced Raman spectroscopy (SERS/TERS)")
    SIMULATED = (5, "Simulated spectrum (no measurement)")
    FT_RAMAN = (6, "FT-Raman spectroscopy (FT-RS)")

    def __init__(self, id: int, description: str):
        self.id = id
        self.description = description


class PrivacyLevel(Enum):
    """Privacy levels for spectra"""

    PRIVATE = "private"
    LIMITED = "limited"
    PUBLIC = "public"


class SpectrumType(Enum):
    """Spectrum data types"""

    RAW = "raw"
    PROCESSED = "processed"
    SYNTHETIC = "synthetic"


@dataclass
class SpectrumMetadata:
    """Metadata for a spectrum upload"""

    name: str
    spectroscopy_type: int
    privacy: str
    spectrum_type: str
    key_words: Optional[List[str]] = None  # ADDED: array<string> keywords


@dataclass
class FileInfo:
    """Information about a spectrum file"""

    path: Path
    name: str
    size: int
    hash: str
    uploaded: bool = False


class DatabaseManager:
    """Manages SQLite database for tokens, upload history, and settings"""

    def __init__(self, db_path: Optional[Path] = None):
        if db_path is None:
            # Store database next to the script
            script_dir = Path(__file__).parent
            self.db_path = script_dir / "ramanbase_uploader.db"
        else:
            self.db_path = db_path
        self._init_database()

    def _init_database(self):
        """Initialize database with schema"""
        with sqlite3.connect(self.db_path) as conn:
            conn.executescript(DB_SCHEMA)
            conn.commit()

    def save_token(self, token: str) -> None:
        """Save API token to database"""
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("INSERT OR REPLACE INTO tokens (token) VALUES (?)", (token,))
            conn.commit()

    def get_token(self) -> Optional[str]:
        """Get the most recent API token"""
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                "SELECT token FROM tokens ORDER BY created_at DESC LIMIT 1"
            )
            result = cursor.fetchone()
            return result[0] if result else None

    def mark_uploaded(
        self, file_path: str, file_hash: str, spectrum_id: Optional[int] = None
    ) -> None:
        """Mark a file as successfully uploaded"""
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """INSERT OR REPLACE INTO upload_history 
                   (file_path, file_hash, spectrum_id) VALUES (?, ?, ?)""",
                (file_path, file_hash, spectrum_id),
            )
            conn.commit()

    def is_uploaded(self, file_path: str, file_hash: str) -> bool:
        """Check if a file has already been uploaded"""
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                "SELECT 1 FROM upload_history WHERE file_path = ? AND file_hash = ?",
                (file_path, file_hash),
            )
            return cursor.fetchone() is not None

    def save_setting(self, key: str, value: str) -> None:
        """Save a setting to database"""
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
                (key, value),
            )
            conn.commit()

    def get_setting(self, key: str, default: str = "") -> str:
        """Get a setting from database"""
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute("SELECT value FROM settings WHERE key = ?", (key,))
            result = cursor.fetchone()
            return result[0] if result else default


class FileScanner:
    """Handles recursive file scanning and hash calculation"""

    def __init__(self, db_manager: DatabaseManager):
        self.db_manager = db_manager

    def scan_folder(
        self,
        folder_path: Path,
        custom_extensions: Optional[str] = None,
        max_depth: int = 3,
    ) -> List[FileInfo]:
        """Recursively scan folder for spectrum files with depth control"""
        files = []

        # Use custom extensions if provided, otherwise use default
        if custom_extensions and custom_extensions.strip():
            extensions = [
                ext.strip() for ext in custom_extensions.split(",") if ext.strip()
            ]
        else:
            extensions = SUPPORTED_EXTENSIONS

        for ext in extensions:
            # Use depth-limited glob pattern
            if max_depth == 1:
                # Only current folder
                pattern = f"*{ext}"
            else:
                # Limited depth: **/*{ext} but we'll filter by depth
                pattern = f"**/*{ext}"

            for file_path in folder_path.glob(pattern):
                if file_path.is_file():
                    # Check depth limit
                    try:
                        relative_path = file_path.relative_to(folder_path)
                        depth = (
                            len(relative_path.parts) - 1
                        )  # -1 because file itself doesn't count as depth
                        if (
                            depth <= max_depth - 1
                        ):  # -1 because we want to include files at max_depth level
                            file_info = self._create_file_info(file_path)
                            file_info.uploaded = self.db_manager.is_uploaded(
                                str(file_info.path), file_info.hash
                            )
                            files.append(file_info)
                    except ValueError:
                        # File is outside the folder path, skip it
                        continue

        return sorted(files, key=lambda f: f.name)

    def _create_file_info(self, file_path: Path) -> FileInfo:
        """Create FileInfo object with hash calculation"""
        file_hash = self._calculate_file_hash(file_path)
        return FileInfo(
            path=file_path,
            name=file_path.name,
            size=file_path.stat().st_size,
            hash=file_hash,
        )

    def _calculate_file_hash(self, file_path: Path) -> str:
        """Calculate SHA256 hash of file"""
        hash_sha256 = hashlib.sha256()
        with open(file_path, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hash_sha256.update(chunk)
        return hash_sha256.hexdigest()


class APIClient:
    """Handles all API communication with RamanBase"""

    def __init__(self, token: str):
        self.token = token
        self.headers = {"Authorization": f"Token {token}"}

    def test_connection(self) -> Tuple[bool, str, Optional[Dict]]:
        """Test API connection and authentication"""
        try:
            response = requests.get(LIST_ENDPOINT, headers=self.headers, timeout=10)

            if response.status_code == 200:
                data = response.json()
                return True, "Connection successful!", data
            elif response.status_code == 401:
                return (
                    False,
                    "Authentication failed. Please check your API token.",
                    None,
                )
            else:
                return False, f"Server error: {response.status_code}", None

        except requests.exceptions.RequestException as e:
            return False, f"Connection error: {str(e)}", None

    def upload_spectrum(
        self, file_path: Path, metadata: SpectrumMetadata
    ) -> Tuple[bool, str, Optional[Dict]]:
        """Upload a single spectrum file"""
        try:
            with open(file_path, "rb") as f:
                files_data = {"files": (file_path.name, f, "application/octet-stream")}

                form_data = self._build_form_data(metadata, file_path)

                response = requests.post(
                    UPLOAD_ENDPOINT,
                    headers=self.headers,
                    files=files_data,
                    data=form_data,
                    timeout=30,
                )

                if response.status_code == 201:
                    return True, "Upload successful", response.json()
                elif response.status_code == 400:
                    error_msg = self._parse_error_response(response)
                    return False, f"Bad request: {error_msg}", None
                elif response.status_code == 429:
                    return False, "Rate limit exceeded", None
                else:
                    return False, f"Upload failed: HTTP {response.status_code}", None

        except Exception as e:
            return False, f"Upload error: {str(e)}", None

    def _build_form_data(
        self, metadata: SpectrumMetadata, file_path: Path
    ) -> Dict[str, Any]:
        """Build form data from metadata"""
        # Use filename if no spectrum name is provided
        spectrum_name = metadata.name if metadata.name else file_path.stem

        form_data: Dict[str, Any] = {
            "name": spectrum_name,
            "spectroscopy_type": metadata.spectroscopy_type,
            "privacy": metadata.privacy,
            "type": metadata.spectrum_type,
        }

        # Send key_words as repeated form fields (one per keyword)
        # requests library will send: key_words=water&key_words=test&key_words=sample
        if metadata.key_words:
            form_data["key_words"] = metadata.key_words

        return form_data

    def _parse_error_response(self, response: requests.Response) -> str:
        """Parse error response from API"""
        try:
            error_data = response.json()
            errors = []
            for field, field_errors in error_data.items():
                if isinstance(field_errors, list):
                    errors.extend([f"{field}: {error}" for error in field_errors])
                else:
                    errors.append(f"{field}: {field_errors}")
            return "; ".join(errors)
        except Exception:
            return response.text[:200]


class UploadWorker(threading.Thread):
    """Background thread for handling uploads"""

    def __init__(
        self,
        files: List[FileInfo],
        metadata: SpectrumMetadata,
        api_client: APIClient,
        db_manager: DatabaseManager,
        delay: float,
        progress_callback,
        log_callback,
        stop_event,
    ):
        super().__init__(daemon=True)
        self.files = files
        self.metadata = metadata
        self.api_client = api_client
        self.db_manager = db_manager
        self.delay = delay
        self.progress_callback = progress_callback
        self.log_callback = log_callback
        self.stop_event = stop_event

        self.successful = 0
        self.failed = 0
        self.start_time = time.time()

    def run(self):
        """Main upload loop"""
        total_files = len(self.files)

        for idx, file_info in enumerate(self.files, 1):
            if self.stop_event.is_set():
                self.log_callback("Upload stopped by user", "WARNING")
                break

            if file_info.uploaded:
                self.log_callback(
                    f"⏭ Skipped (already uploaded): {file_info.name}", "INFO"
                )
                # Calculate estimated remaining time for skipped files too
                elapsed_time = time.time() - self.start_time
                if idx > 0:
                    avg_time_per_file = elapsed_time / idx
                    remaining_files = total_files - idx
                    estimated_remaining = avg_time_per_file * remaining_files
                    self.progress_callback(
                        idx, total_files, file_info.name, estimated_remaining
                    )
                else:
                    self.progress_callback(idx, total_files, file_info.name, 0)
                continue

            # Calculate estimated remaining time
            elapsed_time = time.time() - self.start_time
            if idx > 0:
                avg_time_per_file = elapsed_time / idx
                remaining_files = total_files - idx
                estimated_remaining = avg_time_per_file * remaining_files
                self.progress_callback(
                    idx, total_files, file_info.name, estimated_remaining
                )
            else:
                self.progress_callback(idx, total_files, file_info.name, 0)

            try:
                success, message, response_data = self.api_client.upload_spectrum(
                    file_info.path, self.metadata
                )

                if success:
                    self.successful += 1
                    self.db_manager.mark_uploaded(
                        str(file_info.path),
                        file_info.hash,
                        response_data.get("id") if response_data else None,
                    )
                    self.log_callback(f"✓ Uploaded: {file_info.name}", "SUCCESS")
                else:
                    self.failed += 1
                    self.log_callback(
                        f"✗ Failed: {file_info.name} - {message}", "ERROR"
                    )

            except Exception as e:
                self.failed += 1
                self.log_callback(f"✗ Error: {file_info.name} - {str(e)}", "ERROR")

            # Rate limiting
            if idx < total_files and not self.stop_event.is_set():
                time.sleep(self.delay)

        # Upload complete
        self.log_callback("=" * 50)
        self.log_callback(
            "UPLOAD COMPLETE!", "SUCCESS" if self.failed == 0 else "WARNING"
        )
        self.log_callback(
            f"Successfully uploaded: {self.successful}/{total_files}", "SUCCESS"
        )
        if self.failed > 0:
            self.log_callback(f"Failed uploads: {self.failed}/{total_files}", "ERROR")
        self.log_callback("=" * 50)


class RamanUploaderApp:
    """Main GUI application for RamanBase bulk uploader"""

    def __init__(self, root):
        self.root = root
        self.root.title("RamanBase Bulk Uploader")
        self.root.geometry("900x750")

        # Initialize managers
        self.db_manager = DatabaseManager()
        self.file_scanner = FileScanner(self.db_manager)
        self.api_client = None

        # State
        self.uploading = False
        self.stop_event = threading.Event()
        self.current_files = []

        # Create UI
        self._create_widgets()

        # Load settings immediately after creating widgets
        self._load_settings()

    def _create_widgets(self):
        """Create and layout all GUI widgets"""
        # Main frame with scrollbar
        canvas = tk.Canvas(self.root)
        scrollbar = ttk.Scrollbar(self.root, orient="vertical", command=canvas.yview)
        scrollable_frame = ttk.Frame(canvas)

        scrollable_frame.bind(
            "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )

        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)

        main_frame = ttk.Frame(scrollable_frame, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        row = 0

        # Authentication section
        row = self._create_auth_section(main_frame, row)

        # File selection section
        row = self._create_file_section(main_frame, row)

        # Required metadata section
        row = self._create_required_metadata_section(main_frame, row)

        # Upload settings section
        row = self._create_upload_settings_section(main_frame, row)

        # Control buttons
        row = self._create_control_buttons(main_frame, row)

        # Progress section
        row = self._create_progress_section(main_frame, row)

        # Log section
        row = self._create_log_section(main_frame, row)

        # Configure grid weights
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(row, weight=1)

        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

        self._log("Welcome to RamanBase Bulk Uploader!")
        self._log(
            "Please enter your API token and select a folder with spectrum files."
        )

    def _create_auth_section(self, parent, row):
        """Create authentication section"""
        auth_frame = ttk.LabelFrame(parent, text="Authentication", padding="10")
        auth_frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)

        ttk.Label(auth_frame, text="API Token:").grid(
            row=0, column=0, sticky=tk.W, pady=3
        )
        self.token_entry = ttk.Entry(auth_frame, width=60, show="*")
        self.token_entry.grid(
            row=0, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=3
        )

        ttk.Button(auth_frame, text="Show/Hide", command=self._toggle_token).grid(
            row=0, column=3, padx=5
        )

        ttk.Label(auth_frame, text="Get token at:", font=("TkDefaultFont", 8)).grid(
            row=1, column=0, sticky=tk.W
        )
        token_link = ttk.Label(
            auth_frame,
            text="https://ramanbase.org/account/tokens",
            foreground="blue",
            cursor="hand2",
            font=("TkDefaultFont", 8),
        )
        token_link.grid(row=1, column=1, sticky=tk.W)

        return row + 1

    def _create_file_section(self, parent, row):
        """Create file selection section"""
        file_frame = ttk.LabelFrame(parent, text="Files to Upload", padding="10")
        file_frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)

        ttk.Label(file_frame, text="Spectra Folder:").grid(
            row=0, column=0, sticky=tk.W, pady=3
        )
        self.folder_entry = ttk.Entry(file_frame, width=50)
        self.folder_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=3)
        ttk.Button(file_frame, text="Browse", command=self._browse_folder).grid(
            row=0, column=2, padx=5
        )

        # Extensions field (editable)
        ttk.Label(file_frame, text="File extensions:", font=("TkDefaultFont", 8)).grid(
            row=1, column=0, sticky=tk.W, pady=3
        )
        self.extensions = ttk.Entry(file_frame, width=50)
        self.extensions.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=3)
        ttk.Button(
            file_frame, text="Reset to Default", command=self._reset_extensions
        ).grid(row=1, column=2, padx=5)
        ttk.Label(
            file_frame,
            text="(comma-separated, e.g.: .txt,.csv,.spc - edit to include/exclude extensions)",
            font=("TkDefaultFont", 8),
        ).grid(row=2, column=1, sticky=tk.W)

        # Scan depth setting
        ttk.Label(file_frame, text="Scan depth:", font=("TkDefaultFont", 8)).grid(
            row=3, column=0, sticky=tk.W, pady=3
        )
        self.scan_depth = ttk.Spinbox(file_frame, from_=1, to=10, width=10)
        self.scan_depth.set(1)  # Default to 1 level (current folder only)
        self.scan_depth.grid(row=3, column=1, sticky=tk.W, pady=3)
        ttk.Label(
            file_frame,
            text="(1=current folder only, 2=one subfolder deep, etc.)",
            font=("TkDefaultFont", 8),
        ).grid(row=4, column=1, sticky=tk.W)

        self.file_count_label = ttk.Label(
            file_frame, text="Files found: 0", font=("TkDefaultFont", 9, "bold")
        )
        self.file_count_label.grid(row=5, column=1, sticky=tk.W, pady=3)

        ttk.Label(
            file_frame,
            text="Note: Spectrum names will be taken from filenames",
            font=("TkDefaultFont", 8),
            foreground="gray",
        ).grid(row=6, column=1, sticky=tk.W)

        return row + 1

    def _create_required_metadata_section(self, parent, row):
        """Create required metadata section"""
        required_frame = ttk.LabelFrame(parent, text="Required Metadata", padding="10")
        required_frame.grid(
            row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5
        )

        # Spectrum Name (will be set from filename, but allow override)
        ttk.Label(required_frame, text="*Spectrum Name:").grid(
            row=0, column=0, sticky=tk.W, pady=3
        )
        self.spectrum_name_entry = ttk.Entry(required_frame, width=50)
        self.spectrum_name_entry.grid(
            row=0, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=3
        )
        ttk.Label(
            required_frame,
            text="(leave empty to use filename)",
            font=("TkDefaultFont", 8),
        ).grid(row=1, column=1, columnspan=2, sticky=tk.W)

        # Type (Raw/Processed/Synthetic/Dft/Recovered)
        ttk.Label(required_frame, text="*Type:").grid(
            row=2, column=0, sticky=tk.W, pady=3
        )
        type_values = ["raw", "processed", "synthetic", "dft", "recovered"]
        self.spectrum_type = ttk.Combobox(
            required_frame, width=20, values=type_values, state="readonly"
        )
        self.spectrum_type.set("raw")
        self.spectrum_type.grid(row=2, column=1, sticky=tk.W, pady=3)

        # Spectroscopy Type
        ttk.Label(required_frame, text="*Spectroscopy Type:").grid(
            row=3, column=0, sticky=tk.W, pady=3
        )
        spectroscopy_values = [f"{st.id} - {st.description}" for st in SpectroscopyType]
        self.spectroscopy_type = ttk.Combobox(
            required_frame, width=50, values=spectroscopy_values, state="readonly"
        )
        self.spectroscopy_type.set(spectroscopy_values[0])
        self.spectroscopy_type.grid(row=3, column=1, columnspan=2, sticky=tk.W, pady=3)

        # Privacy Settings
        ttk.Label(required_frame, text="*Privacy Settings:").grid(
            row=4, column=0, sticky=tk.W, pady=3
        )
        privacy_values = [f"{p.value} - {p.name.title()}" for p in PrivacyLevel]
        self.privacy = ttk.Combobox(
            required_frame, width=40, values=privacy_values, state="readonly"
        )
        self.privacy.set(privacy_values[0])
        self.privacy.grid(row=4, column=1, columnspan=2, sticky=tk.W, pady=3)

        # ADDED: Keyword(s)
        ttk.Label(required_frame, text="*Keyword(s):").grid(
            row=5, column=0, sticky=tk.W, pady=3
        )
        self.keywords_entry = ttk.Entry(required_frame, width=50)
        self.keywords_entry.grid(
            row=5, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=3
        )
        ttk.Label(
            required_frame,
            text="Enter one or more; separate with semicolons (e.g., water; NaCl; calibration)",
            font=("TkDefaultFont", 8),
        ).grid(row=6, column=1, columnspan=2, sticky=tk.W)

        return row + 1

    def _create_upload_settings_section(self, parent, row):
        """Create upload settings section"""
        rate_frame = ttk.LabelFrame(parent, text="Upload Settings", padding="10")
        rate_frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)

        ttk.Label(rate_frame, text="Delay between uploads (seconds):").grid(
            row=0, column=0, sticky=tk.W
        )
        self.delay_spinbox = ttk.Spinbox(
            rate_frame, from_=0.0, to=10, increment=0.5, width=10
        )
        self.delay_spinbox.set(DEFAULT_DELAY)
        self.delay_spinbox.grid(row=0, column=1, sticky=tk.W, padx=10)

        ttk.Label(
            rate_frame,
            text="Add if necessary",
            font=("TkDefaultFont", 8),
        ).grid(row=0, column=2, sticky=tk.W)

        return row + 1

    def _create_control_buttons(self, parent, row):
        """Create control buttons"""
        button_frame = ttk.Frame(parent)
        button_frame.grid(row=row, column=0, columnspan=3, pady=10)

        ttk.Button(
            button_frame, text="Test Connection", command=self._test_connection
        ).grid(row=0, column=0, padx=5)
        ttk.Button(button_frame, text="Scan Files", command=self._scan_files).grid(
            row=0, column=1, padx=5
        )
        ttk.Button(button_frame, text="View Files", command=self._view_files).grid(
            row=0, column=2, padx=5
        )
        ttk.Button(button_frame, text="Export Log", command=self._export_log).grid(
            row=0, column=3, padx=5
        )
        self.upload_btn = ttk.Button(
            button_frame, text="Start Upload", command=self._start_upload
        )
        self.upload_btn.grid(row=0, column=4, padx=5)

        self.stop_btn = ttk.Button(
            button_frame,
            text="Stop Upload",
            command=self._stop_upload,
            state=tk.DISABLED,
        )
        self.stop_btn.grid(row=0, column=5, padx=5)

        return row + 1

    def _create_progress_section(self, parent, row):
        """Create progress section"""
        progress_frame = ttk.LabelFrame(parent, text="Progress", padding="10")
        progress_frame.grid(
            row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5
        )

        self.progress = ttk.Progressbar(progress_frame, mode="determinate")
        self.progress.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)

        self.progress_label = ttk.Label(progress_frame, text="Ready")
        self.progress_label.grid(row=1, column=0, sticky=tk.W)

        progress_frame.columnconfigure(0, weight=1)

        return row + 1

    def _create_log_section(self, parent, row):
        """Create log section"""
        log_frame = ttk.LabelFrame(parent, text="Upload Log", padding="10")
        log_frame.grid(
            row=row, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5
        )

        self.log_text = scrolledtext.ScrolledText(
            log_frame, width=80, height=15, wrap=tk.WORD
        )
        self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # Configure text tags for colored output
        self.log_text.tag_config("SUCCESS", foreground="green")
        self.log_text.tag_config("ERROR", foreground="red")
        self.log_text.tag_config("WARNING", foreground="orange")

        log_frame.columnconfigure(0, weight=1)
        log_frame.rowconfigure(0, weight=1)

        return row + 1

    def _toggle_token(self):
        """Toggle token visibility"""
        if self.token_entry.cget("show") == "*":
            self.token_entry.config(show="")
        else:
            self.token_entry.config(show="*")

    def _log(self, message: str, level: str = "INFO"):
        """Add message to log with timestamp and color"""
        timestamp = datetime.now().strftime("%H:%M:%S")
        prefix = (
            "✓"
            if level == "SUCCESS"
            else "✗"
            if level == "ERROR"
            else "ℹ"
            if level == "INFO"
            else "⚠"
        )
        log_line = f"[{timestamp}] {prefix} {message}\n"

        if level in ["SUCCESS", "ERROR", "WARNING"]:
            self.log_text.insert(tk.END, log_line, level)
        else:
            self.log_text.insert(tk.END, log_line)

        self.log_text.see(tk.END)
        self.root.update_idletasks()

    def _browse_folder(self):
        """Open folder browser dialog"""
        folder = filedialog.askdirectory()
        if folder:
            self.folder_entry.delete(0, tk.END)
            self.folder_entry.insert(0, folder)
            self._scan_files()

    def _reset_extensions(self):
        """Reset extensions to default supported extensions"""
        self.extensions.delete(0, tk.END)
        self.extensions.insert(0, ",".join(SUPPORTED_EXTENSIONS))
        self._log("Extensions reset to all supported formats", "SUCCESS")

    def _test_connection(self):
        """Test API connection and authentication"""
        self._log("Testing connection to RamanBase API...")

        token = self.token_entry.get().strip()
        if not token:
            messagebox.showerror("Error", "Please enter your API token")
            return

        # Save token to database
        self.db_manager.save_token(token)

        # Create API client and test
        self.api_client = APIClient(token)
        success, message, data = self.api_client.test_connection()

        if success:
            self._log("Connection successful! Authentication OK ✓", "SUCCESS")
            if data:
                self._log(f"Your account has access to {data.get('count', 0)} spectra")
            messagebox.showinfo(
                "Success", "Connection to RamanBase API successful!\nAuthentication OK."
            )
        else:
            self._log(message, "ERROR")
            messagebox.showerror("Error", message)

    def _scan_files(self):
        """Scan folder for valid spectrum files"""
        folder = self.folder_entry.get().strip()
        if not folder or not Path(folder).exists():
            self._log("Please select a valid folder", "ERROR")
            return

        folder_path = Path(folder)
        extensions = self.extensions.get().strip()
        depth = int(self.scan_depth.get())
        self.current_files = self.file_scanner.scan_folder(
            folder_path, extensions, depth
        )

        uploaded_count = sum(1 for f in self.current_files if f.uploaded)
        total_count = len(self.current_files)

        self.file_count_label.config(
            text=f"Files found: {total_count} ({uploaded_count} already uploaded)"
        )
        self._log(
            f"Found {total_count} spectrum files in folder ({uploaded_count} already uploaded)"
        )

        return self.current_files

    def _view_files(self):
        """Show list of files that will be uploaded"""
        if not self.current_files:
            messagebox.showinfo(
                "No Files", "No spectrum files found. Please scan a folder first."
            )
            return

        # Create popup window
        popup = tk.Toplevel(self.root)
        popup.title("Files to Upload")
        popup.geometry("600x400")

        ttk.Label(
            popup,
            text=f"Found {len(self.current_files)} files:",
            font=("TkDefaultFont", 10, "bold"),
        ).pack(pady=10)

        # File list with scrollbar
        list_frame = ttk.Frame(popup)
        list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        scrollbar = ttk.Scrollbar(list_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        file_listbox = tk.Listbox(
            list_frame, yscrollcommand=scrollbar.set, font=("Courier", 9)
        )
        file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.config(command=file_listbox.yview)

        for file_info in sorted(self.current_files, key=lambda x: x.path):
            status = " (uploaded)" if file_info.uploaded else ""
            file_listbox.insert(tk.END, f"{file_info.path}{status}")

        ttk.Button(popup, text="Close", command=popup.destroy).pack(pady=10)

    def _export_log(self):
        """Export log to text file"""
        log_content = self.log_text.get("1.0", tk.END)
        if not log_content.strip():
            messagebox.showinfo("Empty Log", "Log is empty, nothing to export")
            return

        filename = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
            initialfile=f"ramanbase_upload_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
        )

        if filename:
            try:
                with open(filename, "w", encoding="utf-8") as f:
                    f.write(log_content)
                self._log(f"Log exported to: {filename}", "SUCCESS")
                messagebox.showinfo("Success", f"Log exported to:\n{filename}")
            except Exception as e:
                self._log(f"Failed to export log: {str(e)}", "ERROR")
                messagebox.showerror("Error", f"Failed to export log:\n{str(e)}")

    def _validate_inputs(self) -> bool:
        """Validate required inputs before upload"""
        if not self.token_entry.get().strip():
            messagebox.showerror("Error", "API token is required")
            return False

        if not self.folder_entry.get().strip():
            messagebox.showerror("Error", "Please select a folder")
            return False

        if not self.current_files:
            messagebox.showerror(
                "Error", "No spectrum files found. Please scan a folder first."
            )
            return False

        return True

    def _start_upload(self):
        """Start the upload process"""
        if not self._validate_inputs():
            return

        # Confirm upload
        new_files = [f for f in self.current_files if not f.uploaded]
        if not new_files:
            messagebox.showinfo("No New Files", "All files have already been uploaded.")
            return

        if not messagebox.askyesno(
            "Confirm Upload",
            f"Ready to upload {len(new_files)} new files to RamanBase.\n\nContinue?",
        ):
            return

        # Create API client
        token = self.token_entry.get().strip()
        self.api_client = APIClient(token)
        self.db_manager.save_token(token)

        # Build metadata
        metadata = self._build_metadata()

        # Start upload
        self.uploading = True
        self.stop_event.clear()
        self.upload_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)

        delay = float(self.delay_spinbox.get())

        # Start upload worker
        worker = UploadWorker(
            self.current_files,
            metadata,
            self.api_client,
            self.db_manager,
            delay,
            self._update_progress,
            self._log,
            self.stop_event,
        )
        worker.start()

        # Monitor upload completion
        self._monitor_upload(worker)

    def _stop_upload(self):
        """Stop the upload process"""
        self.stop_event.set()
        self._log("Stopping upload after current file...", "WARNING")

    def _build_metadata(self) -> SpectrumMetadata:
        """Build metadata from form inputs"""
        # Parse spectroscopy type
        spectroscopy_str = self.spectroscopy_type.get()
        spectroscopy_id = int(spectroscopy_str.split(" - ")[0])

        # Parse privacy
        privacy_str = self.privacy.get()
        privacy_value = privacy_str.split(" - ")[0]

        # Parse keywords (semicolon-separated)
        kw_text = (self.keywords_entry.get() or "").strip()
        key_words = [w.strip() for w in kw_text.split(";") if w.strip()] if kw_text else None

        # Build metadata
        spectrum_name = self.spectrum_name_entry.get().strip()
        metadata = SpectrumMetadata(
            name=spectrum_name,  # Will be set from filename if empty
            spectroscopy_type=spectroscopy_id,
            privacy=privacy_value,
            spectrum_type=self.spectrum_type.get(),
            key_words=key_words,  # ADDED
        )

        return metadata

    def _update_progress(
        self, current: int, total: int, filename: str, estimated_remaining: float = 0
    ):
        """Update progress bar and label with estimated remaining time"""
        self.progress["maximum"] = total
        self.progress["value"] = current

        # Format estimated remaining time
        if estimated_remaining > 0:
            if estimated_remaining < 60:
                time_str = f"{estimated_remaining:.0f}s"
            elif estimated_remaining < 3600:
                time_str = f"{estimated_remaining / 60:.1f}m"
            else:
                time_str = f"{estimated_remaining / 3600:.1f}h"
            progress_text = f"Uploading {current}/{total}: {filename} (ETA: {time_str})"
        else:
            progress_text = f"Uploading {current}/{total}: {filename}"

        self.progress_label.config(text=progress_text)
        self.root.update_idletasks()

    def _monitor_upload(self, worker: UploadWorker):
        """Monitor upload worker completion"""
        if worker.is_alive():
            self.root.after(100, lambda: self._monitor_upload(worker))
        else:
            # Upload complete
            self.uploading = False
            self.upload_btn.config(state=tk.NORMAL)
            self.stop_btn.config(state=tk.DISABLED)

            self.progress_label.config(
                text=f"Complete: {worker.successful} successful, {worker.failed} failed"
            )

            result_msg = f"Upload finished!\n\nSuccessful: {worker.successful}\nFailed: {worker.failed}"
            if worker.successful > 0:
                result_msg += (
                    "\n\nView your spectra at:\nhttps://ramanbase.org/my-spectra"
                )

            messagebox.showinfo("Upload Complete", result_msg)

    def _load_settings(self):
        """Load saved settings from database"""
        # Load token
        token = self.db_manager.get_token()
        if token:
            self.token_entry.insert(0, token)

        # Load other settings
        folder = self.db_manager.get_setting("last_folder")
        if folder:
            self.folder_entry.insert(0, folder)

        delay = self.db_manager.get_setting("delay", str(DEFAULT_DELAY))
        self.delay_spinbox.set(delay)

        # Load extensions (default to basic extensions)
        extensions = self.db_manager.get_setting(
            "extensions", ",".join(SUPPORTED_EXTENSIONS)
        )
        self.extensions.insert(0, extensions)

        # Load scan depth
        scan_depth = self.db_manager.get_setting("scan_depth", "1")
        self.scan_depth.set(scan_depth)

        self._log("Settings loaded from previous session", "SUCCESS")

    def _save_settings(self):
        """Save current settings to database"""
        self.db_manager.save_setting("last_folder", self.folder_entry.get().strip())
        self.db_manager.save_setting("delay", self.delay_spinbox.get())
        self.db_manager.save_setting("extensions", self.extensions.get().strip())
        self.db_manager.save_setting("scan_depth", self.scan_depth.get())


def main():
    """Main entry point"""
    root = tk.Tk()
    app = RamanUploaderApp(root)

    # Save settings on close
    def on_closing():
        app._save_settings()
        root.destroy()

    root.protocol("WM_DELETE_WINDOW", on_closing)
    root.mainloop()


if __name__ == "__main__":
    main()
