---
params:
  ref: "word-styles-reference-var1.docx"
  output_file: CI_report.docx
  report_date: "2026-02-04"
  data_dir: "angata"
  mail_day: "Wednesday"
  borders: FALSE
  ci_plot_type: "both"  # options: "absolute", "cumulative", "both"
  colorblind_friendly: TRUE  # use colorblind-friendly palettes (viridis/plasma)
  facet_by_season: FALSE  # facet CI trend plots by season instead of overlaying
  x_axis_unit: "days"  # x-axis unit for trend plots: "days" or "weeks"
output:
   word_document:
    reference_docx: !expr file.path(here::here("r_app/word-styles-reference-var1.docx"))
    toc: no
editor_options:
  chunk_output_type: console
---

```{r setup_parameters, include=FALSE}
# Set up basic report parameters from input values
report_date <- params$report_date
mail_day <- params$mail_day
borders <- params$borders
ci_plot_type <- params$ci_plot_type
colorblind_friendly <- params$colorblind_friendly
facet_by_season <- params$facet_by_season
x_axis_unit <- params$x_axis_unit
```

```{r load_libraries, message=FALSE, warning=FALSE, include=FALSE}
# Configure knitr options
knitr::opts_chunk$set(warning = FALSE, message = FALSE)

# Set flag for reporting scripts to use pivot.geojson instead of pivot_2.geojson
reporting_script <- TRUE

# Load all packages at once with suppressPackageStartupMessages
suppressPackageStartupMessages({
  # File path handling
  library(here)       # For relative path resolution (platform-independent file paths)
  
  # Spatial data handling
  library(sf)         # For reading/manipulating field boundaries (GeoJSON)
  library(terra)      # For raster operations (reading mosaic TIFFs for visualization)
  
  # Data manipulation
  library(tidyverse)  # For dplyr, ggplot2, tidyr (data wrangling and visualization)
  library(tidyr)      # For data reshaping (pivot_longer, pivot_wider for wide-to-long conversion)
  library(lubridate)  # For date/time operations (week extraction, date formatting)
  library(zoo)        # For zoo objects (time series manipulation, na.locf for gap filling)
  
  # Visualization
  library(tmap)       # For interactive maps (field boundary visualization)
  library(ggspatial)  # For basemap tiles (annotation_map_tile for hexbin overlay)
  library(hexbin)     # For hexbin plots in ggplot2
  
  # Reporting
  library(knitr)      # For R Markdown document generation (code execution and output)
  library(flextable)  # For formatted tables in Word output (professional table styling)
})

# Configure tmap for static plotting (required for legend.outside to work)
tmap_mode("plot")  # CRITICAL: Must be "plot" mode for legends outside to render properly
tmap_options(component.autoscale = FALSE)

# Load custom utility functions
tryCatch({
  source("r_app/90_report_utils.R")
}, error = function(e) {
  message(paste("Error loading 90_report_utils.R:", e$message))
  # Try alternative path if the first one fails
  tryCatch({
    source(here::here("r_app", "90_report_utils.R"))
  }, error = function(e) {
    stop("Could not load 90_report_utils.R from either location: ", e$message)
  })
})

```

```{r initialize_project_config, message=FALSE, warning=FALSE, include=FALSE}
# Set the project directory from parameters
project_dir <- params$data_dir

# Source project parameters with error handling
tryCatch({
  source(here::here("r_app", "parameters_project.R"))
}, error = function(e) {
  stop("Error loading parameters_project.R: ", e$message)
})

# Source common utilities for logging and helper functions
tryCatch({
  source(here::here("r_app", "00_common_utils.R"))
}, error = function(e) {
  stop("Error loading 00_common_utils.R: ", e$message)
})

# Load centralized paths
paths <- setup_project_directories(project_dir)

# Make paths globally available for use in other chunks (especially hexbin)
assign("paths", paths, envir = globalenv())

# Load field boundaries for hexbin map and spatial operations
# Uses: load_field_boundaries, field_boundaries_sf, assign(..., envir = globalenv()), safe_log
tryCatch({
  boundaries_list <- load_field_boundaries(paths$data_dir)
  field_boundaries_sf <- boundaries_list$field_boundaries_sf
  assign("field_boundaries_sf", field_boundaries_sf, envir = globalenv())
  assign("field_boundaries_loaded", TRUE, envir = globalenv())
  safe_log("✓ Field boundaries loaded successfully")
}, error = function(e) {
  assign("field_boundaries_loaded", FALSE, envir = globalenv())
  safe_log(paste("⚠ Could not load field boundaries:", e$message), "WARNING")
  safe_log("Hexbin map will not be available", "WARNING")
})

# Log initial configuration
safe_log("Starting the R Markdown script with KPIs")
safe_log(paste("mail_day params:", params$mail_day))
safe_log(paste("report_date params:", params$report_date))
safe_log(paste("mail_day variable:", mail_day))
```

