#' Construct Protein-protein interaction Network using Prize-Collecting Steiner Forest
#'
#' Constructs a condition-specific gene regulatory network based on differential
#' expression results using the PCSF algorithm.
#'
#' @param g An igraph object representing the base network.
#' @param prizes A named numeric vector of gene scores (prizes). Names must match vertex names in g.
#' @param w Numeric. Edge cost scaling weight. Default is 2.
#' @param b Numeric. Balance between prizes and edge costs. Default is 1.
#' @param mu Numeric. Trade-off parameter for sparsity. Default is 5e-04.
#' @param seed Integer. Random seed. Default is 1.
#' @param min_nodes Integer. Minimum number of nodes in subnetwork. Default is 1.
#' @return An igraph object representing the extracted subnetwork. Returns NULL invisibly 
#'   if no prize genes are present, the subnetwork is too small, or the PCSF algorithm fails.
#' @importFrom igraph V E E<- V<- graph_from_data_frame simplify vcount ecount neighbors induced_subgraph edge_attr_names is_igraph
#'
#' @return An igraph object representing the extracted subnetwork. Returns NULL invisibly if no prize genes are present, the subnetwork is too small, or the PCSF algorithm fails
#'
#' @export
construct_ppi_pcsf <- function(g, prizes, w = 2, b = 1, mu = 5e-04, seed = 1, min_nodes = 1) {
  set.seed(seed)
  
  common_genes <- intersect(names(prizes), V(g)$name)
  if (length(common_genes) == 0) {
    warning("None of the prize genes are present in the network.")
    return(invisible())
  }
  
  prize_nodes <- which(V(g)$name %in% common_genes)
  neighbors <- unique(unlist(lapply(prize_nodes, function(x) neighbors(g, x, mode = "all"))))
  nodes_to_keep <- unique(c(prize_nodes, neighbors))
  
  sub_g <- induced_subgraph(g, nodes_to_keep)
  
  if (vcount(sub_g) < min_nodes) {
    warning("Subnetwork too small: ", vcount(sub_g), " node(s). Skipping PCSF.")
    return(invisible())
  }
  
  prizes_sub <- rep(0, vcount(sub_g))
  names(prizes_sub) <- V(sub_g)$name
  prizes_sub[common_genes] <- prizes[common_genes]
  
  if (!"weight" %in% edge_attr_names(sub_g)) {
    E(sub_g)$weight <- 1
  }
  
  result_network <- tryCatch({
      result <- PCSF(
        ppi = sub_g,
        terminals = prizes_sub,
        w = w,
        b = b,
        mu = mu
      )

   
    
    if (!is.null(result) && is_igraph(result)) {
      V(result)$prize <- prizes_sub[V(result)$name]
      return(result)
    } else {
      warning("PCSF did not return a valid network.")
      return(invisible())
    }
  }, error = function(e) {
    warning("PCSF failed: ", e$message)
    return(invisible())
  })
  
  return(result_network)
}


