# downloader.py (Fixed SyntaxError in duplicate check & cleanup - Attempt 7)
import requests
import os
import time
import shutil
import re
import sys
import threading
import traceback
import math  # Needed for floor/log/pow in format_size
from urllib.parse import urlparse, unquote
from selenium.common.exceptions import WebDriverException, TimeoutException
import logging

# Import project modules
from logger import app_logger

try:
    # Assume parser.py is in the same directory or Python path
    import parser
except ImportError:
    app_logger.critical("Failed to import parser.py. Ensure it exists.")
    sys.exit(1)
try:
    # Assume console_ui.py is in the same directory or Python path
    from console_ui import ConsoleUI
except ImportError:
    ConsoleUI = None  # Fallback type
try:
    # Assume selenium_handlers.py is in the same directory or Python path
    import selenium_handlers
except ImportError:
    app_logger.warning("selenium_handlers.py not found")


# --- Helper Functions ---


def _format_size(size_bytes):
    """Formats bytes into a human-readable string (B, KB, MB, GB)."""
    if size_bytes is None or not isinstance(size_bytes, (int, float)) or size_bytes < 0:
        return "Unknown size"
    if size_bytes == 0:
        return "0 B"
    # Check if math is available before using it
    if "math" not in sys.modules:
        return f"{size_bytes} B"  # Basic fallback
    size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
    try:
        i = int(math.floor(math.log(size_bytes, 1024)))
        # Prevent index out of bounds for extremely large sizes
        if i >= len(size_name):
            i = len(size_name) - 1
        p = math.pow(1024, i)
        s = round(size_bytes / p, 2)
        return f"{s} {size_name[i]}"
    except (ValueError, OverflowError):  # Handle potential math errors
        return f"{size_bytes} B"


def get_filename_from_url(url):
    """Extracts a potential filename from the URL path."""
    try:
        path = urlparse(url).path
        # Decode URL encoding (%20 -> space, etc.)
        filename = os.path.basename(unquote(path))
        if filename:
            return filename
    except Exception:
        # Ignore errors during URL parsing/decoding
        pass
    return None


def get_filename_from_headers(response):
    """Extracts filename from Content-Disposition header."""
    cd = response.headers.get("content-disposition")
    if not cd:
        return None
    # Try filename*= first (handles Unicode)
    fname = re.findall("filename\*=([^;\n]*)", cd, re.IGNORECASE)
    if fname:
        fn = fname[0].strip().strip("'\"")
        # Handle encoding prefix (e.g., UTF-8'')
        if fn.lower().startswith("utf-8''"):
            try:
                fn = unquote(fn[7:], encoding="utf-8")
            except Exception:
                pass  # Fallback if unquote fails
        elif fn.lower().startswith("iso-8859-1''"):
            try:
                fn = unquote(fn[11:], encoding="iso-8859-1")
            except Exception:
                pass
        # Add handling for other potential encodings if needed
        return fn.strip()

    # Fallback to filename=
    fname = re.findall("filename=([^;\n]*)", cd, re.IGNORECASE)
    if fname:
        # Strip potential quotes
        return fname[0].strip().strip("'\"")
    return None