```{r load_kpi_data, message=FALSE, warning=FALSE, include=FALSE}
## SIMPLE KPI LOADING - robust lookup with fallbacks

# First, show working directory for debugging
cat("\n=== DEBUG: R Markdown Working Directory ===\n")
cat(paste("getwd():", getwd(), "\n"))
cat(paste("Expected knit_dir from R Markdown:", knitr::opts_knit$get("root.dir"), "\n\n"))

# Primary expected directory from centralized paths
kpi_data_dir <- paths$kpi_reports_dir
date_suffix <- format(as.Date(report_date), "%Y%m%d")

# Calculate current week from report_date using ISO 8601 week numbering
current_week <- as.numeric(format(as.Date(report_date), "%V"))
current_year <- as.numeric(format(as.Date(report_date), "%G"))
week_suffix <- paste0("week", sprintf("%02d", current_week), "_", current_year)

# Candidate filenames we expect (exact and common variants)
expected_summary_names <- c(
  paste0(project_dir, "_field_analysis_", week_suffix, ".rds"),
  paste0(project_dir, "_field_analysis_", date_suffix, ".rds"),
  paste0(project_dir, "_field_analysis.rds"),
  "field_analysis.rds",
  paste0("field_analysis_", week_suffix, ".rds"),
  paste0("field_analysis_", date_suffix, ".rds")
)

expected_field_details_names <- c(
  paste0(project_dir, "_field_details_", week_suffix, ".rds"),
  paste0(project_dir, "_field_details_", date_suffix, ".rds"),
  paste0(project_dir, "_field_details.rds"),
  "field_details.rds"
)

# Helper to attempt loading a file from the directory or fallback to a workspace-wide search
try_load_from_dir <- function(dir, candidates) {
  if (!dir.exists(dir)) return(NULL)
  for (name in candidates) {
    f <- file.path(dir, name)
    if (file.exists(f)) return(f)
  }
  return(NULL)
}

# Try primary directory first (field_level/)
summary_file <- try_load_from_dir(kpi_data_dir, expected_summary_names)
field_details_file <- try_load_from_dir(kpi_data_dir, expected_field_details_names)

# If not found, try parent directory (kpis/) where RDS is often saved by Script 80
if (is.null(summary_file) || is.null(field_details_file)) {
  parent_dir <- dirname(kpi_data_dir)  # One level up: reports/kpis/
  safe_log(paste("KPI files not found in", kpi_data_dir, "—trying parent directory:", parent_dir))
  
  if (is.null(summary_file)) {
    summary_file <- try_load_from_dir(parent_dir, expected_summary_names)
  }
  if (is.null(field_details_file)) {
    field_details_file <- try_load_from_dir(parent_dir, expected_field_details_names)
  }
}

# If still not found, perform a workspace-wide search (slower) limited to laravel_app storage
if (is.null(summary_file) || is.null(field_details_file)) {
  safe_log(paste("KPI files not found in", kpi_data_dir, "or parent directory—searching workspace for RDS files"))
  # List rds files under laravel_app/storage/app recursively
  files <- list.files(path = file.path("laravel_app", "storage", "app"), pattern = "\\.rds$", recursive = TRUE, full.names = TRUE)
  # Try to match by expected names
  if (is.null(summary_file)) {
    matched <- files[basename(files) %in% expected_summary_names]
    if (length(matched) > 0) summary_file <- matched[1]
  }
  if (is.null(field_details_file)) {
    matched2 <- files[basename(files) %in% expected_field_details_names]
    if (length(matched2) > 0) field_details_file <- matched2[1]
  }
}

# Final checks and load with safe error messages
kpi_files_exist <- FALSE

# Debug: log what we're looking for
cat("\n=== KPI LOADING DEBUG ===\n")
cat(paste("Working directory:", getwd(), "\n"))
cat(paste("project_dir:", project_dir, "\n"))
cat(paste("report_date:", report_date, "\n"))
cat(paste("Calculated week:", current_week, "year:", current_year, "\n"))
cat(paste("Looking for KPI files in:", kpi_data_dir, "\n"))
cat(paste("Directory exists:", dir.exists(kpi_data_dir), "\n"))
cat(paste("Expected filenames to match:\n"))
for (name in expected_summary_names) cat(paste("  -", name, "\n"))

# List what's actually in the directory
if (dir.exists(kpi_data_dir)) {
  actual_files <- list.files(kpi_data_dir, pattern = ".*\\.rds$", full.names = FALSE)
  cat(paste("Files in KPI directory (", length(actual_files), " total):\n"))
  for (f in actual_files) cat(paste("  -", f, "\n"))
} else {
  cat("KPI directory does NOT exist!\n")
}

if (!is.null(summary_file) && file.exists(summary_file)) {
  cat(paste("✓ FOUND summary file:", summary_file, "\n"))
  cat(paste("  File size:", file.size(summary_file), "bytes\n"))
  summary_data <- tryCatch(readRDS(summary_file), error = function(e) { cat(paste("ERROR reading RDS:", e$message, "\n")); NULL })
  
  # Explicitly assign to global environment for downstream chunks
  assign("summary_data", summary_data, envir = globalenv())
  
  if (!is.null(summary_data)) {
    cat(paste("  ✓ Loaded successfully. Class:", class(summary_data), "\n"))
    if (is.list(summary_data)) {
      cat(paste("  List names:", paste(names(summary_data), collapse = ", "), "\n"))
    }
    
    # Convert new RDS structure (field_analysis, field_analysis_summary) to legacy summary_tables format
    if (is.list(summary_data) && !is.data.frame(summary_data)) {
      # New format from 09_field_analysis_weekly.R - just pass it through
      if ("field_analysis_summary" %in% names(summary_data)) {
        cat("  ✓ Found field_analysis_summary in list - will use this structure\n")
        # Keep the new structure intact - combined_kpi_table will use it directly
        kpi_files_exist <- TRUE
      } else {
        cat("  ! Old format detected\n")
        # Old format - keep as is
        summary_tables <- summary_data
        if (!is.null(summary_tables)) kpi_files_exist <- TRUE
      }
    } else {
      cat("  ! Data frame format\n")
      # Data frame format or direct tables
      summary_tables <- summary_data
      if (!is.null(summary_tables)) kpi_files_exist <- TRUE
    }
  } else {
    cat("  ✗ Failed to load RDS - summary_data is NULL\n")
  }
} else {
  safe_log(paste("KPI summary file not found. Searched:", paste(expected_summary_names, collapse=", ")), "WARNING")
  safe_log(paste("Attempted directory:", kpi_data_dir), "WARNING")
  # Try searching the entire workspace as fallback
  files <- list.files(path = file.path("laravel_app", "storage", "app"), pattern = "kpi.*\\.rds$", recursive = TRUE, full.names = TRUE)
  safe_log(paste("Found", length(files), "KPI RDS files in workspace"), "INFO")
  if (length(files) > 0) {
    safe_log(paste("Available files:", paste(basename(files), collapse = ", ")), "INFO")
  }
}

if (!is.null(field_details_file) && file.exists(field_details_file)) {
  safe_log(paste("Loading field details from:", field_details_file))
  field_details_table <- tryCatch(readRDS(field_details_file), error = function(e) { safe_log(paste("Failed to read field details RDS:", e$message), "ERROR"); NULL })
  if (!is.null(field_details_table)) kpi_files_exist <- kpi_files_exist && TRUE
} else {
  safe_log(paste("Field details file not found. Searched:", paste(expected_field_details_names, collapse=", ")), "WARNING")
  # Try to extract field_details from summary_data if available
  if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
    field_details_table <- summary_data$field_analysis %>%
      rename(`Mean CI` = Mean_CI, `CV Value` = CV, Field = Field_id)
    safe_log("Extracted field details from field_analysis data")
  }
}

if (kpi_files_exist) {
  safe_log("✓ KPI summary tables loaded successfully")
} else {
  safe_log("KPI files could not be located or loaded. KPI sections will be skipped.", "WARNING")
}
```