#' Download and Process STRING Protein-Protein Interaction Network
#'
#' Downloads and processes the STRING protein-protein interaction network,
#' converting it to a simplified igraph object. The function downloads the
#' network from STRING database, filters interactions by confidence score,
#' converts STRING IDs to ENTREZ IDs, and returns the largest connected
#' component as an undirected graph.
#'
#' @param organism Character string specifying the NCBI taxonomy identifier.
#'        Default is "9606" (Homo sapiens).
#' @param score_threshold Numeric value between 0 and 1000 specifying the minimum
#'        combined score threshold for including interactions. Default is 700.
#' @param use_default it will return the default network (9606 and score of 700)
#'
#' @return An igraph object representing the largest connected component of
#'         the filtered STRING network, with the following properties:
#'         \itemize{
#'           \item Undirected edges
#'           \item No self-loops
#'           \item No multiple edges
#'           \item Edge weights (1 - combined_score/1000)
#'           \item Vertex names as ENTREZ IDs
#'         }
#'
#' @details 
#' STRING combined scores range from 0 (low confidence) to 1000 (high confidence). 
#' Because PCSF treats edge weights as traversal costs to be minimized, 
#' scores are inverted via 1 - score/1000 so that higher-confidence interactions correspond to lower costs.
#' The function performs the following steps:
#' \enumerate{
#'   \item Downloads protein interactions from STRING database
#'   \item Filters interactions based on combined score
#'   \item Downloads and processes STRING ID to ENTREZ ID mappings
#'   \item Creates an igraph object with filtered interactions
#'   \item Removes self-loops and multiple edges
#'   \item Extracts the largest connected component
#' }
#'
#'
#' @importFrom data.table fread
#' @importFrom igraph V E E<- V<- graph_from_data_frame simplify vcount ecount neighbors induced_subgraph edge_attr_names is_igraph graph_from_data_frame components
#' @importFrom utils download.file
#' @importFrom stats na.omit
#'
#' @export
get_string_network <- function(
    organism = "9606",
    score_threshold = 700,
    use_default = TRUE
) {
  
  if (use_default) {
    return(readRDS(system.file(
      "extdata",
      "STRING_9606_700.rds",
      package = "XYomics"
    )))
  }
  
  temp_dir <- tempdir()
  
  tryCatch({
    
    # Download interaction file
    string_url <- paste0(
      "https://stringdb-static.org/download/protein.links.v11.5/",
      organism, ".protein.links.v11.5.txt.gz"
    )
    
    temp_links <- file.path(temp_dir, "string_links.txt.gz")
    download.file(string_url, temp_links, mode = "wb", method = "auto")
    
    interactions <- data.table::fread(temp_links)
    
    interactions <- interactions[
      interactions$combined_score >= score_threshold,
    ]

    # Download alias file
    alias_url <- paste0(
      "https://stringdb-static.org/download/protein.aliases.v11.5/",
      organism, ".protein.aliases.v11.5.txt.gz"
    )
    
    temp_alias <- file.path(temp_dir, "string_alias.txt.gz")
    download.file(alias_url, temp_alias, mode = "wb", method = "auto")
    
    string_to_entrez <- data.table::fread(
      temp_alias,
      header = TRUE,
      quote = "",
      sep = "\t"
    )
    
    colnames(string_to_entrez) <- c(
      "string_protein_id",
      "alias_name",
      "source"
    )
    
    # Keep only Entrez mappings
    entrez_sources <- "Ensembl_EntrezGene"
    
    string_to_entrez <- string_to_entrez[
      string_to_entrez$source %in% entrez_sources,
    ]
    
    # Build ID map
    id_map <- string_to_entrez$alias_name
    names(id_map) <- gsub(
      paste0(organism, "\\."),
      "",
      string_to_entrez$string_protein_id
    )
    
    protein1 <- gsub(paste0(organism, "\\."), "", interactions$protein1)
    protein2 <- gsub(paste0(organism, "\\."), "", interactions$protein2)
    
    edges <- data.frame(
      from = id_map[protein1],
      to = id_map[protein2],
      weight = 1 - interactions$combined_score / 1000,
      stringsAsFactors = FALSE
    )
    
    # Remove unmapped edges
    edges <- edges[stats::complete.cases(edges), ]
    
    # Build graph safely
    if (nrow(edges) < 1) {
      warning("No edges passed threshold or ID mapping failed.")
      return(NULL)
    }
    
    g <- igraph::graph_from_data_frame(edges, directed = FALSE)
    
    g <- igraph::simplify(
      g,
      remove.multiple = TRUE,
      remove.loops = TRUE,
      edge.attr.comb = "mean"
    )

    # Keep largest connected component
    components <- igraph::components(g)
    
    if (components$no > 1) {
      largest_component <- which.max(components$csize)
      g <- igraph::induced_subgraph(
        g,
        vids = V(g)[components$membership == largest_component]
      )
    }
    
    return(g)
    
  }, finally = {
    # cleanup
    unlink(file.path(temp_dir, "string_links.txt.gz"))
    unlink(file.path(temp_dir, "string_alias.txt.gz"))
  })
}



