"""Shared utility functions for the Research Library."""

import hashlib
import os
import subprocess
import sys
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse

from flask import jsonify
from loguru import logger

from ...config.paths import get_library_directory
from ...database.models.library import Document, DocumentCollection
from ...security.path_validator import PathValidator


def is_downloadable_domain(url: str) -> bool:
    """Check if URL is from a downloadable academic domain using proper URL parsing."""
    try:
        if not url:
            return False

        parsed = urlparse(url.lower())
        hostname = parsed.hostname or ""
        path = parsed.path or ""
        query = parsed.query or ""

        # Check for direct PDF files
        if path.endswith(".pdf") or ".pdf?" in url.lower():
            return True

        # List of downloadable academic domains
        downloadable_domains = [
            "arxiv.org",
            "biorxiv.org",
            "medrxiv.org",
            "ncbi.nlm.nih.gov",
            "pubmed.ncbi.nlm.nih.gov",
            "europepmc.org",
            "semanticscholar.org",
            "researchgate.net",
            "academia.edu",
            "sciencedirect.com",
            "springer.com",
            "nature.com",
            "wiley.com",
            "ieee.org",
            "acm.org",
            "plos.org",
            "frontiersin.org",
            "mdpi.com",
            "acs.org",
            "rsc.org",
            "tandfonline.com",
            "sagepub.com",
            "oxford.com",
            "cambridge.org",
            "bmj.com",
            "nejm.org",
            "thelancet.com",
            "jamanetwork.com",
            "annals.org",
            "ahajournals.org",
            "cell.com",
            "science.org",
            "pnas.org",
            "elifesciences.org",
            "embopress.org",
            "journals.asm.org",
            "microbiologyresearch.org",
            "jvi.asm.org",
            "genome.cshlp.org",
            "genetics.org",
            "g3journal.org",
            "plantphysiol.org",
            "plantcell.org",
            "aspb.org",
            "bioone.org",
            "company-of-biologists.org",
            "biologists.org",
            "jeb.biologists.org",
            "dmm.biologists.org",
            "bio.biologists.org",
            "doi.org",
            "ssrn.com",
            "openreview.net",
        ]

        # Check if hostname matches any downloadable domain
        for domain in downloadable_domains:
            if hostname == domain or hostname.endswith("." + domain):
                return True

        # Special case for PubMed which might appear in path
        if "pubmed" in hostname or "/pubmed/" in path:
            return True

        # Check for PDF in path or query parameters
        if "/pdf/" in path or "type=pdf" in query or "format=pdf" in query:
            return True

        return False

    except Exception:
        logger.warning(f"Error parsing URL {url}")
        return False


def is_downloadable_url(url: str) -> bool:
    """Check if a URL is downloadable (academic domain or direct PDF link).

    This is the single source of truth for downloadability checks.
    Combines domain checking with PDF extension/path detection.

    Args:
        url: The URL to check

    Returns:
        True if the URL is from a downloadable academic domain or is a direct PDF link
    """
    return is_downloadable_domain(url)


def get_document_for_resource(session, resource):
    """Get Document for a ResearchResource.

    Checks resource.document_id first (library resources point directly
    to existing Documents), falls back to Document.resource_id lookup
    (web downloads create Documents with resource_id set).
    """
    if resource.document_id:
        return (
            session.query(Document).filter_by(id=resource.document_id).first()
        )
    return session.query(Document).filter_by(resource_id=resource.id).first()


def get_url_hash(url: str) -> str:
    """
    Generate a SHA256 hash of a URL.

    Args:
        url: The URL to hash

    Returns:
        The SHA256 hash of the URL
    """
    return hashlib.sha256(url.lower().encode()).hexdigest()


def ensure_in_collection(
    session, document_id: str, collection_id: str
) -> "DocumentCollection":
    """Get or create a DocumentCollection link between a document and a collection.

    Args:
        session: SQLAlchemy session
        document_id: UUID of the document
        collection_id: UUID of the collection

    Returns:
        The existing or newly created DocumentCollection row
    """
    existing = (
        session.query(DocumentCollection)
        .filter_by(document_id=document_id, collection_id=collection_id)
        .first()
    )
    if existing:
        return existing

    doc_collection = DocumentCollection(
        document_id=document_id,
        collection_id=collection_id,
        indexed=False,
    )
    session.add(doc_collection)
    return doc_collection