```{r load_cloud_coverage_data, eval=TRUE, message=FALSE, warning=FALSE, include=FALSE}
## LOAD PER-FIELD CLOUD COVERAGE DATA
# Cloud coverage calculated from the mosaic by script 09
# Expected filename pattern: [project_dir]_cloud_coverage_week[N].rds or _cloud_coverage_[date].rds

expected_cloud_names <- c(
  paste0(project_dir, "_cloud_coverage_week", week_suffix, ".rds"),
  paste0(project_dir, "_cloud_coverage_week", current_week, ".rds"),
  paste0(project_dir, "_cloud_coverage_", date_suffix, ".rds"),
  paste0(project_dir, "_cloud_coverage.rds"),
  paste0(project_dir, "_per_field_cloud_coverage.rds"),
  "cloud_coverage.rds",
  "per_field_cloud_coverage.rds"
)

# Try to load cloud coverage from KPI directory
cloud_file <- try_load_from_dir(kpi_data_dir, expected_cloud_names)

# If not found in KPI dir, search workspace
if (is.null(cloud_file)) {
  files <- list.files(path = file.path("laravel_app", "storage", "app"), pattern = "\\.rds$", recursive = TRUE, full.names = TRUE)
  matched <- files[basename(files) %in% expected_cloud_names]
  if (length(matched) > 0) cloud_file <- matched[1]
}

# Load cloud coverage data if file exists
per_field_cloud_coverage <- NULL
cloud_coverage_available <- FALSE

if (!is.null(cloud_file) && file.exists(cloud_file)) {
  safe_log(paste("Loading cloud coverage data from:", cloud_file))
  per_field_cloud_coverage <- tryCatch(
    readRDS(cloud_file), 
    error = function(e) { 
      safe_log(paste("Failed to read cloud coverage RDS:", e$message), "WARNING"); 
      NULL 
    }
  )
  
  if (!is.null(per_field_cloud_coverage) && nrow(per_field_cloud_coverage) > 0) {
    cloud_coverage_available <- TRUE
    safe_log("✓ Per-field cloud coverage data loaded successfully")
  }
} else {
  safe_log("Per-field cloud coverage file not found. Cloud sections will be skipped.", "WARNING")
}
```

```{r calculate_dates_and_weeks, message=FALSE, warning=FALSE, include=FALSE, eval=TRUE}
# Set locale for consistent date formatting
Sys.setlocale("LC_TIME", "C")

# Initialize date variables from parameters
today <- as.character(report_date)
mail_day_as_character <- as.character(mail_day)

# Calculate report dates and weeks using ISO 8601 week numbering
report_date_obj <- as.Date(today)
current_week <- as.numeric(format(report_date_obj, "%V"))
year <- as.numeric(format(report_date_obj, "%Y"))

# Calculate dates for weekly analysis
week_start <- report_date_obj - ((as.numeric(format(report_date_obj, "%w")) + 1) %% 7)
week_end <- week_start + 6

# Calculate week days (copied from 05 script for compatibility)
report_date_as_week_day <- weekdays(lubridate::ymd(today))
days_of_week <- c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")

# Calculate initial week number
week <- lubridate::week(today)
safe_log(paste("Initial week calculation:", week, "today:", today))

# Calculate previous dates for comparisons
today_minus_1 <- as.character(lubridate::ymd(today) - 7)
today_minus_2 <- as.character(lubridate::ymd(today) - 14)
today_minus_3 <- as.character(lubridate::ymd(today) - 21)

# Adjust week calculation based on mail day
if (which(days_of_week == report_date_as_week_day) > which(days_of_week == mail_day_as_character)) {
  safe_log("Adjusting weeks because of mail day")
  week <- lubridate::week(today) + 1
  today_minus_1 <- as.character(lubridate::ymd(today))
  today_minus_2 <- as.character(lubridate::ymd(today) - 7)
  today_minus_3 <- as.character(lubridate::ymd(today) - 14)
}

# Calculate week numbers for previous weeks
week_minus_1 <- week - 1
week_minus_2 <- week - 2
week_minus_3 <- week - 3

# Format current week with leading zeros
week <- sprintf("%02d", week)

safe_log(paste("Report week:", current_week, "Year:", year))
safe_log(paste("Week range:", week_start, "to", week_end))
```

```{r load_ci_data, message=FALSE, warning=FALSE, include=FALSE, eval=TRUE}
# Load CI index data with error handling
tryCatch({
  CI_quadrant <- readRDS(here::here(paths$cumulative_ci_vals_dir, "All_pivots_Cumulative_CI_quadrant_year_v2.rds")) 
  
  safe_log("Successfully loaded CI quadrant data")
}, error = function(e) {
  stop("Error loading CI quadrant data: ", e$message)
})
```

```{r compute_benchmarks_once, include=FALSE, eval=TRUE}
# Compute CI benchmarks once for the entire estate
benchmarks <- compute_ci_benchmarks(CI_quadrant, project_dir, c(10, 50, 90))
if (!is.null(benchmarks)) {
  safe_log("Benchmarks computed successfully for the report")
} else {
  safe_log("Failed to compute benchmarks", "WARNING")
}
```

```{r compute_total_acreage, include=FALSE, eval=TRUE}
# Precompute total acreage for use in cover page inline expression
# Safely extract and sum acreage from KPI data (may not exist until key_insights runs)
total_acreage <- 0
if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
  field_analysis_df <- summary_data$field_analysis
  total_acreage <- sum(field_analysis_df$Acreage, na.rm = TRUE)
  if (is.na(total_acreage)) total_acreage <- 0
}
```


<!-- Dynamic cover page  -->
::: {custom-style="Cover_title" style="text-align:center; margin-top:120px;"}
<span style="font-size:100pt; line-height:1.0; font-weight:700;">Satellite Based Field Reporting</span>
:::