#' Plot a condition-specific protein–protein interaction network with DEG annotations
#'
#' Visualizes a gene or protein interaction network for a selected differential
#' expression category. Nodes are sized by degree centrality, and hub genes can be
#' optionally annotated with mini barplots showing sex-specific log fold changes.
#'
#' @param g An igraph object representing the interaction network.
#' @param DEG_type Character string specifying the DEG category to visualize.
#' @param result_categories A data.frame containing DEG results with at least
#'   the columns DEG_Type, Gene_Symbols, Male_avg_logFC, and Female_avg_logFC.
#' @param cell_type Optional character string used in the plot title.
#' @param top_hubs Integer specifying the number of hub genes to highlight.
#' @param show_barplot Logical indicating whether hub barplots are displayed.
#'
#' @return A ggplot object representing the network visualization.
#'
#' @details
#' Nodes represent genes or proteins and edges represent interactions. Node size
#' reflects degree centrality (number of connections). The top \code{top_hubs}
#' nodes by degree are labeled. When \code{show_barplot = TRUE}, mini barplots are
#' overlaid on hub nodes to display sex-specific log fold changes. The visualization
#' is restricted to the largest connected component of the network.
#'
#' @examples
#' # Minimal reproducible example (CRAN-safe)
#' library(igraph)
#'
#' # Create a small toy network
#' g <- make_ring(5)
#' V(g)$name <- paste0("Gene", 1:5)
#'
#' # Create minimal DEG table
#' result_categories <- data.frame(
#'   DEG_Type = rep("example", 5),
#'   Gene_Symbols = paste0("Gene", 1:5),
#'   Male_avg_logFC = runif(5, -1, 1),
#'   Female_avg_logFC = runif(5, -1, 1)
#' )
#'
#' # Run function
#' plot_network(g, DEG_type = "example", result_categories = result_categories)
#'
#' @importFrom igraph V degree components induced_subgraph vcount
#' @import ggraph
#' @import ggplot2
#' @import ggrepel
#' @import dplyr
#' @import tidyr
#' @import grid
#' @importFrom cowplot get_legend ggdraw draw_plot draw_grob
#' @export
plot_network <- function(g, DEG_type, result_categories, cell_type ="", top_hubs=20, show_barplot=FALSE) {
  
  # ---- Safety checks ----
  if (is.null(g)) {
    message("plot_network: Input graph is NULL.")
    return(NULL)
  }
  
  if (!igraph::is_igraph(g)) {
    stop("plot_network: g must be an igraph object.")
  }
  
  if (igraph::vcount(g) == 0) {
    message("plot_network: Graph contains no nodes.")
    return(NULL)
  }
  
  # Node size scaling
  min_size <- if (vcount(g) > 30) 1 else 3
  
  # Largest connected component
  comps <- igraph::components(g, mode = "weak")
  main_id <- which.max(comps$csize)
  g <- igraph::induced_subgraph(g, vids = V(g)[comps$membership == main_id])
  
  # Degree & top hubs
  degree_vals <- degree(g, mode = "all")
  V(g)$degree <- degree_vals
  top_n <- min(top_hubs, vcount(g))
  top_nodes <- names(sort(degree_vals, decreasing = TRUE))[1:top_n]
  
  deg_breaks <- sort(unique(degree_vals))
  
  if (length(deg_breaks) > 6) {
    deg_breaks <- pretty(deg_breaks, n = 5)
  }
  
  # Layout
  set.seed(123)
  layout <- create_layout(g, layout = "fr")
  layout$label <- if (vcount(g) < 20) layout$name else ifelse(layout$name %in% top_nodes, layout$name, "")
  
  # prevent warning when checking package
  x <- y <- label <- NULL 
  # Base network plot
  gplot_net <- ggraph(layout) +
    geom_edge_link(alpha = 0.5, color = "grey", show.legend = FALSE) +
    geom_node_point(aes(size = degree), fill = "lightblue", shape = 21, color = "black", stroke = 0.5, show.legend = TRUE) +
    scale_size(range = c(min_size, 10), breaks = deg_breaks, name = "Node Degree") +
    geom_label_repel(aes(x = x, y = y , label = label),
                     color = "black", size = 3, fill = "white",
                     box.padding = 0.5, point.padding = 0.5, max.overlaps = 1000) +
    theme_void() +
    labs(title = paste("Protein-protein interaction network", cell_type, DEG_type)) +
    theme(legend.position = "right", plot.title = element_text(hjust = 0.5, size = 14))
  
  # prevent warning when checking package
  DEG_Type <- Gene_Symbols <- Male_avg_logFC <- Female_avg_logFC <- logFC <- Sex <- NULL 
  if (show_barplot!=F){
    # Prepare DEG data
    df <- result_categories %>%
      filter(DEG_Type == DEG_type, Gene_Symbols %in% V(g)$name) %>%
      dplyr::select(Gene_Symbols, Male_avg_logFC, Female_avg_logFC) %>%
      pivot_longer(cols = ends_with("FC"), names_to = "Sex", values_to = "logFC")
    
    # Mini-barplots, no legend per barplot
    barplots <- lapply(V(g)$name, function(gene) {
      sub_df <- df[df$Gene_Symbols == gene, ]
      ggplot(sub_df, aes(Sex, logFC, fill = Sex)) +
        geom_col(show.legend = FALSE) +
        scale_fill_manual(values = c("Female_avg_logFC" = "salmon", "Male_avg_logFC" = "blue")) +
        geom_hline(yintercept = 0, color = "black", linewidth = 0.3) +
        theme_void() +
        theme(plot.margin = margin(0, 0, 0, 0),
              panel.background = element_rect(fill = "transparent", color = NA))
    })
    
    # Scale mini-plots
    n_nodes <- vcount(g)
    base_scale <- 0.015
    plot_scale <- base_scale * (n_nodes / 3)^2.0
    plot_scale <- min(plot_scale, 0.8)
    
 
    # Embed mini-barplots
    for (i in seq_len(nrow(layout))) {
      if (layout$name[i] %in% intersect(top_nodes, df$Gene_Symbols)) {
        gplot_net <- gplot_net + annotation_custom(
          grob = ggplotGrob(barplots[[i]]),
          xmin = layout$x[i] - plot_scale,
          xmax = layout$x[i] + plot_scale,
          ymin = layout$y[i] - plot_scale,
          ymax = layout$y[i] + plot_scale
        )
      }
    }
    
    # Global legend for barplot
    legend_df <- data.frame(Sex = c("Female_avg_logFC", "Male_avg_logFC"), logFC = c(1, 1))
    legend_plot <- ggplot(legend_df, aes(Sex, logFC, fill = Sex)) +
      geom_col() +
      scale_fill_manual(values = c("Female_avg_logFC" = "salmon", "Male_avg_logFC" = "blue"),
                        labels = c("Female", "Male"), name = "logFC") +
      theme_void() +
      theme(legend.position = "bottom",
            legend.title = element_text(size = 10),
            legend.text = element_text(size = 8))
    barplot_legend <- cowplot::get_legend(legend_plot)
    
    # Combine network + global legend
    final_plot <- cowplot::ggdraw() +
      cowplot::draw_plot(gplot_net) +
      cowplot::draw_grob(barplot_legend, x = 0.35, y = 0, width = 0.3, height = 0.05)
    
  } else{
    final_plot <- gplot_net
  }
 
  
  return(final_plot)
}