def get_library_storage_path(username: str) -> Path:
    """
    Get the storage path for a user's library.

    Uses the settings system which respects environment variable overrides:
    - research_library.storage_path: Base path for library storage
    - research_library.shared_library: If true, all users share the same directory

    Args:
        username: The username

    Returns:
        Path to the library storage directory
    """
    from ...utilities.db_utils import get_settings_manager

    settings = get_settings_manager()

    # Get the base path from settings (uses centralized path, respects LDR_DATA_DIR)
    base_path = (
        Path(
            settings.get_setting(
                "research_library.storage_path",
                str(get_library_directory()),
            )
        )
        .expanduser()
        .resolve()
    )

    # Check if shared library mode is enabled
    shared_library = settings.get_setting(
        "research_library.shared_library", False
    )

    if shared_library:
        # Shared mode: all users use the same directory
        base_path.mkdir(parents=True, exist_ok=True)
        return base_path
    # Default: user isolation with subdirectories
    user_dir = base_path / username
    user_dir.mkdir(parents=True, exist_ok=True)
    return user_dir


def open_file_location(file_path: str) -> bool:
    """
    Open the file location in the system file manager.

    Args:
        file_path: Path to the file

    Returns:
        True if successful, False otherwise
    """
    try:
        # Validate path is safe (blocks system dirs, path traversal)
        validated = PathValidator.validate_local_filesystem_path(file_path)
        folder = str(validated.parent)
        if sys.platform == "win32":
            os.startfile(folder)
        elif sys.platform == "darwin":  # macOS
            result = subprocess.run(
                ["open", folder], capture_output=True, text=True, shell=False
            )
            if result.returncode != 0:
                logger.error(f"Failed to open folder on macOS: {result.stderr}")
                return False
        else:  # Linux
            result = subprocess.run(
                ["xdg-open", folder],
                capture_output=True,
                text=True,
                shell=False,
            )
            if result.returncode != 0:
                logger.error(f"Failed to open folder on Linux: {result.stderr}")
                return False
        return True
    except Exception:
        logger.exception("Failed to open file location")
        return False


def get_absolute_library_path(
    relative_path: str, username: str
) -> Optional[Path]:
    """
    Get the absolute path from a relative library path.

    Uses PathValidator to prevent path traversal attacks.

    Args:
        relative_path: The relative path from library root
        username: The username

    Returns:
        The absolute path, or None if the path is unsafe
    """
    library_root = get_library_storage_path(username)
    try:
        # Use PathValidator to prevent path traversal attacks
        safe_path = PathValidator.validate_safe_path(
            relative_path, str(library_root)
        )
        if safe_path is None:
            return None
        result = Path(safe_path)
        if result.is_symlink():
            logger.warning(f"Symlink blocked: {relative_path}")
            return None
        return result
    except ValueError:
        logger.warning(f"Path traversal blocked: {relative_path}")
        return None


def get_absolute_path_from_settings(relative_path: str) -> Optional[Path]:
    """
    Get absolute path using settings manager for library root.

    Uses PathValidator to prevent path traversal attacks.

    Args:
        relative_path: The relative path from library root

    Returns:
        The absolute path, or None if the path is unsafe
    """
    from ...utilities.db_utils import get_settings_manager

    settings = get_settings_manager()
    library_root = (
        Path(
            settings.get_setting(
                "research_library.storage_path",
                str(get_library_directory()),
            )
        )
        .expanduser()
        .resolve()
    )

    if not relative_path:
        return library_root

    try:
        # Use PathValidator to prevent path traversal attacks
        safe_path = PathValidator.validate_safe_path(
            relative_path, str(library_root)
        )
        if safe_path is None:
            return None
        result = Path(safe_path)
        if result.is_symlink():
            logger.warning(f"Symlink blocked: {relative_path}")
            return None
        return result
    except ValueError:
        logger.warning(f"Path traversal blocked: {relative_path}")
        return None


def handle_api_error(operation: str, error: Exception, status_code: int = 500):
    """
    Handle API errors consistently - log internally, return generic message to user.

    This prevents information exposure by logging full error details internally
    while returning a generic message to the user.

    Args:
        operation: Description of the operation that failed (for logging)
        error: The exception that occurred
        status_code: HTTP status code to return (default: 500)

    Returns:
        Flask JSON response tuple (response, status_code)
    """
    # Log the full error internally with stack trace
    logger.exception(f"Error during {operation}")

    # Return generic message to user (no internal details exposed)
    return jsonify(
        {
            "success": False,
            "error": "An internal error occurred. Please try again or contact support.",
        }
    ), status_code