::: {custom-style="Cover_subtitle" style="text-align:center; margin-top:18px;"}
<span style="font-size:20pt; font-weight:600;">Chlorophyll Index (CI) Monitoring Report — `r toupper(params$data_dir)` Estate (Week `r (if (!is.null(params$week)) params$week else format(as.Date(params$report_date), '%V'))`, `r format(as.Date(params$report_date), '%Y')`)</span>
:::

\newpage

## Report Generated

**Farm Location:** `r toupper(project_dir)` Estate  
**Report Period:** Week `r current_week` of `r year`         
**Report Generated on:** `r format(Sys.time(), "%B %d, %Y at %H:%M")`               
**Farm Size Included in Analysis:** `r formatC(total_acreage, format="f", digits=1)` acres                                                   
**Data Source:** Planet Labs Satellite Imagery  
**Analysis Type:** Chlorophyll Index (CI) Monitoring  

## Key Insights

```{r key_insights, echo=FALSE, results='asis', eval=TRUE}
# Calculate key insights from KPI data
if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
  field_analysis_df <- summary_data$field_analysis
  field_analysis_summary <- summary_data$field_analysis_summary
  
  # Field uniformity insights
  field_cv <- field_analysis_df$CV
  excellent_fields <- sum(field_cv < 0.08, na.rm = TRUE)
  good_fields <- sum(field_cv >= 0.08 & field_cv < 0.15, na.rm = TRUE)
  total_fields <- sum(!is.na(field_cv))
  excellent_pct <- ifelse(total_fields > 0, round(excellent_fields / total_fields * 100, 1), 0)
  good_pct <- ifelse(total_fields > 0, round(good_fields / total_fields * 100, 1), 0)
  
  # Area change insights - extract from field_analysis_summary
  parse_ci_change <- function(change_str) {
    if (is.na(change_str)) return(NA)
    match <- regexpr("^[+-]?[0-9]+\\.?[0-9]*", change_str)
    if (match > 0) {
      return(as.numeric(substr(change_str, match, attr(match, "match.length"))))
    }
    return(NA)
  }
  
  field_analysis_df$ci_change_numeric <- sapply(field_analysis_df$Weekly_ci_change, parse_ci_change)
  total_acreage <- sum(field_analysis_df$Acreage, na.rm = TRUE)
  improving_acreage <- sum(field_analysis_df$Acreage[field_analysis_df$ci_change_numeric > 0.2], na.rm = TRUE)
  declining_acreage <- sum(field_analysis_df$Acreage[field_analysis_df$ci_change_numeric < -0.2], na.rm = TRUE)
  improving_pct <- ifelse(total_acreage > 0, round(improving_acreage / total_acreage * 100, 1), 0)
  declining_pct <- ifelse(total_acreage > 0, round(declining_acreage / total_acreage * 100, 1), 0)
  
  cat("- ", excellent_pct, "% of fields have excellent uniformity (CV < 0.08)\n", sep="")
  cat("- ", good_pct, "% of fields have good uniformity (CV < 0.15)\n", sep="")
  cat("- ", round(improving_acreage, 1), " acres (", improving_pct, "%) of farm area is improving week-over-week\n", sep="")
  cat("- ", round(declining_acreage, 1), " acres (", declining_pct, "%) of farm area is declining week-over-week\n", sep="")
  
} else {
  cat("KPI data not available for key insights.\n")
}
```


## Report Structure

**Section 1:** Cane supply zone analyses, summaries and Key Performance Indicators (KPIs)  
**Section 2:** Explanation of the report, definitions, methodology, and CSV export structure 

\newpage

# Section 1: Farm-wide Analyses and KPIs

## 1.1 Overview of cane supply area, showing zones with number of acres being harvest ready  

