from bfabric import Bfabric
from loguru import logger

from bfabric_web_apps.utils.get_logger import get_logger
from bfabric_web_apps.objects.BfabricInterface import bfabric_interface
from bfabric_web_apps.utils.get_power_user_wrapper import get_power_user_wrapper

from pathlib import Path

def create_workunit(token_data, application_name, application_description, application_id, container_id):
    """
    Create a single workunit in B-Fabric.

    Args:
        token_data (dict): Authentication token data.
        application_name (str): Name of the application.
        application_description (str): Description of the application.
        application_id (int): Application ID.
        container_id (int): Container ID (Order ID).
    
    Returns:
        obj: Created workunit object or None if creation fails.
    """
    L = get_logger(token_data)
    wrapper = bfabric_interface.get_wrapper(token_data)

    workunit_data = {
        "name": f"Workunit - {application_name} - Container {container_id}",
        "description": f"{application_description} for Container {container_id}",
        "applicationid": int(application_id),
        "containerid": container_id, 
    }

    try:
        workunit_response = L.logthis(
            api_call=wrapper.save,
            endpoint="workunit",
            obj=workunit_data,
            params=None,
            flush_logs=True
        )
        workunit_id = workunit_response[0].get("id")
        print(f"Created Workunit ID: {workunit_id} for Order ID: {container_id}")

        # First we get the existing workunit_ids for the current job object: 
        pre_existing_workunit_ids = [elt.get("id") for elt in wrapper.read("job", {"id": token_data.get("jobId")})[0].get("workunit", [])]
        
        # Now we associate the job object with the workunits 
        job = L.logthis(
            api_call=L.power_user_wrapper.save,
            endpoint="job",
            obj={"id": token_data.get("jobId"), "workunitid": [workunit_id] + pre_existing_workunit_ids},
            params=None,
            flush_logs=True
        )
        return workunit_response[0]

    except Exception as e:
        L.log_operation(
            "Error | ORIGIN: run_main_job function",
            f"Failed to create workunit for Order {container_id}: {e}",
            params=None,
            flush_logs=True,
        )
        print(f"Failed to create workunit for Order {container_id}: {e}")
        return None


def create_workunits(token_data, application_name, application_description, application_id, container_ids):
    """
    Create multiple workunits in B-Fabric.

    Args:
        token_data (dict): Authentication token data.
        application_name (str): Name of the application.
        application_description (str): Description of the application.
        application_id (int): Application ID.
        container_ids (list): List of container IDs.
    
    Returns:
        list[obj]: List of created workunit objects.
    """
    if not isinstance(container_ids, list):
        container_ids = [container_ids]  # Ensure it's a list

    workunits = [
        create_workunit(token_data, application_name, application_description, application_id, container_id)
        for container_id in container_ids
    ]

    return [wu for wu in workunits if wu is not None]  # Filter out None values


from pathlib import Path

def create_resource(token_data, workunit_id, file_path, storage_id="20"): # GWC Server is storage id 20. 
    """
    Attach a single file as a resource to an existing B-Fabric workunit.

    Args:
        token_data (dict): Authentication token data.
        workunit_id (int): ID of the workunit to associate the resource with.
        file_path (str): Full path to the file to attach.
    
    Returns:
        obj: Resource object if successful, None otherwise.
    """
    L = get_logger(token_data)
    wrapper = get_power_user_wrapper(token_data)

    try:
        file_path = Path(file_path)
        
        # Attaching the resource
        print(f"Attaching: {file_path.name} to workunit: {workunit_id}")
        
        result = wrapper.save(
            endpoint="resource",
            obj={
                "workunitid": str(workunit_id),
                "name": file_path.name,
                "description": f"Resource attached to workunit {workunit_id}",
                "relativepath": file_path,
                "storageid": str(storage_id),
            }
        )

        if result:
            resource_id = result[0].get("id")
            print(f"Resource attached: {file_path.name} (ID: {resource_id})")
            return result[0]
        else:
            raise ValueError(f"Failed to attach resource: {file_path.name}")

    except Exception as e:
        L.log_operation(
            "error | ORIGIN: run_main_job function",
            f"Failed to attach resource: {e}",
            params=None,
            flush_logs=True,
        )
        print(f"Failed to attach resource: {e}")
        return None


def create_resources(token_data, workunit_id, file_paths):
    """
    Attach multiple files as resources to an existing B-Fabric workunit.

    Args:
        token_data (dict): Authentication token data.
        workunit_id (int): ID of the workunit to associate the resources with.
        file_paths (list): List of full paths to files to attach.
    
    Returns:
        list[obj]: List of successfully attached resource objects.
    """
    if not isinstance(file_paths, list):
        file_paths = [file_paths]  # Ensure it's a list

    resources = [
        create_resource(token_data, workunit_id, file_path)
        for file_path in file_paths
    ]

    return [res_id for res_id in resources if res_id is not None]  # Filter out None values