def attempt_rename(source_path, target_dir, desired_filename):
    """
    Attempts to move source_path to target_dir with desired_filename.
    Checks for duplicates (exact name + size) before renaming/moving.
    Handles existing files (different size) by adding suffixes (_1, _2, ...).
    Returns the final path, "DUPLICATE_SKIPPED", or None on error.
    """
    thread_name = threading.current_thread().name
    if not os.path.exists(source_path):
        app_logger.error(
            f"[{thread_name}] Source file for rename does not exist: {source_path}"
        )
        return None

    if not os.path.exists(target_dir):
        try:
            os.makedirs(target_dir)
            app_logger.debug(f"[{thread_name}] Created target directory: {target_dir}")
        except OSError as e:
            app_logger.error(
                f"[{thread_name}] Failed to create target directory {target_dir}: {e}"
            )
            return None

    # Ensure desired_filename is sanitized
    desired_filename = parser.sanitize_filename(desired_filename)
    initial_target_path = os.path.join(target_dir, desired_filename)
    final_target_path = initial_target_path  # Start with the initial path

    # --- Duplicate Check (Name & Size) ---
    if os.path.exists(initial_target_path):
        try:
            existing_size = os.path.getsize(initial_target_path)
            new_size = os.path.getsize(source_path)
            if existing_size == new_size:
                app_logger.info(
                    f"[{thread_name}] Skipping save: Existing file '{desired_filename}' found with same size ({_format_size(existing_size)})."
                )
                # <<< CORRECTED SYNTAX: try...except block with indentation >>>
                try:
                    os.remove(source_path)
                    app_logger.debug(
                        f"[{thread_name}] Deleted duplicate temp file: {source_path}"
                    )
                except OSError as e:
                    app_logger.warning(
                        f"[{thread_name}] Could not remove duplicate temp file {source_path}: {e}"
                    )
                # <<< END CORRECTION >>>
                return "DUPLICATE_SKIPPED"  # Specific status for skipped duplicate
            else:
                # File exists but size differs, proceed to find suffix
                app_logger.warning(
                    f"[{thread_name}] Target file {desired_filename} exists but size differs (Existing: {_format_size(existing_size)}, New: {_format_size(new_size)}). Adding suffix."
                )
                # Fall through to suffix logic below
        except OSError as e:
            app_logger.warning(
                f"[{thread_name}] Could not check size for potential duplicate '{desired_filename}': {e}"
            )
            # Proceed with suffix logic as a precaution if size check fails
            # Fall through to suffix logic below

    # --- Suffix Logic (Only if initial exists with different size, or size check failed) ---
    if os.path.exists(initial_target_path):  # Re-check needed in case size check failed
        counter = 1
        name, ext = os.path.splitext(desired_filename)
        # Start finding suffixed name
        final_target_path = os.path.join(target_dir, f"{name}_{counter}{ext}")
        while os.path.exists(final_target_path):
            counter += 1
            final_target_path = os.path.join(target_dir, f"{name}_{counter}{ext}")
            if counter > 100:  # Safety break
                app_logger.error(
                    f"[{thread_name}] Could not find available suffixed filename for {desired_filename} after 100 attempts."
                )
                # <<< CORRECTED SYNTAX: try...except block with indentation >>>
                try:
                    os.remove(source_path)  # Clean up temp file
                except OSError:
                    pass  # Ignore error if cleanup fails
                # <<< END CORRECTION >>>
                return None  # Indicate failure to find a name
        app_logger.warning(
            f"[{thread_name}] Renaming to suffixed name: {os.path.basename(final_target_path)}"
        )

    # --- Perform Move (to initial_target_path or suffixed final_target_path) ---
    try:
        shutil.move(source_path, final_target_path)
        app_logger.debug(
            f"[{thread_name}] Moved {os.path.basename(source_path)} -> {os.path.basename(final_target_path)}"
        )
        return final_target_path  # Return the actual final path used
    except Exception as e:
        app_logger.error(
            f"[{thread_name}] Failed to move {source_path} to {final_target_path}: {e}"
        )
        # <<< CORRECTED SYNTAX: try...except block with indentation >>>
        try:
            os.remove(source_path)  # Attempt cleanup on move failure
        except OSError:
            pass  # Ignore error if cleanup fails
        # <<< END CORRECTION >>>
        return None