```{r overview_map, fig.width=7, fig.height=6, fig.align="center", echo=FALSE, message=FALSE, warning=FALSE}
# Create a hexbin overview map with ggplot
tryCatch({
  # Use per-field field_analysis data from RDS (already loaded in load_kpi_data chunk)
  if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
    analysis_data <- summary_data$field_analysis %>% 
      select(Field_id, Status_Alert) %>%
      rename(Status_trigger = Status_Alert)  # Rename to Status_trigger for compatibility with hexbin logic
  } else {
    analysis_data <- tibble(Field_id = character(), Status_trigger = character())
  }
  
  # Load field boundaries if not already available (should be from initialize_project_config)
  if (!exists("field_boundaries_sf")) {
    if (exists("paths") && !is.null(paths$data_dir)) {
      boundaries_list <- load_field_boundaries(paths$data_dir)
      field_boundaries_sf <- boundaries_list$field_boundaries_sf
    } else {
      stop("Cannot locate field boundaries: paths$data_dir not available")
    }
  }
  
  # Define constants
  ACRE_CONV <- 4046.856
  TARGET_CRS <- 4326  # WGS84 for web basemap compatibility (was 32736 UTM)
  
  # Process polygons into points
  # IMPORTANT: Calculate centroids in projected CRS (UTM 36S for southern Africa) to avoid 
  # st_centroid warnings about longitude/latitude data, then transform back to WGS84
  points_processed <- field_boundaries_sf %>%
    st_make_valid() %>%
    mutate(
      # Calculate area, convert to numeric to strip units, divide by conversion factor
      area_ac = round(as.numeric(st_area(geometry)) / ACRE_CONV, 2)
    ) %>%
    filter(
      # Filter polygons with no surface area
      !is.na(area_ac), area_ac > 0
    ) %>%
    left_join (
      # Add the status_trigger information
      analysis_data %>% select(Field_id, Status_trigger),
      by = c("field" = "Field_id")
    ) %>%
    st_transform(crs = 32736) %>%  # UTM zone 36S (southern Africa)
    st_centroid() %>%
    st_transform(crs = TARGET_CRS) %>%
    bind_cols(st_coordinates(.))
  
  # Validate coordinates - check for NaN, Inf, or missing values
  if (any(!is.finite(points_processed$X)) || any(!is.finite(points_processed$Y))) {
    points_processed <- points_processed %>%
      filter(is.finite(X) & is.finite(Y))
  }
  
  # Final validation: ensure we have valid coordinates before proceeding
  valid_x <- is.finite(points_processed$X) & is.finite(points_processed$Y)
  if (!any(valid_x)) {
    stop("No valid coordinates found in field boundaries. Cannot create hexbin map.")
  }
  
  # Subsetting by harvest status
  points_ready <- points_processed %>% 
    filter(Status_trigger == "harvest_ready")
  points_not_ready <- points_processed %>%
    filter(Status_trigger != "harvest_ready" | is.na(Status_trigger))

  # Generate breaks for color gradients
  breaks_vec <- c(0, 5, 10, 15, 20, 30, 35)
  labels_vec <- as.character(breaks_vec)
  labels_vec[length(labels_vec)] <- ">30"
  labels_vec[1] <- "0.1"
  
  # Calculate data bounds for coordinate limits (prevents basemap scale conflicts)
  # Use actual data bounds without dummy points to avoid column mismatch
  x_limits <- c(
    floor(min(points_processed$X, na.rm = TRUE) * 20) / 20,  # Round down to avoid edge clipping
    ceiling(max(points_processed$X, na.rm = TRUE) * 20) / 20   # Round up for padding
  )
  y_limits <- c(
    floor(min(points_processed$Y, na.rm = TRUE) * 20) / 20,
    ceiling(max(points_processed$Y, na.rm = TRUE) * 20) / 20
  )

  # Create hexbin map with enhanced aesthetics, basemap, and proper legend
  ggplot() +
    # OpenStreetMap basemap (zoom=11 appropriate for agricultural fields)
    ggspatial::annotation_map_tile(type = "osm", zoom = 11, progress = "none", alpha = 0.5) +
    # Hexbin for NOT ready fields (light background)
    geom_hex(
      data = points_not_ready, 
      aes(x = X, y = Y, weight = area_ac, alpha = "Not harvest ready"), 
      binwidth = c(0.012, 0.012),
      fill = "#ffffff",
      colour = "#0000009a",
      linewidth = 0.1
    ) +
    # Hexbin for READY fields (colored gradient)
    geom_hex(
      data = points_ready, 
      aes(x = X, y = Y, weight = area_ac), 
      binwidth = c(0.012, 0.012),
      alpha = 0.9,
      colour = "#0000009a",
      linewidth = 0.1
    ) +
    # Color gradient scale for acreage
    scale_fill_viridis_b(
      option = "viridis",
      direction = -1,
      breaks = breaks_vec,
      labels = labels_vec,
      limits = c(0, 35),
      oob = scales::squish,
      name = "Total Acres"
    ) +
    # Alpha scale for "not ready" status indication
    scale_alpha_manual(
      name = NULL,
      values = 0.8
    ) +
    # Titles and subtitle
    labs(subtitle = "Acres of fields 'harvest ready within a month'") +
    # Spatial coordinate system with explicit bounds (prevents basemap calc_limits conflicts)
    coord_sf(crs = 4326, xlim = x_limits, ylim = y_limits, expand = FALSE) +
    # Theme customization
    theme_minimal() +
    theme(
      legend.position = "right",
      legend.box = "vertical",
      legend.title.align = 0.5,
      plot.subtitle = element_text(size = 11, face = "bold")
    ) +
    # Custom legend guides with enhanced styling
    guides(
      fill = guide_coloursteps(
        barwidth = 1, 
        barheight = 20, 
        title.position = "top",
        order = 1   
      ),
      alpha = guide_legend(
        override.aes = list(
          fill = "#ffffff",     
          colour = "#0000009a", 
          shape = 22           
        ),
        order = 2      
      )
    )

}, error = function(e) {
  warning("Error creating hexbin map:", e$message)
})
```


## 1.2 Key Performance Indicators 