def search_containers(
    bfabric: Bfabric,
    query: str,
    max_results: int = 20,
    valid_project_states: set[str] | None = None,
    valid_order_states: set[str] | None = None,
) -> list[dict]:
    """
    Search containers by name or ID.

    - If query is numeric, searches by ID directly
    - Otherwise searches by name with multiple case variations (SQL LIKE is case-sensitive)
    Filters results by valid container states.

    Args:
        bfabric: Authenticated B-Fabric client
        query: Search query (min 2 characters) - can be name or ID
        max_results: Maximum results to return
        valid_project_states: Set of valid project states (default: running, finished)
        valid_order_states: Set of valid order states (default: accepted, arrived, processing, etc.)

    Returns:
        List of container dicts with valid states
    """
    # Minimum length check: allow single digits for ID search, otherwise require 2+ chars
    if not query or (len(query) < 2 and not query.isdigit()):
        return []

    # Default valid states
    if valid_project_states is None:
        valid_project_states = {"running", "finished"}
    if valid_order_states is None:
        valid_order_states = {
            "accepted", "arrived", "processing", "processed",
            "analysing", "analysed", "reopened"
        }

    logger.info(f"Searching containers for query: {query}")

    try:
        all_results = []

        # Check if query is numeric (search by ID)
        if query.isdigit():
            # Search by exact ID
            results = bfabric.read(
                "container",
                {"id": int(query)},
                max_results=1,
            )
            if results and results.results:
                all_results.extend(results.results)

            # For 2+ digit queries, also search in names
            if len(query) >= 2:
                results = bfabric.read(
                    "container",
                    {"name": f"%{query}%"},
                    max_results=max_results,
                )
                if results and results.results:
                    all_results.extend(results.results)
        else:
            # Search by name with multiple case variations (SQL LIKE is case-sensitive)
            case_variants = {query.lower(), query.upper(), query.capitalize()}

            for variant in case_variants:
                results = bfabric.read(
                    "container",
                    {"name": f"%{variant}%"},
                    max_results=max_results,
                )
                if results and results.results:
                    all_results.extend(results.results)

        if not all_results:
            return []

        # Dedupe by ID
        seen_ids = set()
        unique_results = []
        for r in all_results:
            if r["id"] not in seen_ids:
                seen_ids.add(r["id"])
                unique_results.append(r)

        # Filter by valid states
        containers = []
        for container_dict in unique_results:
            classname = container_dict.get("classname", "").lower()
            status = container_dict.get("status", "").lower()

            if classname == "project" and status in valid_project_states:
                containers.append(container_dict)
            elif classname == "order" and status in valid_order_states:
                containers.append(container_dict)

            if len(containers) >= max_results:
                break

        logger.info(f"Found {len(containers)} containers matching '{query}'")
        return containers

    except Exception as e:
        logger.error(f"Error searching containers: {e}")
        return []


# =============================================================================
# NEW FUNCTIONS: Advanced workunit/resource operations
# =============================================================================

from .models import ResourceDef, CloneDef, DuplicateMatch


def create_workunit_simple(
    token_data: dict,
    container_id: int,
    application_id: int,
    name: str,
    description: str = "",
) -> dict | None:
    """
    Create a workunit with custom name and description.

    Unlike create_workunit(), this function:
    - Accepts custom name/description (not auto-generated)
    - Does NOT associate with a job object
    - Uses direct wrapper.save() without logging overhead

    Args:
        token_data: Authentication token data
        container_id: Container (Project/Order) ID
        application_id: Application ID
        name: Workunit name
        description: Workunit description (optional)

    Returns:
        Created workunit dict or None on failure
    """
    # L = get_logger(token_data)  # Commented out - enable if logging needed
    wrapper = bfabric_interface.get_wrapper(token_data)

    workunit_data = {
        "name": name,
        "description": description or "",
        "containerid": container_id,
        "applicationid": int(application_id),
    }

    try:
        # result = L.logthis(api_call=wrapper.save, endpoint="workunit", obj=workunit_data, flush_logs=True)
        result = wrapper.save("workunit", workunit_data)

        if result and result.results:
            workunit = result.results[0]
            logger.info(f"Created workunit {workunit.get('id')}: {name}")
            return workunit

        logger.error("Failed to create workunit: empty response")
        return None

    except Exception as e:
        logger.error(f"Error creating workunit: {e}")
        return None