def check_file_stability(filepath, interval, checks_needed):
    """Checks if a file size is stable over a period."""
    thread_name = threading.current_thread().name
    stable_counter = 0
    last_size = -1
    check_count = 0
    while stable_counter < checks_needed:
        check_count += 1
        if not os.path.exists(filepath):
            app_logger.warning(
                f"[{thread_name}] File {filepath} disappeared during stability check."
            )
            return False
        try:
            current_size = os.path.getsize(filepath)
        except OSError as e:
            app_logger.warning(
                f"[{thread_name}] Error getting size for {filepath}: {e}"
            )
            time.sleep(interval)
            continue
        if current_size == last_size and current_size > 0:
            stable_counter += 1
        else:
            stable_counter = 0
        last_size = current_size
        if check_count > checks_needed * 5 and checks_needed > 0:
            app_logger.warning(
                f"[{thread_name}] File {os.path.basename(filepath)} stability check exceeded max attempts."
            )
            return False
        if stable_counter < checks_needed:
            time.sleep(interval)
    app_logger.debug(
        f"[{thread_name}] File {os.path.basename(filepath)} confirmed stable ({_format_size(last_size)})."
    )
    return True


def wait_for_download_complete(
    download_dir, timeout, stability_interval, stability_checks
):
    """Waits for a non-temporary file in download_dir to stabilize."""
    thread_name = threading.current_thread().name
    start_time = time.time()
    app_logger.debug(
        f"[{thread_name}] Starting download wait in '{download_dir}' (Timeout: {timeout}s)"
    )
    while time.time() - start_time < timeout:
        try:
            files = [
                f
                for f in os.listdir(download_dir)
                if not f.startswith(".")
                and os.path.isfile(os.path.join(download_dir, f))
            ]
        except FileNotFoundError:
            app_logger.error(
                f"[{thread_name}] Download directory not found: {download_dir}"
            )
            return None
        except OSError as e:
            app_logger.error(
                f"[{thread_name}] Error listing download directory {download_dir}: {e}"
            )
            time.sleep(stability_interval)
            continue
        potential_files = [
            os.path.join(download_dir, f)
            for f in files
            if not f.lower().endswith((".tmp", ".crdownload", ".part", ".partial"))
        ]
        if potential_files:
            dl_filepath = potential_files[0]
            if len(potential_files) > 1:
                app_logger.warning(
                    f"[{thread_name}] Multiple potential downloads found in {download_dir}, checking first: {os.path.basename(dl_filepath)}"
                )
            app_logger.debug(
                f"[{thread_name}] Potential download file found: {os.path.basename(dl_filepath)}. Checking stability..."
            )
            if check_file_stability(dl_filepath, stability_interval, stability_checks):
                try:
                    final_size = os.path.getsize(dl_filepath)
                    app_logger.info(
                        f"[{thread_name}] Selenium download detected: {os.path.basename(dl_filepath)} ({_format_size(final_size)})"
                    )
                    return dl_filepath
                except OSError as e:
                    app_logger.error(
                        f"[{thread_name}] Error getting size of stable file {dl_filepath}: {e}"
                    )
                    return None
        time.sleep(stability_interval)
    app_logger.error(
        f"[{thread_name}] Timed out after {timeout}s waiting for download to complete in {download_dir}"
    )
    return None


# --- Download Functions ---