```{r combined_kpi_table, echo=FALSE, results='asis'}
# Create consolidated KPI table from field_analysis data
# Shows: Phases, Triggers, Area Change, Cloud Influence, and Total Farm

if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
  # Load field analysis data
  field_analysis_df <- summary_data$field_analysis
  
  # If field_analysis_summary is NULL or doesn't exist, create it from field_analysis_df
  if (is.null(summary_data$field_analysis_summary) || !("field_analysis_summary" %in% names(summary_data)) || 
      !is.data.frame(summary_data$field_analysis_summary)) {
    
    # Create summary by aggregating by Status_Alert and Phase categories
    phase_summary <- field_analysis_df %>%
      filter(!is.na(Phase)) %>%
      group_by(Phase) %>%
      summarise(
        Acreage = sum(Acreage, na.rm = TRUE),
        Field_count = n_distinct(Field_id),
        .groups = "drop"
      ) %>%
      mutate(Category = Phase) %>%
      select(Category, Acreage, Field_count)
    
    # Create Status trigger summary - includes both active alerts and "No active triggers"
    trigger_summary <- tryCatch({
      # Active alerts (fields with non-NA Status_Alert)
      active_alerts <- field_analysis_df %>%
        filter(!is.na(Status_Alert), Status_Alert != "") %>%
        group_by(Status_Alert) %>%
        summarise(
          Acreage = sum(Acreage, na.rm = TRUE),
          Field_count = n_distinct(Field_id),
          .groups = "drop"
        ) %>%
        mutate(Category = Status_Alert) %>%
        select(Category, Acreage, Field_count)
      
      # No active triggers (fields with NA Status_Alert)
      no_alerts <- field_analysis_df %>%
        filter(is.na(Status_Alert) | Status_Alert == "") %>%
        summarise(
          Acreage = sum(Acreage, na.rm = TRUE),
          Field_count = n_distinct(Field_id),
          .groups = "drop"
        ) %>%
        mutate(Category = "No active triggers") %>%
        select(Category, Acreage, Field_count)
      
      # Combine active alerts and no-alert fields
      bind_rows(active_alerts, no_alerts)
    }, error = function(e) {
      data.frame(Category = character(), Acreage = numeric(), Field_count = numeric())
    })
    
    # Combine into summary
    field_analysis_summary <- bind_rows(phase_summary, trigger_summary)
    
  } else {
    # Use existing summary from RDS
    field_analysis_summary <- summary_data$field_analysis_summary
  }
  
  # Phase names and trigger names to extract from summary
  phase_names <- c("Germination", "Tillering", "Grand Growth", "Maturation", "Unknown Phase")
  # Trigger names now include both active alerts AND "No active triggers" (calculated dynamically above)
  trigger_names <- c("harvest_ready", "harvested_bare", "stress_detected", 
                     "germination_delayed", "growth_on_track", "No active triggers")
  
  # Extract phase distribution - match on category names directly
  if (!is.null(field_analysis_summary) && nrow(field_analysis_summary) > 0) {
    # Phase rows with field count
    phase_rows <- field_analysis_summary %>%
      filter(Category %in% phase_names) %>%
      select(Category, Acreage, Field_count) %>%
      mutate(KPI_Group = "PHASE DISTRIBUTION", .before = 1)
  
    # Trigger rows with field count
    trigger_rows <- field_analysis_summary %>%
      filter(Category %in% trigger_names) %>%
      select(Category, Acreage, Field_count) %>%
      mutate(KPI_Group = "OPERATIONAL ALERTS", .before = 1)
    
    # If no triggers found, add a placeholder row
    if (nrow(trigger_rows) == 0) {
      trigger_rows <- data.frame(
        KPI_Group = "OPERATIONAL ALERTS",
        Category = "No active triggers",
        Acreage = 0,
        Field_count = 0,
        stringsAsFactors = FALSE
      )
    }
  
    # Calculate area change from field_analysis data
    total_acreage <- sum(field_analysis_df$Acreage, na.rm = TRUE)
    total_fields <- n_distinct(field_analysis_df$Field_id)
    
    # Parse Weekly_ci_change to determine improvement/decline
    parse_ci_change <- function(change_str) {
      if (is.na(change_str)) return(NA)
      match <- regexpr("^[+-]?[0-9]+\\.?[0-9]*", change_str)
      if (match > 0) {
        return(as.numeric(substr(change_str, match, attr(match, "match.length"))))
      }
      return(NA)
    }
    
    field_analysis_df$ci_change_numeric <- sapply(field_analysis_df$Weekly_ci_change, parse_ci_change)
    
    # Area change rows with field count
    improving_df <- field_analysis_df %>%
      filter(ci_change_numeric > 0.2)
    stable_df <- field_analysis_df %>%
      filter(ci_change_numeric >= -0.2 & ci_change_numeric <= 0.2)
    declining_df <- field_analysis_df %>%
      filter(ci_change_numeric < -0.2)
    
    improving_acreage <- sum(improving_df$Acreage, na.rm = TRUE)
    improving_field_count <- n_distinct(improving_df$Field_id)
    stable_acreage <- sum(stable_df$Acreage, na.rm = TRUE)
    stable_field_count <- n_distinct(stable_df$Field_id)
    declining_acreage <- sum(declining_df$Acreage, na.rm = TRUE)
    declining_field_count <- n_distinct(declining_df$Field_id)
    
    improving_pct <- ifelse(total_acreage > 0, round(improving_acreage / total_acreage * 100, 1), 0)
    declining_pct <- ifelse(total_acreage > 0, round(declining_acreage / total_acreage * 100, 1), 0)
    stable_pct <- ifelse(total_acreage > 0, round(stable_acreage / total_acreage * 100, 1), 0)
    
    # Calculate percentages for phases and triggers
    phase_pcts <- phase_rows %>%
      mutate(Percent = paste0(round(Acreage / total_acreage * 100, 1), "%"))
    
    trigger_pcts <- trigger_rows %>%
      mutate(Percent = paste0(round(Acreage / total_acreage * 100, 1), "%"))
    
    area_change_rows <- data.frame(
      KPI_Group = "AREA CHANGE",
      Category = c("Improving", "Stable", "Declining"),
      Acreage = c(round(improving_acreage, 2), round(stable_acreage, 2), round(declining_acreage, 2)),
      Field_count = c(improving_field_count, stable_field_count, declining_field_count),
      Percent = c(paste0(improving_pct, "%"), paste0(stable_pct, "%"), paste0(declining_pct, "%")),
      stringsAsFactors = FALSE
    )
    
    # Cloud influence rows with field count - aggregate by Cloud_category
    cloud_rows <- tryCatch({
      field_analysis_df %>%
        filter(!is.na(Cloud_category)) %>%
        group_by(Cloud_category) %>%
        summarise(
          Acreage = sum(Acreage, na.rm = TRUE),
          Field_count = n_distinct(Field_id),
          .groups = "drop"
        ) %>%
        mutate(
          KPI_Group = "CLOUD INFLUENCE",
          Category = Cloud_category,
          Percent = paste0(round(Acreage / total_acreage * 100, 1), "%"),
          Acreage = round(Acreage, 2)
        ) %>%
        select(KPI_Group, Category, Acreage, Field_count, Percent)
    }, error = function(e) {
      data.frame(
        KPI_Group = character(),
        Category = character(),
        Acreage = numeric(),
        Field_count = numeric(),
        Percent = character()
      )
    })
    
    # Total farm row
    total_row <- data.frame(
      KPI_Group = "TOTAL FARM",
      Category = "Total Acreage",
      Acreage = round(total_acreage, 2),
      Field_count = total_fields,
      Percent = "100%",
      stringsAsFactors = FALSE
    )
    
    # Combine all rows
    combined_df <- bind_rows(
      phase_pcts,
      trigger_pcts,
      area_change_rows,
      cloud_rows,
      total_row
    )
    
    # Create grouped display where KPI_Group name appears only once per group
    combined_df <- combined_df %>%
      group_by(KPI_Group) %>%
      mutate(
        KPI_display = if_else(row_number() == 1, KPI_Group, "")
      ) %>%
      ungroup() %>%
      select(KPI_display, Category, Acreage, Percent, Field_count)
    
    # Render as flextable with merged cells
    ft <- flextable(combined_df) %>%
      set_header_labels(
        KPI_display = "KPI Category",
        Category = "Item",
        Acreage = "Acreage",
        Percent = "Percentage of total fields",
        Field_count = "# Fields"
      ) %>%
      merge_v(j = "KPI_display") %>%
      autofit()
    
    # Add horizontal lines after each KPI group (at cumulative row positions)
    # Calculate row positions: row 1 is header, then data rows follow
    phase_count <- nrow(phase_rows)
    trigger_count <- nrow(trigger_rows)
    area_count <- nrow(area_change_rows)
    cloud_count <- nrow(cloud_rows)
    
    # Add lines after phases, triggers, area change, and cloud groups (before totals)
    if (phase_count > 0) {
      ft <- ft %>% hline(i = phase_count, border = officer::fp_border(width = 1))
    }
    if (trigger_count > 0) {
      ft <- ft %>% hline(i = phase_count + trigger_count, border = officer::fp_border(width = 1))
    }
    if (area_count > 0) {
      ft <- ft %>% hline(i = phase_count + trigger_count + area_count, border = officer::fp_border(width = 1))
    }
    if (cloud_count > 0) {
      ft <- ft %>% hline(i = phase_count + trigger_count + area_count + cloud_count, border = officer::fp_border(width = 1))
    }
    
    ft
  } else {
    cat("KPI summary data available but is empty/invalid.\n")
  }
} else {
  cat("KPI summary data not available.\n")
}
```