def register_resources_batch(
    token_data: dict,
    workunit_id: int,
    resources: list[ResourceDef],
) -> list[dict]:
    """
    Register multiple resources in a single API call (batch save).

    Uses method="update" for efficiency (skips workunit reindexing).

    Args:
        token_data: Authentication token data
        workunit_id: Workunit ID to attach resources to
        resources: List of ResourceDef objects

    Returns:
        List of created resource dicts (empty list on failure)
    """
    if not resources:
        return []

    wrapper = bfabric_interface.get_wrapper(token_data)

    resource_defs = []
    for res in resources:
        resource_defs.append({
            "name": res.name,
            "workunitid": workunit_id,
            "relativepath": res.relativepath,
            "size": res.size,
            "filechecksum": res.md5,
            "storageid": res.storage_id,
            "status": res.status,
        })

    try:
        result = wrapper.save("resource", resource_defs, method="update")

        if result and result.results:
            logger.info(f"Registered {len(result.results)} resources in batch")
            return result.results

        logger.error("Failed to register resources: empty response")
        return []

    except Exception as e:
        logger.error(f"Error registering resources: {e}")
        return []


def clone_resources_batch(
    token_data: dict,
    workunit_id: int,
    clones: list[CloneDef],
) -> list[dict]:
    """
    Register resources that point to existing files (no file copy).

    Creates new resource entries that reference the same physical file
    on storage. Uses method="update" for efficiency.

    Args:
        token_data: Authentication token data
        workunit_id: Workunit ID to attach cloned resources to
        clones: List of CloneDef objects

    Returns:
        List of created resource dicts (empty list on failure)
    """
    if not clones:
        return []

    wrapper = bfabric_interface.get_wrapper(token_data)

    clone_defs = []
    for clone in clones:
        clone_defs.append({
            "name": clone.name,
            "workunitid": workunit_id,
            "relativepath": clone.source_relativepath,  # Reuse existing path
            "size": clone.size,
            "filechecksum": clone.md5,
            "storageid": clone.source_storage_id,  # Original storage ID
            "status": "AVAILABLE",
        })

    try:
        result = wrapper.save("resource", clone_defs, method="update")

        if result and result.results:
            logger.info(f"Cloned {len(result.results)} resources in batch")
            return result.results

        logger.error("Failed to clone resources: empty response")
        return []

    except Exception as e:
        logger.error(f"Error cloning resources: {e}")
        return []


def set_workunit_available(
    token_data: dict,
    workunit_id: int,
) -> bool:
    """
    Set workunit status to AVAILABLE.

    Call this after all resources have been registered.

    Args:
        token_data: Authentication token data
        workunit_id: Workunit ID to update

    Returns:
        True on success, False on failure
    """
    wrapper = bfabric_interface.get_wrapper(token_data)

    try:
        result = wrapper.save("workunit", {"id": workunit_id, "status": "AVAILABLE"})

        if result and result.results:
            logger.info(f"Set workunit {workunit_id} status to AVAILABLE")
            return True

        logger.error(f"Failed to update workunit {workunit_id} status")
        return False

    except Exception as e:
        logger.error(f"Error updating workunit status: {e}")
        return False


# =============================================================================
# DUPLICATE DETECTION FUNCTIONS
# =============================================================================


def get_container_resources(
    bfabric: Bfabric,
    container_id: int,
    max_results: int = 10000,
) -> dict[str, DuplicateMatch]:
    """
    Get all resources in a container, indexed by MD5 checksum.

    Used for duplicate detection before upload.

    Args:
        bfabric: Authenticated B-Fabric client
        container_id: Container (Project/Order) ID
        max_results: Maximum resources to fetch

    Returns:
        Dict mapping MD5 -> DuplicateMatch
    """
    logger.info(f"Fetching resources for container {container_id}")

    try:
        resources_response = bfabric.read(
            "resource",
            {"containerid": container_id},
            max_results=max_results,
        )

        if not resources_response or not resources_response.results:
            logger.info(f"No existing resources in container {container_id}")
            return {}

        # Build MD5 -> DuplicateMatch lookup
        resources_by_md5: dict[str, DuplicateMatch] = {}

        for resource in resources_response.results:
            md5 = resource.get("filechecksum")
            if not md5:
                continue

            wu_info = resource.get("workunit", {})
            storage_info = resource.get("storage", {})

            resources_by_md5[md5] = DuplicateMatch(
                resource_id=resource["id"],
                filename=resource.get("name", ""),
                relativepath=resource.get("relativepath", ""),
                workunit_id=wu_info.get("id", 0) if isinstance(wu_info, dict) else 0,
                workunit_name=wu_info.get("name", "Unknown") if isinstance(wu_info, dict) else "Unknown",
                storage_id=storage_info.get("id", 0) if isinstance(storage_info, dict) else 0,
                md5=md5,
            )

        logger.info(f"Found {len(resources_by_md5)} resources with MD5 in container {container_id}")
        return resources_by_md5

    except Exception as e:
        logger.error(f"Error fetching container resources: {e}")
        return {}