def download_direct_file(
    url, target_dir, desired_filename_base, config, ui=None, shutdown_event=None
):
    """Downloads a file directly using requests. Returns (success, path_or_status, size, reason)."""
    thread_name = threading.current_thread().name
    success = False
    final_path_or_status = None
    downloaded_size = 0
    reason = "Download initialization failed"
    part_filepath = None

    try:
        timeout_conn = config.getfloat(
            "Timeouts", "direct_download_timeout", fallback=30.0
        )
        timeout_idle = config.getfloat("Timeouts", "direct_idle_timeout", fallback=60.0)
        chunk_size = 8192

        headers = {"User-Agent": config.get("Misc", "user_agent")}
        app_logger.debug(f"[{thread_name}] Sending GET request to {url}")
        with requests.get(
            url, headers=headers, stream=True, timeout=(timeout_conn, timeout_idle)
        ) as r:
            reason = f"Request error: {r.status_code} {r.reason} for url: {url}"
            r.raise_for_status()

            content_length = r.headers.get("content-length")
            total_size = int(content_length) if content_length else None
            size_str = _format_size(total_size) if total_size else "Unknown size"
            app_logger.info(f"[{thread_name}] File size: {size_str}")

            filename_header = get_filename_from_headers(r)
            filename_url = get_filename_from_url(url)
            actual_filename = (
                filename_header or filename_url or f"{desired_filename_base}.tmp"
            )
            actual_filename = parser.sanitize_filename(actual_filename)
            final_desired_name = parser.get_final_desired_filename(
                desired_filename_base, actual_filename
            )

            part_filepath = os.path.join(target_dir, f"{final_desired_name}.part")
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
            app_logger.info(
                f"[{thread_name}] Saving temporarily to: {os.path.relpath(part_filepath)}"
            )

            dl_start_time = time.time()
            with open(part_filepath, "wb") as f:
                for chunk in r.iter_content(chunk_size=chunk_size):
                    if shutdown_event and shutdown_event.is_set():
                        raise Exception("Download cancelled due to shutdown request")
                    if chunk:
                        f.write(chunk)
                        downloaded_size += len(chunk)
            dl_end_time = time.time()
            duration = dl_end_time - dl_start_time
            speed_str = (
                f"{_format_size(downloaded_size / duration)}/s"
                if duration > 0
                else "Inf"
            )
            app_logger.debug(
                f"[{thread_name}] .part download finished in {duration:.2f}s ({speed_str})"
            )

            final_path_or_status = attempt_rename(
                part_filepath, target_dir, final_desired_name
            )

            if final_path_or_status == "DUPLICATE_SKIPPED":
                success = True
                reason = "Duplicate file with same size already exists"
            elif final_path_or_status and os.path.exists(final_path_or_status):
                success = True
                reason = "Download successful"
                try:
                    downloaded_size = os.path.getsize(final_path_or_status)
                except Exception:
                    pass
            else:
                success = False
                reason = f"Failed to rename/move temp file {os.path.basename(part_filepath or 'N/A')}"
                final_path_or_status = None

    except requests.exceptions.RequestException as req_err:
        reason = f"Request error: {req_err} for {url}"
        app_logger.error(f"[{thread_name}] {reason}")
        success = False
        final_path_or_status = None
        downloaded_size = 0
    except Exception as e:
        if "shutdown request" in str(e):
            reason = str(e)
            app_logger.warning(f"[{thread_name}] {reason} for {url}")
        else:
            reason = f"Error during direct download: {e}"
            app_logger.error(f"[{thread_name}] {reason} for {url}", exc_info=True)
        success = False
        final_path_or_status = None
        downloaded_size = 0
    finally:
        if not success and part_filepath and os.path.exists(part_filepath):
            try:
                os.remove(part_filepath)
                app_logger.debug(
                    f"[{thread_name}] Cleaned up partial file: {part_filepath}"
                )
            except OSError as e:
                app_logger.warning(
                    f"[{thread_name}] Could not remove partial file {part_filepath}: {e}"
                )

    return success, final_path_or_status, downloaded_size, reason