```{r data, message=TRUE, warning=TRUE, include=FALSE}
# All data comes from the field analysis performed in 09_field_analysis_weekly.R
# The report renders KPI tables and field summaries from that data
```

```{r load_field_boundaries, message=FALSE, warning=FALSE, include=FALSE}
# Load field boundaries from parameters (with fallback if geometry is invalid)
field_boundaries_loaded <- FALSE

tryCatch({
  # Try to load and validate the field boundaries
  if (exists("field_boundaries_sf") && !is.null(field_boundaries_sf)) {
    # Try to filter - this will trigger geometry validation
    AllPivots0 <- field_boundaries_sf %>%
      dplyr::filter(!is.na(field), !is.na(sub_field))
    
    # If successful, also create merged field list
    AllPivots_merged <- AllPivots0 %>% 
      dplyr::filter(!is.na(field), !is.na(sub_field)) %>%
      dplyr::group_by(field) %>% 
      dplyr::summarise(.groups = 'drop')
    
    field_boundaries_loaded <- TRUE
    safe_log("✓ Successfully loaded field boundaries")
  } else {
    safe_log("⚠ field_boundaries_sf not found in environment")
  }
}, error = function(e) {
  # If geometry is invalid, try to fix or skip
  safe_log(paste("⚠ Error loading field boundaries:", e$message), "WARNING")
  safe_log("Attempting to fix invalid geometries using st_make_valid()...", "WARNING")
  
  tryCatch({
    # Try to repair invalid geometries
    field_boundaries_sf_fixed <- sf::st_make_valid(field_boundaries_sf)
    assign("field_boundaries_sf_fixed", field_boundaries_sf_fixed, envir = .GlobalEnv)
    
    AllPivots0 <- field_boundaries_sf_fixed %>%
      dplyr::filter(!is.na(field), !is.na(sub_field))
    assign("AllPivots0", AllPivots0, envir = .GlobalEnv)
    
    AllPivots_merged <- AllPivots0 %>% 
      dplyr::filter(!is.na(field), !is.na(sub_field)) %>%
      dplyr::group_by(field) %>% 
      dplyr::summarise(.groups = 'drop')
    assign("AllPivots_merged", AllPivots_merged, envir = .GlobalEnv)
    
    assign("field_boundaries_loaded", TRUE, envir = .GlobalEnv)
    safe_log("✓ Fixed invalid geometries and loaded field boundaries")
  }, error = function(e2) {
    safe_log(paste("⚠ Could not repair geometries:", e2$message), "WARNING")
    safe_log("Continuing without field boundary data", "WARNING")
  })
})
```
\newpage

# Section 2: Support Document for weekly SmartCane data package. 

## 1. About This Document

This document is the support document to the SmartCane data file. It includes the definitions, explanatory calculations and suggestions for interpretations of the data as provided. For additional questions please feel free to contact SmartCane support, through your contact person, or via info@smartcane.ag.  


## 2. About the Data File

The data file is automatically populated based on normalized and indexed remote sensing images of provided polygons. Specific SmartCane algorithms provide tailored calculation results developed to support the sugarcane operations by:   

• Supporting harvest planning mill-field logistics to ensure optimal tonnage and sucrose levels                                         
• Monitoring of the crop growth rates on the farm, providing evidence of performance                                                 
• Identifying growth-related issues that are in need of attention                                               
• Enabling timely actions to minimize negative impact                                                            

Key Features of the data file: - High-resolution satellite imagery analysis - Week-over-week change detection - Individual field performance metrics - Actionable insights for crop management. 



#### *What is the Chlorophyll Index (CI)?*

The Chlorophyll Index (CI) is a vegetation index that measures the relative amount of chlorophyll in plant leaves. Chlorophyll is the green pigment responsible for photosynthesis in plants. Higher CI values indicate:  
• Greater photosynthetic activity  
• Healthier plant tissue  
• Better nitrogen uptake  
• More vigorous crop growth  

CI values typically range from 0 (bare soil or severely stressed vegetation) to 7+ (very healthy, dense vegetation). For sugarcane, values between 3-7 generally indicate good crop health, depending on the growth stage.



<div align="center">
![Chlorophyll Index Example](CI_graph_example.png)
</div>


### Data File Structure and Columns
The data file is organized in rows, one row per agricultural field (polygon), and columns, providing field data, actual measurements, calculation results and descriptions. The data file can be directly integration with existing farm management systems for further analysis. Each column is described hereunder: 

