#' Compute local diffusion exponents for all nodes using exact analytical method
#'
#' For each node i, the mean squared resistance displacement is computed exactly
#' via the transition matrix powers: MSD_i(t), for t = 1, 2, ..., max_time.
#' Then a log-log linear regression is performed
#' over the specified time window to estimate the local diffusion exponent H_i.
#'
#' @param adj Symmetric adjacency matrix (0/1, no self-loops) of a connected graph.
#' @param max_time Maximum number of steps (starting from t=1).
#' @param fit_range Numeric vector of length 2, e.g. c(t_min, t_max), specifying
#'   the time window for fitting (1 <= t_min < t_max <= max_time).
#' @return A list with components:
#'   \item{H}{Numeric vector of length n, the estimated local diffusion exponents.}
#'   \item{msd}{Matrix of size max_time x n, where rows correspond to times 1:max_time
#'              and columns correspond to nodes. Entry [t, i] = MSD_i(t).}
#'   \item{fit}{List of lm objects (one per node), or NULL if fit failed.}
#' @export
#' @importFrom stats lm coef
#' @references
#' Nie, Chun-Xiao. "Vertex‑level diffusion scaling from resistance distance."  .Chaos, Under Review.
#' @examples
#' # Example: path graph with 4 nodes
#' adj <- matrix(c(0,1,0,0,
#'                 1,0,1,0,
#'                 0,1,0,1,
#'                 0,0,1,0), nrow=4, byrow=TRUE)
#' res <- exactlocaldiffexp(adj, max_time = 50, fit_range = c(10, 40))
#' print(res$H)
exactlocaldiffexp <- function(adj, max_time, fit_range) {
  # Input validation
  if (!is.matrix(adj)) stop("adj must be a matrix.")
  if (nrow(adj) != ncol(adj)) stop("adj must be square.")
  if (!isSymmetric(adj)) stop("adj must be symmetric.")
  if (any(diag(adj) != 0)) stop("adj must have zero diagonal.")
  n <- nrow(adj)
  deg <- rowSums(adj)
  if (any(deg == 0)) stop("Graph contains isolated nodes (zero degree).")
  if (length(fit_range) != 2) stop("fit_range must be a vector of length 2.")
  if (fit_range[1] < 1 || fit_range[2] > max_time || fit_range[1] >= fit_range[2]) {
    stop("fit_range must satisfy 1 <= t_min < t_max <= max_time.")
  }

  # Compute resistance distance matrix
  if (!exists("resistance_distance_matrix", mode = "function")) {
    stop("Function resistance_distance_matrix() not found. Please load it.")
  }
  Omega <- resistance_distance_matrix(adj)
  M <- Omega^2   # squared resistance distances

  # Build row-stochastic transition matrix P
  P <- adj / deg

  # Compute exact MSD for all nodes for t = 1..max_time
  msd <- matrix(0, nrow = max_time, ncol = n)
  colnames(msd) <- 1:n
  rownames(msd) <- 1:max_time
  Pt <- diag(n)   # P^0
  for (t in 1:max_time) {
    Pt <- Pt %*% P
    # rowSums(Pt * M) gives MSD_i(t) for each start node i
    msd[t, ] <- rowSums(Pt * M)
  }

  # Time indices for fitting (t from fit_range[1] to fit_range[2])
  t_vals <- fit_range[1]:fit_range[2]
  idx <- t_vals  # these are the time steps (1-based)
  if (length(idx) < 2) {
    warning("Insufficient points in fit_range for fitting.")
    return(list(H = rep(NA, n), msd = msd, fit = vector("list", n)))
  }

  # Fit for each node
  H <- numeric(n)
  fit_objects <- vector("list", n)
  for (i in 1:n) {
    y <- log(msd[idx, i])
    x <- log(t_vals)
    valid <- is.finite(y) & is.finite(x)
    if (sum(valid) < 2) {
      H[i] <- NA
      fit_objects[[i]] <- NULL
      next
    }
    fit <- lm(y[valid] ~ x[valid])
    H[i] <- coef(fit)[2] / 2
    fit_objects[[i]] <- fit
  }

  return(list(H = H, msd = msd, fit = fit_objects))
}