#' Get Top Hub Genes by Betweenness Centrality
#'
#' Computes normalized betweenness centrality for an igraph network and
#' returns the top N hub nodes ranked by centrality.
#'
#' @param g An `igraph` object representing the network.
#' @param top_n Integer. Number of top hub nodes to return. Default is 10.
#' @param directed Logical. Whether the network is directed. Default is FALSE.
#'
#' @return A named numeric vector of betweenness scores for the top hub nodes.
#'
#' @importFrom igraph betweenness is_igraph
#'
#' @export
get_top_hubs <- function(g, top_n = 10, directed = FALSE) {
  
  if (!igraph::is_igraph(g)) {
    stop("Input must be an igraph object")
  }
  
  if (top_n <= 0) {
    stop("top_n must be > 0")
  }
  
  bet_scores <- igraph::betweenness(g, directed = directed, normalized = T)
  sorted_bet <- sort(bet_scores, decreasing = TRUE)
  top_nodes <- utils::head(sorted_bet, n = top_n)
  
  return(top_nodes)
}


#' Run PPI PCSf pipeline for single cell DEG categories
#'
#' Builds protein protein interaction subnetworks for each cell type
#' based on DEG categories and FDR derived node prizes.
#'
#' @param result_categories A named list of data.frames containing DEG results per cell type.
#'   Each data.frame must include DEG_Type, Gene_Symbols, Male_FDR, and Female_FDR.
#' @param g An igraph object representing the global protein interaction network.
#' @param target_cell_type_flag Character. Specific cell type to analyze or "all".
#' @return A named list containing PCSf networks per cell type and DEG category.
#'
#' @importFrom igraph vcount
#'
#' @export
ppi_pipeline <- function(result_categories, g, target_cell_type_flag = "all") {
  
  # Internal safe log10 function to avoid Inf from zero FDRs
  safe_log10 <- function(x, min_val = 1e-300) {
    -log10(pmax(x, min_val))
  }
  if (inherits(result_categories, "data.frame")) {
    result_categories <- list(all = result_categories)
  }
  
  network_results <- list()
  
  if (!("all" %in% target_cell_type_flag)) {
    cells=target_cell_type_flag
  } else{
    cells=names(result_categories)
  }
  for (cell_type in cells) {
    
    cell_results <- result_categories[[cell_type]]
    
    male_specific   <- cell_results[cell_results$DEG_Type == "male-specific", ]
    female_specific <- cell_results[cell_results$DEG_Type == "female-specific", ]
    sex_dimorphic   <- cell_results[cell_results$DEG_Type == "sex-dimorphic", ]
    sex_neutral     <- cell_results[cell_results$DEG_Type == "sex-neutral", ]
    
    male_prizes <- safe_log10(male_specific$Male_FDR)
    names(male_prizes) <- male_specific$Gene_Symbols
    
    female_prizes <- safe_log10(female_specific$Female_FDR)
    names(female_prizes) <- female_specific$Gene_Symbols
    
    dimorphic_prizes <- safe_log10(pmin(sex_dimorphic$Male_FDR, sex_dimorphic$Female_FDR))
    names(dimorphic_prizes) <- sex_dimorphic$Gene_Symbols
    
    neutral_prizes   <- safe_log10(pmin(sex_neutral$Male_FDR, sex_neutral$Female_FDR))
    names(neutral_prizes) <- sex_neutral$Gene_Symbols
    
    male_network <- if (length(male_prizes) > 0)
      construct_ppi_pcsf(g, male_prizes) else NULL
    
    female_network <- if (length(female_prizes) > 0)
      construct_ppi_pcsf(g, female_prizes) else NULL
    
    dimorphic_network <- if (length(dimorphic_prizes) > 0)
      construct_ppi_pcsf(g, dimorphic_prizes) else NULL
    
    neutral_network <- if (length(neutral_prizes) > 0)
      construct_ppi_pcsf(g, neutral_prizes) else NULL
    
    network_results[[cell_type]] <- list(
      male_network      = male_network,
      female_network    = female_network,
      dimorphic_network = dimorphic_network,
      neutral_network   = neutral_network
    )
  }
  
  network_results <- lapply(network_results, function(cell) {
    Filter(Negate(is.null), cell)
  })
  
  return(network_results)
}