| **Nr.** | **Column**     | **Description**                                                       | **Example**     |
|-----|---------------|----------------------------------------------------------------------------|-------------|
|-----|---------------|----------------------------------------------------------------------------|-------------|
| **1** | **Field_id** | Unique identifier for a cane field combining field name and sub-field number. This can be the same as Field_Name but is also helpful in keeping track of cane fields should they change, split or merge. | "00110" |
| **2** | **Farm_Section** | Sub-area or section name | "Section a" |
| **3** | **Field_name** | Client Name or label assigned to a cane field. | "Tinga1" |
| **4** | **Acreage** | Field size in acres | "4.5" |
| **5** | **Status_trigger** | Shows changes in crop status worth alerting. More detailed explanation of the possible alerts is written down  under key concepts. | "Harvest_ready" |
| **6** | **Last_harvest_or_planting_date** | Date of most recent harvest as per satellite detection algorithm / or manual entry | “2025-03-14” | 
| **7** |**Age_week** | Time elapsed since planting/harvest in weeks; used to predict expected growth phases. Reflects planting/harvest date (left). | "40" |
| **8** | **Phase (age based)** | Current growth phase (e.g., germination, tillering, stem elongation, grain fill, mature) inferred from crop age | "Maturation" |
| **9** | **Germination_progress** | Estimated percentage or stage of germination/emergence based on CI patterns and age. This goes for young fields (age < 4 months). Remain at 100% when finished. | "maturation_progressing" |
| **10** | **Mean_CI** | Average Chlorophyll Index value across the field; higher values indicate healthier, greener vegetation. Calculated on a 7-day merged weekly image  | "3.95" | 
| **11** | **Weekly CI Change** | Week-over-week change in Mean_CI; positive values indicate greening/growth, negative values indicate yellowing/decline | "0.79" |
| **12** | **Four_week_trend** | Long term change in mean CI; smoothed trend (strong growth, growth, no growth, decline, strong decline) | "0.87" | 
| **13** | **CI_range** | Min-max Chlorophyll Index values within the field; wide ranges indicate spatial heterogeneity/patches. Derived from week mosaic | "3.6-5.6" |
| **14** | **CI_Percentiles** | The CI-range without border effects | "3.5-4.4" | 
| **15** | **CV** | Coefficient of variation of CI; measures field uniformity (lower = more uniform, >0.25 = poor uniformity). Derived from week mosaic. In percentages | "10.01%" |
| **16** | **CV_Trend_Short_Term** | Trend of CV over two weeks. Indicating short-term heterogeneity  | "0.15" |
| **17** | **CV_Trend_Long_Term** | Slope of 8-week trend line. | "0.32" |
| **18** | **Imminent_prob** | Probability (0-1) that the field is ready for harvest based on LSTM harvest model predictions | "0.8" | 
| **19** | **Cloud_pct_clear** | Percentage of field visible in the satellite image (unobstructed by clouds); lower values indicate poor data quality | "70%" | 
| **20** | **Cloud_category** | Classification of cloud cover level (e.g., clear, partial, heavy); indicates confidence in CI measurements | "Partial Coverage" |



\newpage
# 3. Key Concepts 

#### *Growth Phases (Age-Based)*

Each field is assigned to one of four growth phases based on age in weeks since planting:

| **Phase** | **Age Range** | **Characteristics** |
|-------|-----------|-----------------|
| Germination | 0-6 weeks | Crop emergence and early establishment; high variability expected |
| Tillering | 4-16 weeks | Shoot multiplication and plant establishment; rapid growth phase |
| Grand Growth | 17-39 weeks | Peak vegetative growth; maximum height and biomass accumulation |
| Maturation | 39+ weeks | Ripening phase; sugar accumulation and preparation for harvest |



#### *Status Alert*

Status alerts indicate the current field condition based on CI and age-related patterns. Each field receives **one alert** reflecting its most relevant status:

| **Alert** | **Condition** | **Phase** | **Messaging** |
|---------|-----------|-------|-----------|
| Ready for harvest-check | Harvest model > 0.50 and crop is mature | Active from 52 weeks onwards | Ready for harvest-check |
| harvested/bare | Field of 50 weeks or older either shows mean CI values lower than 1.5 (for a maximum of three weeks) OR drops from higher CI to lower than 1.5. Alert drops if CI rises and passes 1.5 again | Maturation (39+) | Harvested or bare field |
| stress_detected_whole_field | Mean CI on field drops by 2+ points but field mean CI remains higher than 1.5 | Any | Strong decline in crop health |


#### *Harvest Date and Harvest Imminent*
The SmartCane algorithm calculates the last harvest date and the probability of harvest approaching in the next 4 weeks. Two different algorithms are used.                                 
The **last harvest date** is a timeseries analyses of the CI levels of the past years, based on clean factory managed fields as data set for the machine learning, a reliability of over 90% has been reached. Smallholder managed fields of small size (0.3 acres) have specific side effects and field management characteristics, that influence the model results.                                                 
**Imminent_probability** of harvest is a prediction algorithm, estimating the likelihood of a crop ready to be harvested in the near future. This prediction takes the CI-levels into consideration, building on the vegetative development of sugarcane in the last stage of Maturation, where all sucrose is pulled into the stalk, depleting the leaves from energy and productive function, reducing the levels of CI in the leave tissue.                                  
Both algorithms are not always in sync, and can have contradictory results. Wider field characteristics analyses is suggested if such contradictory calculation results occur. 




\newpage
## Report Metadata

```{r report_metadata, echo=FALSE}
# Calculate total area from field analysis data
total_area_acres <- 0
if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
  total_area_acres <- sum(summary_data$field_analysis$Acreage, na.rm = TRUE)
}

# Calculate total fields
total_fields_count <- 0
if (exists("AllPivots0")) {
  total_fields_count <- nrow(AllPivots0 %>% filter(!is.na(field)) %>% group_by(field) %>% summarise())
} else if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
  total_fields_count <- n_distinct(summary_data$field_analysis$Field_id)
}

metadata_info <- data.frame(
  Metric = c(
    "Report Generated", 
    "Data Source", 
    "Analysis Period", 
    "Total Fields [number]",
    "Total Area [acres]",
    "Next Update",
    "Service provided",
    "Starting date service"
  ),
  Value = c(
    format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
    paste("Project", toupper(project_dir)),
    paste("Week", current_week, "of", year),
    ifelse(total_fields_count > 0, total_fields_count, "Unknown"),
    ifelse(total_area_acres > 0, round(total_area_acres, 0), "Unknown"),
    "Next Wednesday",
    "Cane Supply Office - Weekly",
    "23 dec 2025"
  ),
  stringsAsFactors = FALSE
)

ft <- flextable(metadata_info) %>%
  set_caption("Report Metadata") %>%
  autofit()

ft
```

*This report was automatically generated by the SmartCane monitoring system. For questions or additional analysis, please contact the technical team at info@smartcane.ag.*