def download_with_selenium(
    driver,
    url,
    target_dir,
    desired_filename_base,
    handler_func,
    config,
    selenium_worker_dl_dir,
    ui=None,
    item_info=None,
    shutdown_event=None,
):
    """Downloads using Selenium. Returns (success, path_or_status, size, reason)."""
    thread_name = threading.current_thread().name
    success = False
    final_path_or_status = None
    downloaded_size = 0
    reason = "Selenium download initialization failed"
    downloaded_temp_filepath = None

    try:
        if shutdown_event and shutdown_event.is_set():
            raise Exception("Skipped due to shutdown request")
        app_logger.info(f"[{thread_name}] Navigating to page: {url}")
        nav_wait = config.getfloat("Selenium", "navigation_wait", fallback=3.0)
        page_load_timeout = config.getint("Selenium", "page_load_timeout", fallback=90)
        driver.set_page_load_timeout(page_load_timeout)
        driver.get(url)
        if shutdown_event and shutdown_event.is_set():
            raise Exception("Cancelled after navigation due to shutdown request")
        time.sleep(nav_wait)

        if shutdown_event and shutdown_event.is_set():
            raise Exception("Cancelled before handler due to shutdown request")
        if not handler_func(driver, config, ui):
            reason = "Selenium site handler failed to initiate download"
            app_logger.error(f"[{thread_name}] {reason} for {url}.")
            return False, None, 0, reason

        if shutdown_event and shutdown_event.is_set():
            raise Exception("Cancelled after handler due to shutdown request")

        timeout = config.getint("Timeouts", "download_wait_timeout", fallback=360)
        stability_interval = config.getfloat(
            "Timeouts", "download_stability_interval", fallback=3.0
        )
        stability_checks = config.getint(
            "Timeouts", "download_stability_checks", fallback=5
        )

        app_logger.info(
            f"[{thread_name}] Selenium handler successful for {url}. Waiting for download..."
        )
        downloaded_temp_filepath = wait_for_download_complete(
            selenium_worker_dl_dir, timeout, stability_interval, stability_checks
        )
        if shutdown_event and shutdown_event.is_set():
            raise Exception("Cancelled after download wait due to shutdown request")

        if downloaded_temp_filepath:
            try:
                temp_size = os.path.getsize(downloaded_temp_filepath)
            except OSError as e:
                app_logger.warning(
                    f"[{thread_name}] Could not get size of temp file {downloaded_temp_filepath}: {e}"
                )
                temp_size = 0

            actual_filename = os.path.basename(downloaded_temp_filepath)
            actual_filename_sanitized = parser.sanitize_filename(actual_filename)
            final_desired_name = parser.get_final_desired_filename(
                desired_filename_base, actual_filename_sanitized
            )

            final_path_or_status = attempt_rename(
                downloaded_temp_filepath, target_dir, final_desired_name
            )

            if final_path_or_status == "DUPLICATE_SKIPPED":
                success = True
                reason = "Duplicate file with same size already exists"
                downloaded_size = temp_size
            elif final_path_or_status and os.path.exists(final_path_or_status):
                success = True
                reason = "Download successful"
                try:
                    downloaded_size = os.path.getsize(final_path_or_status)
                except Exception:
                    downloaded_size = temp_size  # Fallback to temp size
            else:
                success = False
                reason = f"Failed to move/rename downloaded file from {selenium_worker_dl_dir}"
                downloaded_size = temp_size
                final_path_or_status = None
        else:
            success = False
            reason = "Download did not complete or stabilize within timeout"
            downloaded_size = 0
            final_path_or_status = None

    except TimeoutException as selenium_timeout:
        reason = f"Selenium timeout during interaction: {selenium_timeout}"
        app_logger.error(f"[{thread_name}] {reason} for {url}", exc_info=False)
        success = False
        final_path_or_status = None
        downloaded_size = 0
    except WebDriverException as selenium_error:
        reason = f"Selenium WebDriver error: {selenium_error}"
        app_logger.error(f"[{thread_name}] {reason} for {url}", exc_info=False)
        success = False
        final_path_or_status = None
        downloaded_size = 0
    except Exception as e:
        if "shutdown request" in str(e):
            reason = str(e)
            app_logger.warning(f"[{thread_name}] {reason} for {url}")
        else:
            reason = f"Unexpected error during Selenium interaction: {e}"
            app_logger.error(f"[{thread_name}] {reason} for {url}", exc_info=True)
        success = False
        final_path_or_status = None
        downloaded_size = 0
    finally:
        if (
            not success
            and downloaded_temp_filepath
            and os.path.exists(downloaded_temp_filepath)
            and final_path_or_status != "DUPLICATE_SKIPPED"
        ):
            app_logger.warning(
                f"[{thread_name}] Cleaning up potentially orphaned temp file: {downloaded_temp_filepath}"
            )
            try:
                os.remove(downloaded_temp_filepath)
            except OSError as e:
                app_logger.warning(
                    f"[{thread_name}] Could not remove orphaned temp file {downloaded_temp_filepath}: {e}"
                )

    return success, final_path_or_status, downloaded_size, reason