#' Generate protein-protein interaction network plots for single-cell DEGs
#'
#' @description
#' Accepts  a nested list of igraph networks (cell_type × DEG_type) object and produces annotated network visualizations.
#'
#' @param network_list A single igraph object or a nested list: cell_type → DEG_type → igraph
#' @param result_categories_list A data.frame or named list matching the networks, containing DEG results.
#' @param top_hubs Number of hub genes to annotate (default = 20)
#' @param ncol Number of columns when combining DEG type plots (default = 2)
#' @param show_barplot Logical indicating whether hub barplots are displayed.
#'
#' @return Named list of network plots per cell type (or `"all"` for single network)
#' @import patchwork
#' @export
plot_network_pipeline <- function(network_list, result_categories_list, top_hubs = 20, ncol = 2,  show_barplot = FALSE) {
  
  if (inherits(result_categories_list, "data.frame")) {
    result_categories_list<- list(all = result_categories_list)
  }
  
  if (inherits(network_list, "data.frame")) {
    network_list<- list(all = network_list)
  }
  
  
  all_plots <- list()
  
  for (cell_type in names(network_list)) {
    cell_networks <- network_list[[cell_type]]
    if (!is.list(cell_networks)) next
    
    cell_plots <- lapply(names(cell_networks), function(deg_type) {
      plot_network(
        g = cell_networks[[deg_type]],
        DEG_type = deg_type,
        result_categories = result_categories_list[[cell_type]],
        cell_type = cell_type,
        top_hubs = top_hubs,
        show_barplot = show_barplot  
      )
    })
    
    names(cell_plots) <- names(cell_networks)
    cell_plots <- cell_plots[!sapply(cell_plots, is.null)]
    
    if (length(cell_plots) > 0) {
      # Combine with patchwork
      all_plots[[cell_type]] <- patchwork::wrap_plots(cell_plots, ncol = ncol)
    }
  }
  
  return(all_plots)
}
