---
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)
  language: "en"  # Language code for translations (e.g., "en", "es-mx", "pt-br", "sw")
  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
language <- params$language
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)
  library(jsonlite)   # For reading in the translation JSON
  library(glue)       # For variable interpolation in translations
})

# 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
}
```

```{r load_translations, include = FALSE}
# Load the translations document from JSON
tryCatch({
  translations_json <- fromJSON("translations/translations_91.json", simplifyVector = FALSE)
  
  # Flatten all sheets into a single named vector: key -> translated string
  sheet_names <- c("main_translations", "status_translations", "figure_translations")
  
  # Determine language column (fall back to English)
  # Supported languages are derived dynamically from the JSON keys
  supported_langs <- unique(unlist(lapply(translations_json, function(sheet) {
    lapply(sheet, names)
  })))
  lang_col <- if (language %in% supported_langs) language else "en"
  if (lang_col != language) {
    safe_log(paste("Specified language not supported, defaulting to 'en'"), "WARNING")
  } else {
    safe_log(paste0("Loaded localisation for '", language, "'"))
  }
  
  # Build the tr named vector (same interface as report 90)
  tr <- unlist(lapply(sheet_names, function(s) {
    sheet <- translations_json[[s]]
    vapply(names(sheet), function(key) {
      val <- sheet[[key]][[lang_col]]
      if (is.null(val) || val == "") sheet[[key]][["en"]] else val
    }, character(1))
  }))
  
  safe_log(paste("Translations loaded:", length(tr), "keys"))
}, error = function(e) {
  safe_log(paste("Error loading translation file:", e$message), "ERROR")
  tr <<- NULL
})
```


<!-- 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;">`r tr_key("cover_title")`</span>
:::

::: {custom-style="Cover_subtitle" style="text-align:center; margin-top:18px;"}
<span style="font-size:20pt; font-weight:600;">`r tr_key("cover_subtitle")`</span>
:::

\newpage

`r tr_key("report_summary_heading")`

`r tr_key("report_farm_location")` `r toupper(project_dir)` Estate  
`r tr_key("report_period_label")`         
`r tr_key("report_generated_on")` `r format(Sys.time(), "%B %d, %Y at %H:%M")`               
`r tr_key("report_farm_size")`                                                   
`r tr_key("report_data_source")`  
`r tr_key("report_analysis_type")`  

`r tr_key("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(tr_key("insight_excellent_unif"), "\n")
  cat(tr_key("insight_good_unif"), "\n")
  cat(tr_key("insight_improving"), "\n")
  cat(tr_key("insight_declining"), "\n")
  
} else {
  cat(tr_key("kpi_na_insights"), "\n")
}
```


`r tr_key("report_structure_heading")`

`r tr_key("report_structure_body")`

\newpage

`r tr_key("section_i")`

`r tr_key("section_1_1")`  

```{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)) {
    fa <- summary_data$field_analysis
    status_col <- intersect(c("Status_Alert", "Status_trigger"), names(fa))[1]
    analysis_data <- fa %>%
      select(Field_id, all_of(status_col)) %>%
      rename(Status_trigger = all_of(status_col))
  } 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 and unit preference
  TARGET_CRS <- 4326  # WGS84 for web basemap compatibility
  unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE)
  
  # Process polygons into points
  # IMPORTANT: Use area from field_analysis_df (calculated in script 80) rather than recalculating
  # This ensures consistent unit preference across all outputs
  points_processed <- field_boundaries_sf %>%
    st_make_valid() %>%
    # Note: Area will come from the left_join with analysis_data below
    filter(
      # Only include fields that exist in the analysis data
      field %in% analysis_data$Field_id
    ) %>%
    left_join(
      # Get area and status from analysis data (area calculated in script 80)
      analysis_data %>% 
        left_join(
          summary_data$field_analysis %>% select(Field_id, Acreage),
          by = "Field_id"
        ) %>%
        select(Field_id, Status_trigger, Acreage),
      by = c("field" = "Field_id")
    ) %>%
    mutate(
      # Rename Acreage to area_value for consistency with rest of code
      # (If we ever change unit preference, this will have already been calculated in the correct unit by script 80)
      area_value = round(Acreage, 2)
    ) %>%
    filter(
      # Filter polygons with no area
      !is.na(area_value), area_value > 0
    ) %>%
    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(5, 10, 15, 20, 30, 35)
  labels_vec <- as.character(breaks_vec)
  labels_vec[length(labels_vec)] <- ">30"
  
  # 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
  )

  # ggplot2's StatBinhex computes the hex grid origin as c(min(x), min(y)) of each layer's
  # own data. Feeding different subsets (points_ready / points_not_ready) to the two layers
  # gives different origins → misaligned hexes. Fix: pass points_processed to BOTH layers.
  # For the coloured layer, use ready_area = area_value for harvest-ready fields, 0 elsewhere.
  # The scale censors 0 → NA → transparent, so non-ready hexes are invisible in that layer.
  points_processed <- points_processed %>%
    mutate(ready_area = ifelse(Status_trigger == "harvest_ready", area_value, 0))

  # 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 background: ALL fields as white hexes (anchors the hex grid to the full extent)
    geom_hex(
      data = points_processed,
      aes(x = X, y = Y, weight = area_value, alpha = tr_key("hexbin_not_ready")),
      binwidth = c(0.012, 0.012),
      fill = "#ffffff",
      colour = "#0000009a",
      linewidth = 0.1
    ) +
    # Hexbin overlay: same grid (same data), coloured only where ready_area > 0
    geom_hex(
      data = points_processed,
      aes(x = X, y = Y, weight = ready_area),
      binwidth = c(0.012, 0.012),
      alpha = 0.9,
      colour = "#0000009a",
      linewidth = 0.1
    ) +
    # Color gradient scale — values of 0 (non-ready hexes) are censored → NA → transparent
    scale_fill_viridis_b(
      option = "viridis",
      direction = -1,
      breaks = breaks_vec,
      labels = labels_vec,
      limits = c(0.001, 35),
      oob = scales::oob_censor,
      na.value = "transparent",
      name = tr_key("hexbin_legend_acres")
    ) +
    # Alpha scale for "not ready" status indication
    scale_alpha_manual(
      name = NULL,
      values = 0.8
    ) +
    # Titles and subtitle
    labs(subtitle = tr_key("hexbin_subtitle")) +
    # 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)
})
```


`r tr_key("section_1_2")` 

```{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)) {
    
    # Detect which status column is present (Status_Alert from cane_supply, Status_trigger from others)
    status_col <- intersect(c("Status_Alert", "Status_trigger"), names(field_analysis_df))[1]

    # Create summary by aggregating by status 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)
      active_alerts <- field_analysis_df %>%
        filter(!is.na(.data[[status_col]]), .data[[status_col]] != "") %>%
        group_by(across(all_of(status_col))) %>%
        summarise(
          Acreage = sum(Acreage, na.rm = TRUE),
          Field_count = n_distinct(Field_id),
          .groups = "drop"
        ) %>%
        mutate(Category = .data[[status_col]]) %>%
        select(Category, Acreage, Field_count)

      # No active triggers (fields with NA status)
      no_alerts <- field_analysis_df %>%
        filter(is.na(.data[[status_col]]) | .data[[status_col]] == "") %>%
        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 = tr_key("kpi_col_category"),
        Category = tr_key("kpi_col_item"),
        Acreage = tr_key("kpi_col_acreage"),
        Percent = tr_key("kpi_col_percent"),
        Field_count = tr_key("kpi_col_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(tr_key("kpi_empty"), "\n")
  }
} else {
  cat(tr_key("kpi_unavailable"), "\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

`r tr_key("section_2_heading")` 

`r tr_key("about_doc_heading")`

`r tr_key("about_doc_body")`  


`r tr_key("about_data_heading")`

`r tr_key("about_data_body")`   

• `r tr_key("about_data_bullet_harvest")`                                         
• `r tr_key("about_data_bullet_monitoring")`                                                 
• `r tr_key("about_data_bullet_identifying")`                                               
• `r tr_key("about_data_bullet_enabling")`                                                            

`r tr_key("about_data_key_features")` 



`r tr_key("ci_section_heading")`

`r tr_key("ci_intro_body")`  
• `r tr_key("ci_bullet_photosynthetic")`  
• `r tr_key("ci_bullet_healthy")`  
• `r tr_key("ci_bullet_nitrogen")`  
• `r tr_key("ci_bullet_vigorous")`  

`r tr_key("ci_range_body")`



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


`r tr_key("data_structure_heading")`
`r tr_key("data_structure_intro")` 

| **Nr.** | **Column**     | **Description**                                                       | **Example**     |
|-----|---------------|----------------------------------------------------------------------------|-------------|
|-----|---------------|----------------------------------------------------------------------------|-------------|
| **1** | **Field_id** | `r tr_key("col_field_id_desc")` | "00110" |
| **2** | **Farm_Section** | `r tr_key("col_farm_section_desc")` | "Section a" |
| **3** | **Field_name** | `r tr_key("col_field_name_desc")` | "Tinga1" |
| **4** | **Acreage** | `r tr_key("col_acreage_desc")` | "4.5" |
| **5** | **Status_trigger** | `r tr_key("col_status_trigger_desc")` | "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** | `r tr_key("col_age_week_desc")` | "40" |
| **8** | **Phase (age based)** | `r tr_key("col_phase_desc")` | "Maturation" |
| **9** | **Germination_progress** | `r tr_key("col_germination_progress_desc")` | "maturation_progressing" |
| **10** | **Mean_CI** | `r tr_key("col_mean_ci_desc")` | "3.95" | 
| **11** | **Weekly CI Change** | `r tr_key("col_weekly_ci_change_desc")` | "0.79" |
| **12** | **Four_week_trend** | `r tr_key("col_four_week_trend_desc")` | "0.87" | 
| **13** | **CI_range** | `r tr_key("col_ci_range_desc")` | "3.6-5.6" |
| **14** | **CI_Percentiles** | `r tr_key("col_ci_percentiles_desc")` | "3.5-4.4" | 
| **15** | **CV** | `r tr_key("col_cv_desc")` | "10.01%" |
| **16** | **CV_Trend_Short_Term** | `r tr_key("col_cv_trend_short_desc")` | "0.15" |
| **17** | **CV_Trend_Long_Term** | `r tr_key("col_cv_trend_long_desc")` | "0.32" |
| **18** | **Imminent_prob** | `r tr_key("col_imminent_prob_desc")` | "0.8" | 
| **19** | **Cloud_pct_clear** | `r tr_key("col_cloud_pct_desc")` | "70%" | 
| **20** | **Cloud_category** | `r tr_key("col_cloud_category_desc")` | "Partial Coverage" |



\newpage
`r tr_key("key_concepts_heading")` 

`r tr_key("growth_phases_heading")`

`r tr_key("growth_phases_intro")`

| **Phase** | **Age Range** | **Characteristics** |
|-------|-----------|-----------------|
| `r tr_key("Germination")` | `r tr_key("germination_age_range")` | `r tr_key("germination_characteristics")` |
| `r tr_key("Tillering")` | `r tr_key("tillering_age_range")` | `r tr_key("tillering_characteristics")` |
| `r tr_key("Grand Growth")` | `r tr_key("grand_growth_age_range")` | `r tr_key("grand_growth_characteristics")` |
| `r tr_key("Maturation")` | `r tr_key("maturation_age_range")` | `r tr_key("maturation_characteristics")` |



`r tr_key("status_alert_heading")`

`r tr_key("status_alert_intro")`

| **Alert** | **Condition** | **Phase** | **Messaging** |
|---------|-----------|-------|-----------|
| `r tr_key("harvest_ready")` | `r tr_key("harvest_ready_condition")` | `r tr_key("harvest_ready_phase_info")` | `r tr_key("harvest_ready_msg")` |
| `r tr_key("harvested_bare")` | `r tr_key("harvested_bare_condition")` | `r tr_key("harvested_bare_phase_info")` | `r tr_key("harvested_bare_msg")` |
| `r tr_key("stress_detected")` | `r tr_key("stress_condition")` | `r tr_key("stress_phase_info")` | `r tr_key("stress_msg")` |


`r tr_key("harvest_date_heading")`
`r tr_key("harvest_date_body_1")`                                 
`r tr_key("harvest_date_body_2")`                                                 
`r tr_key("harvest_date_body_3")`                                  
`r tr_key("harvest_date_body_4")` 




\newpage
`r tr_key("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(
    tr_key("report_gen"), 
    tr_key("data_source"), 
    tr_key("analysis_period"), 
    tr_key("tot_fields_number"),
    tr_key("tot_area_acres"),
    tr_key("next_update"),
    tr_key("service_provided"),
    tr_key("service_start")
  ),
  Value = c(
    format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
    paste(tr_key("project"), toupper(project_dir)),
    paste(tr_key("week"), current_week, tr_key("of"), year),
    ifelse(total_fields_count > 0, total_fields_count, tr_key("unknown")),
    ifelse(total_area_acres > 0, round(total_area_acres, 0), tr_key("unknown")),
    tr_key("next_wed"),
    tr_key("cane_supply_service"),
    "23 dec 2025"
  ),
  stringsAsFactors = FALSE
)

ft <- flextable(metadata_info) %>%
  set_header_labels(
    Metric = tr_key("metric"),
    Value = tr_key("value")
  ) %>%
  set_caption(tr_key("metadata_caption")) %>%
  autofit()

ft
```

`r tr_key("disclaimer")`