---
params:
  ref: "word-styles-reference-var1.docx"
  output_file: "CI_report.docx"
  report_date: "2026-02-04" #!r Sys.Date()
  data_dir: "aura"
  mail_day: "Wednesday"
  borders: FALSE
  ci_plot_type: "both"
  colorblind_friendly: TRUE
  facet_by_season: FALSE
  x_axis_unit: "days"
output:
   word_document:
    reference_docx: !expr file.path("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 and spatial annotations (OSM basemap with 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(officer)    # For Word document manipulation (custom formatting, headers, footers)
})

# 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("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"))
  source(here::here("r_app", "00_common_utils.R"))
}, error = function(e) {
  stop("Error loading project utilities: ", e$message)
})

# Load centralized paths
paths <- setup_project_directories(project_dir)

# Assign global variables for use in visualization functions
weekly_CI_mosaic <- paths$weekly_mosaic_dir  # Per-field mosaic directory

# 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))
safe_log(paste("weekly_CI_mosaic path:", weekly_CI_mosaic))
```

```{r load_kpi_data, message=FALSE, warning=FALSE, include=FALSE}
## LOAD KPI DATA - DYNAMIC PROJECT-SPECIFIC loading
# NO workspace-wide fallback that might load wrong project

# Build expected KPI file path strictly from project_dir
kpi_data_dir <- paths$kpi_reports_dir # file.path(paths$reports_dir, "kpis")  # Should be: laravel_app/storage/app/{project}/reports/kpis

# Calculate week from report_date
current_week <- as.numeric(format(as.Date(report_date), "%V"))
current_year <- as.numeric(format(as.Date(report_date), "%G"))

# The ACTUAL filename format from 80_calculate_kpis.R output (after fix)
# Format: {project_dir}_field_analysis_week{WW}_{YYYY}.rds
kpi_rds_filename <- paste0(project_dir, "_field_analysis_week", 
                           sprintf("%02d_%d", current_week, current_year), ".rds")
kpi_rds_path <- file.path(kpi_data_dir, kpi_rds_filename)

safe_log(paste("Looking for KPI file:", kpi_rds_path))
safe_log(paste("Project directory:", project_dir))
safe_log(paste("Expected filename:", kpi_rds_filename))

# Load with strict error checking - NO fallback to find other project's files
kpi_files_exist <- FALSE
summary_tables <- NULL
field_details_table <- NULL

if (dir.exists(kpi_data_dir)) {
  if (file.exists(kpi_rds_path)) {
    safe_log(paste("✓ Found KPI file for", project_dir))
    loaded_data <- tryCatch(
      readRDS(kpi_rds_path),
      error = function(e) {
        safe_log(paste("ERROR reading KPI RDS:", e$message), "ERROR")
        return(NULL)
      }
    )
    
    # Handle RDS structure from 80_utils_agronomic_support.R
    # Expected: list(field_analysis = dataframe, kpis = list, summary_tables = list, ...)
    # OR just a dataframe (for backward compatibility)
    
    if (!is.null(loaded_data)) {
      # Try to extract field_analysis from different possible structures
      if (is.data.frame(loaded_data)) {
        # Direct dataframe (simplest case)
        field_details_table <- loaded_data
        safe_log("✓ Loaded field_analysis dataframe directly")
      } else if (is.list(loaded_data)) {
        # List structure - try different key names
        if ("field_analysis_df" %in% names(loaded_data)) {
          field_details_table <- loaded_data$field_analysis_df
          safe_log("✓ Loaded field_analysis_df from list")
        } else if ("field_analysis" %in% names(loaded_data)) {
          field_details_table <- loaded_data$field_analysis
          safe_log("✓ Loaded field_analysis from list")
        } else if ("kpis" %in% names(loaded_data)) {
          # Might be the full output from orchestrator - create combined table
          safe_log("✓ Found kpis list in loaded data")
          # For now, skip - we need the combined field table
        }
        
        # Also check if summary_tables already exists in the RDS
        if ("summary_tables" %in% names(loaded_data)) {
          summary_tables <- loaded_data$summary_tables
          safe_log("✓ Loaded pre-computed summary_tables from RDS")
        }
      }
      
      # If we successfully loaded field_details_table, transform it into summary_tables
      if (!is.null(field_details_table) && nrow(field_details_table) > 0) {
        safe_log(paste("✓ Loaded field_details_table with", nrow(field_details_table), "fields"))
        safe_log(paste("  Columns:", paste(names(field_details_table), collapse=", ")))
        
        # NORMALIZATION: Normalize column structure (Field→Field_id rename + expected_cols injection)
        field_details_table <- normalize_field_details_columns(field_details_table)
        
        # Normalize other common column name variations
        column_mappings <- list(
          c("CV Value", "CV"),
          c("Mean CI", "Mean_CI"),
          c("Yield Forecast (t/ha)", "TCH_Forecasted"),
          c("Gap Score", "Gap_Score"),
          c("Growth Uniformity", "Growth_Uniformity"),
          c("Decline Risk", "Decline_Risk"),
          c("Patchiness Risk", "Patchiness_Risk"),
          c("Moran's I", "Morans_I")
        )
        
        for (mapping in column_mappings) {
          old_name <- mapping[1]
          new_name <- mapping[2]
          if (old_name != new_name && old_name %in% names(field_details_table) && !new_name %in% names(field_details_table)) {
            field_details_table <- field_details_table %>%
              dplyr::rename(!!new_name := old_name)
            safe_log(paste("  ✓ Normalized:", old_name, "→", new_name))
          }
        }

        
        # Only create summary_tables if not already loaded from RDS
        if (is.null(summary_tables)) {
          summary_tables <- list()
          
          # 1. Uniformity summary - GROUP BY Uniformity_Category and COUNT
          if ("Uniformity_Category" %in% names(field_details_table)) {
            summary_tables$uniformity <- field_details_table %>%
              group_by(interpretation = Uniformity_Category) %>%
              summarise(field_count = n(), .groups = 'drop')
            safe_log("  ✓ Created uniformity summary")
          }
          
          # 2. Area change summary - GROUP BY Area_Change_Interpretation and COUNT
          if ("Area_Change_Interpretation" %in% names(field_details_table)) {
            summary_tables$area_change <- field_details_table %>%
              group_by(interpretation = Area_Change_Interpretation) %>%
              summarise(field_count = n(), .groups = 'drop')
            safe_log("  ✓ Created area_change summary")
          }
          
          # 3. Growth decline summary - GROUP BY Trend_Interpretation and COUNT
          if ("Trend_Interpretation" %in% names(field_details_table)) {
            summary_tables$growth_decline <- field_details_table %>%
              group_by(trend_interpretation = Trend_Interpretation) %>%
              summarise(field_count = n(), .groups = 'drop')
            safe_log("  ✓ Created growth_decline summary")
          }
          
          # 4. Patchiness summary - GROUP BY Patchiness_Risk + Gini ranges
          if ("Patchiness_Risk" %in% names(field_details_table) && "Gini_Coefficient" %in% names(field_details_table)) {
            summary_tables$patchiness <- field_details_table %>%
              mutate(
                gini_category = case_when(
                  Gini_Coefficient < 0.2 ~ "Uniform (Gini<0.2)",
                  Gini_Coefficient < 0.4 ~ "Moderate (Gini 0.2-0.4)",
                  TRUE ~ "High (Gini≥0.4)"
                )
              ) %>%
              group_by(gini_category, patchiness_risk = Patchiness_Risk) %>%
              summarise(field_count = n(), .groups = 'drop')
            safe_log("  ✓ Created patchiness summary")
          }
          
          # 5. TCH forecast summary - show actual value ranges (quartiles) instead of counts
          if ("TCH_Forecasted" %in% names(field_details_table)) {
            tch_values <- field_details_table %>%
              filter(!is.na(TCH_Forecasted)) %>%
              pull(TCH_Forecasted)
            
            if (length(tch_values) > 0) {
              # Defensive check: if all TCH values are identical, handle as special case
              if (length(unique(tch_values)) == 1) {
                # All values are identical
                identical_value <- tch_values[1]
                summary_tables$tch_forecast <- tibble::tibble(
                  tch_category = "All equal",
                  range = paste0(round(identical_value, 1), " t/ha"),
                  field_count = length(tch_values)
                )
                safe_log("  ✓ Created tch_forecast summary (all TCH values identical)")
              } else {
                # Multiple distinct values - use three-quartile approach
                q25 <- quantile(tch_values, 0.25, na.rm = TRUE)
                q50 <- quantile(tch_values, 0.50, na.rm = TRUE)
                q75 <- quantile(tch_values, 0.75, na.rm = TRUE)
                min_val <- min(tch_values, na.rm = TRUE)
                max_val <- max(tch_values, na.rm = TRUE)
                
                summary_tables$tch_forecast <- tibble::tibble(
                  tch_category = c("Top 25%", "Middle 50%", "Bottom 25%"),
                  range = c(
                    paste0(round(q75, 1), "-", round(max_val, 1), " t/ha"),
                    paste0(round(q25, 1), "-", round(q75, 1), " t/ha"),
                    paste0(round(min_val, 1), "-", round(q25, 1), " t/ha")
                  ),
                  field_count = c(
                    nrow(field_details_table %>% filter(TCH_Forecasted >= q75, !is.na(TCH_Forecasted))),
                    nrow(field_details_table %>% filter(TCH_Forecasted >= q25 & TCH_Forecasted < q75, !is.na(TCH_Forecasted))),
                    nrow(field_details_table %>% filter(TCH_Forecasted < q25, !is.na(TCH_Forecasted)))
                  )
                )
                safe_log("  ✓ Created tch_forecast summary with value ranges")
              }
            }
          }
          
          # 6. Gaps summary - GROUP BY Gap_Level and COUNT
          if ("Gap_Level" %in% names(field_details_table)) {
            summary_tables$gap_filling <- field_details_table %>%
              group_by(gap_level = Gap_Level) %>%
              summarise(field_count = n(), .groups = 'drop')
            safe_log("  ✓ Created gap_filling summary")
          }
          
          safe_log(paste("✓ Created", length(summary_tables), "summary tables from field_details"))
        }
        
        kpi_files_exist <- TRUE
        
      } else {
        safe_log("ERROR: Could not extract field_analysis dataframe from RDS", "ERROR")
      }
    }
    
  } else {
    safe_log(paste("KPI file not found:", kpi_rds_path), "WARNING")
    safe_log(paste("Files in directory:", paste(list.files(kpi_data_dir, pattern="\\.rds$"), collapse=", ")), "WARNING")
  }
} else {
  safe_log(paste("KPI directory does not exist:", kpi_data_dir), "WARNING")
}

if (!kpi_files_exist) {
  safe_log(paste("Skipping KPI sections - no data for", project_dir, "on", report_date), "WARNING")
  summary_tables <- NULL
  field_details_table <- NULL
}

# DEBUG: Log what was actually loaded
if (exists("summary_tables") && !is.null(summary_tables)) {
  safe_log(paste("✓ summary_tables available with", length(summary_tables), "KPIs"))
  for (kpi_name in names(summary_tables)) {
    kpi_df <- summary_tables[[kpi_name]]
    if (!is.null(kpi_df) && is.data.frame(kpi_df)) {
      safe_log(paste("  -", kpi_name, ":", nrow(kpi_df), "rows"))
    }
  }
} else {
  safe_log("WARNING: summary_tables is NULL or does not exist", "WARNING")
}

# summary_tables  # Uncomment for debugging
```

```{r calculate_dates_and_weeks, message=FALSE, warning=FALSE, include=FALSE}
# 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 (consistent with scripts 40 & 80)
report_date_obj <- as.Date(today)
current_week <- lubridate::isoweek(report_date_obj)
current_iso_year <- lubridate::isoyear(report_date_obj)
year <- lubridate::isoyear(report_date_obj)  # Use ISO year, not calendar year

# Calculate dates for weekly analysis (Monday-based, consistent with ISO 8601)
week_start <- lubridate::floor_date(report_date_obj, unit = "week", week_start = 1)
week_end <- week_start + 6

# Calculate previous week dates using date arithmetic (handles year boundaries automatically)
today_minus_1 <- as.character(report_date_obj - 7)
today_minus_2 <- as.character(report_date_obj - 14)
today_minus_3 <- as.character(report_date_obj - 21)

# Calculate week numbers for previous weeks using date arithmetic
prev_week_1_date <- report_date_obj - 7
prev_week_2_date <- report_date_obj - 14
prev_week_3_date <- report_date_obj - 21

week_minus_1 <- sprintf("%02d", lubridate::isoweek(prev_week_1_date))
week_minus_1_year <- lubridate::isoyear(prev_week_1_date)

week_minus_2 <- sprintf("%02d", lubridate::isoweek(prev_week_2_date))
week_minus_2_year <- lubridate::isoyear(prev_week_2_date)

week_minus_3 <- sprintf("%02d", lubridate::isoweek(prev_week_3_date))
week_minus_3_year <- lubridate::isoyear(prev_week_3_date)

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

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

```{r load_ci_data, message=FALSE, warning=FALSE, include=FALSE}
# Load CI quadrant data for field-level analysis
tryCatch({
  # Try multiple path constructions to handle different directory structures (dynamically)
  candidate_paths <- c(
    file.path(paths$cumulative_ci_vals_dir, "All_pivots_Cumulative_CI_quadrant_year_v2.rds"),
    here::here(paths$cumulative_ci_vals_dir, "All_pivots_Cumulative_CI_quadrant_year_v2.rds"),
    file.path("laravel_app/storage/app", project_dir, "Data/extracted_ci/cumulative_vals/All_pivots_Cumulative_CI_quadrant_year_v2.rds")
  )
  
  # Find first valid path
  ci_quadrant_path <- NULL
  for (path in candidate_paths) {
    if (file.exists(path)) {
      ci_quadrant_path <- path
      break
    }
  }
  
  if (is.null(ci_quadrant_path)) {
    safe_log(paste("CI quadrant file not found. Tried:", paste(candidate_paths, collapse=", ")), "WARNING")
    CI_quadrant <- NULL
  } else {
    CI_quadrant <- readRDS(ci_quadrant_path)
    safe_log(paste("Successfully loaded CI quadrant data from:", ci_quadrant_path))
  }
}, error = function(e) {
  safe_log(paste("Error loading CI quadrant data:", e$message), "WARNING")
  CI_quadrant <<- NULL
})

# NOTE: Overview maps skipped for this report
# Individual field sections load their own per-field mosaics directly

# Load harvesting data (required for ci_plot() function)
tryCatch({
  harvesting_data <- load_harvesting_data(paths$data_dir)
  if (!is.null(harvesting_data)) {
    safe_log(paste("Successfully loaded harvesting data:", nrow(harvesting_data), "records"))
  } else {
    safe_log("No harvesting data available - ci_plot() will work with limited functionality", "WARNING")
    harvesting_data <- NULL
  }
}, error = function(e) {
  safe_log(paste("Error loading harvesting data:", e$message), "ERROR")
  harvesting_data <<- NULL
})
```


```{r compute_benchmarks_once, include=FALSE}
# Compute CI benchmarks once for the entire estate
if (!is.null(CI_quadrant) && nrow(CI_quadrant) > 0) {
  tryCatch({
    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")
      benchmarks <- NULL
    }
  }, error = function(e) {
    safe_log(paste("Error computing benchmarks:", e$message), "WARNING")
    benchmarks <<- NULL
  })
} else {
  safe_log("Skipping benchmark computation - CI quadrant data not available", "WARNING")
  benchmarks <- 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;">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)` Farm (Week `r { rd <- params$report_date; rd <- if (inherits(rd, "Date")) rd else suppressWarnings(as.Date(rd)); if (is.na(rd)) rd <- Sys.Date(); if (!is.null(params$week)) params$week else format(rd, '%V') }`, `r { rd <- params$report_date; rd <- if (inherits(rd, "Date")) rd else suppressWarnings(as.Date(rd)); if (is.na(rd)) rd <- Sys.Date(); format(rd, '%Y') }`)</span>
:::

\newpage

## Report Summary

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

## Report Structure

**Section 1:** Farm-wide analyses, summaries and Key Performance Indicators (KPIs)  
**Section 2:** Field-by-field detailed analyses with maps and trend graphs  
**Section 3:** Explanation of the report, definitions, and methodology

## Key Insights

```{r key_insights, echo=FALSE, results='asis'}
# Calculate key insights from KPI data
if (exists("summary_tables") && !is.null(summary_tables) && length(summary_tables) > 0) {
  
  # Aggregate per-field KPI data into summaries on-the-fly
  
  # 1. Uniformity insights - group by interpretation
  if (!is.null(summary_tables$uniformity) && nrow(summary_tables$uniformity) > 0) {
    cat("**Field Uniformity:**\n")
    uniformity_counts <- summary_tables$uniformity %>%
      dplyr::select(interpretation, count = field_count)
    
    for (i in seq_len(nrow(uniformity_counts))) {
      status <- uniformity_counts$interpretation[i]
      count <- uniformity_counts$count[i]
      if (!is.na(status) && !is.na(count) && count > 0) {
        cat("- ", count, " field(s) with ", status, "\n", sep="")
      }
    }
  }
  
  # 2. Area change insights - group by interpretation
  if (!is.null(summary_tables$area_change) && nrow(summary_tables$area_change) > 0) {
    cat("\n**Area Change Status:**\n")
    area_counts <- summary_tables$area_change %>%
      dplyr::select(interpretation, count = field_count)
    
    for (i in seq_len(nrow(area_counts))) {
      status <- area_counts$interpretation[i]
      count <- area_counts$count[i]
      if (!is.na(status) && !is.na(count) && count > 0) {
        cat("- ", count, " field(s) ", status, "\n", sep="")
      }
    }
  }
  
  # 3. Growth trend insights - group by trend_interpretation
  if (!is.null(summary_tables$growth_decline) && nrow(summary_tables$growth_decline) > 0) {
    cat("\n**Growth Trends (4-Week):**\n")
    growth_counts <- summary_tables$growth_decline %>%
      dplyr::select(trend_interpretation, count = field_count)
    
    for (i in seq_len(nrow(growth_counts))) {
      trend <- growth_counts$trend_interpretation[i]
      count <- growth_counts$count[i]
      if (!is.na(trend) && !is.na(count) && count > 0) {
        cat("- ", count, " field(s) with ", trend, "\n", sep="")
      }
    }
  }
  
  # 4. Patchiness insights - group by patchiness_risk
  if (!is.null(summary_tables$patchiness) && nrow(summary_tables$patchiness) > 0) {
    cat("\n**Field Patchiness Risk:**\n")
    patchiness_counts <- summary_tables$patchiness %>%
      dplyr::select(patchiness_risk, count = field_count)
    
    for (i in seq_len(nrow(patchiness_counts))) {
      risk <- patchiness_counts$patchiness_risk[i]
      count <- patchiness_counts$count[i]
      if (!is.na(risk) && !is.na(count) && count > 0) {
        cat("- ", count, " field(s) at ", risk, " risk\n", sep="")
      }
    }
  }
  
  # 5. Total fields analyzed
  total_fields <- sum(summary_tables$uniformity$field_count, na.rm = TRUE)
  cat("\n**Total Fields Analyzed:** ", total_fields, "\n", sep="")
 
} else {
  cat("KPI data not available for ", project_dir, " on this date.\n", sep="")
}
```

\newpage

# Section 1: Farm-wide Analyses and KPIs

## Executive Summary - Key Performance Indicators

```{r combined_kpi_table, echo=FALSE, results='asis'}
# Display KPI tables - standardized format with Level, Count, Percent columns
if (exists("summary_tables") && !is.null(summary_tables) && length(summary_tables) > 0) {
  
  tryCatch({
    # KPI metadata for display
    kpi_display_order <- list(
      uniformity = list(display = "Field Uniformity", level_col = "interpretation", count_col = "field_count"),
      area_change = list(display = "Area Change", level_col = "interpretation", count_col = "field_count"),
      growth_decline = list(display = "Growth Decline (4-Week Trend)", level_col = "trend_interpretation", count_col = "field_count"),
      patchiness = list(display = "Field Patchiness", level_col = "gini_category", count_col = "field_count", detail_col = "patchiness_risk"),
      tch_forecast = list(display = "TCH Forecasted", level_col = "tch_category", detail_col = "range", count_col = "field_count"),
      gap_filling = list(display = "Gaps", level_col = "gap_level", count_col = "field_count")
    )

    standardize_kpi <- function(df, level_col, count_col, detail_col = NULL) {
      if (is.null(level_col) || !(level_col %in% names(df)) || is.null(count_col) || !(count_col %in% names(df))) {
        return(NULL)
      }
      total <- sum(df[[count_col]], na.rm = TRUE)
      total <- ifelse(total == 0, NA_real_, total)

      # If detail_col is specified, use it as the Level instead
      if (!is.null(detail_col) && detail_col %in% names(df)) {
        display_level <- df[[detail_col]]
      } else {
        display_level <- df[[level_col]]
      }

      df %>%
        dplyr::transmute(
          Level = as.character(display_level),
          Count = as.integer(round(as.numeric(.data[[count_col]]))),
          Percent = if (is.na(total)) {
            NA_real_
          } else {
            round(Count / total * 100, 1)
          }
        )
    }

    combined_df_list <- list()
    order_keys <- names(kpi_display_order)

    for (kpi_key in order_keys) {
      config <- kpi_display_order[[kpi_key]]
      if (!kpi_key %in% names(summary_tables)) next
      kpi_df <- summary_tables[[kpi_key]]
      if (is.null(kpi_df) || !is.data.frame(kpi_df) || nrow(kpi_df) == 0) next

      # Pass detail_col if it exists in config
      detail_col <- if (!is.null(config$detail_col)) config$detail_col else NULL
      kpi_rows <- standardize_kpi(kpi_df, config$level_col, config$count_col, detail_col)

      if (!is.null(kpi_rows)) {
        kpi_rows$KPI <- config$display
        combined_df_list[[length(combined_df_list) + 1]] <- kpi_rows
      }
    }

    combined_df <- dplyr::bind_rows(combined_df_list)

    if (nrow(combined_df) > 0) {
      combined_df <- combined_df %>%
        dplyr::mutate(KPI_group = KPI) %>%
        dplyr::group_by(KPI) %>%
        dplyr::mutate(
          KPI_display = if_else(dplyr::row_number() == 1, KPI, "")
        ) %>%
        dplyr::ungroup()

      kpi_group_sizes <- rle(combined_df$KPI_group)$lengths

      display_df <- combined_df %>%
        dplyr::select(KPI = KPI_display, Level, Count, Percent)

      ft <- flextable(display_df) %>%
        merge_v(j = "KPI") %>%
        autofit()

      cum_rows <- cumsum(kpi_group_sizes)
      for (i in seq_along(cum_rows)) {
        if (i < length(cum_rows)) {
          ft <- ft %>%
            hline(i = cum_rows[i], border = officer::fp_border(width = 2))
        }
      }

      ft
    } else {
      cat("No valid KPI summary tables found for display.\n")
    }
    
  }, error = function(e) {
    safe_log(paste("Error displaying KPI tables:", e$message), "WARNING")
    cat("KPI summary tables could not be displayed. Individual KPI sections will be shown below.\n")
  })
  
} else {
  cat("Note: KPI summary tables have not been loaded. Detailed KPI analysis will be available once data is computed.\n")
}
```

\newpage
## Field Alerts

```{r field_alerts_table, echo=FALSE, results='asis'}
# Generate alerts for all fields
generate_field_alerts <- function(field_details_table) {
  if (is.null(field_details_table) || nrow(field_details_table) == 0) {
    return(NULL)  # Return NULL to signal no data
  }
  
  # Check for required columns
  required_cols <- c("Field", "Field Size (acres)", "Growth Uniformity", "Yield Forecast (t/ha)", 
                     "Gap Score", "Decline Risk", "Patchiness Risk", "Mean CI", "CV Value", "Moran's I")
  missing_cols <- setdiff(required_cols, colnames(field_details_table))
  
  if (length(missing_cols) > 0) {
    message("Field details missing required columns: ", paste(missing_cols, collapse = ", "))
    return(NULL)  # Return NULL if required columns are missing
  }
  
  alerts_list <- list()
  
  # Get unique fields
  unique_fields <- unique(field_details_table$Field)
  
  for (field_name in unique_fields) {
    field_data <- field_details_table %>% filter(Field == field_name)
    
    # Aggregate data for the field
    field_summary <- field_data %>%
      summarise(
        field_size = sum(`Field Size (acres)`, na.rm = TRUE),
        uniformity_levels = paste(unique(`Growth Uniformity`), collapse = "/"),
        avg_yield_forecast = mean(`Yield Forecast (t/ha)`, na.rm = TRUE),
        max_gap_score = max(`Gap Score`, na.rm = TRUE),
        highest_decline_risk = case_when(
          any(`Decline Risk` == "Very-high") ~ "Very-high",
          any(`Decline Risk` == "High") ~ "High", 
          any(`Decline Risk` == "Moderate") ~ "Moderate",
          any(`Decline Risk` == "Low") ~ "Low",
          TRUE ~ "Unknown"
        ),
        highest_patchiness_risk = case_when(
          any(`Patchiness Risk` == "High") ~ "High",
          any(`Patchiness Risk` == "Medium") ~ "Medium",
          any(`Patchiness Risk` == "Low") ~ "Low",
          any(`Patchiness Risk` == "Minimal") ~ "Minimal",
          TRUE ~ "Unknown"
        ),
        avg_mean_ci = mean(`Mean CI`, na.rm = TRUE),
        avg_cv = mean(`CV Value`, na.rm = TRUE),
        .groups = 'drop'
      )
    
    # Generate alerts for this field based on simplified CV-Moran's I priority system (3 levels)
    field_alerts <- c()
    
    # Get CV and Moran's I values
    avg_cv <- field_summary$avg_cv
    morans_i <- mean(field_data[["Moran's I"]], na.rm = TRUE)
    
    # Determine priority level (1=Urgent, 2=Monitor, 3=No stress)
    priority_level <- get_field_priority_level(avg_cv, morans_i)
    
    # Generate alerts based on priority level
    if (priority_level == 1) {
      field_alerts <- c(field_alerts, "Priority field - recommend inspection")
    } else if (priority_level == 2) {
      field_alerts <- c(field_alerts, "Monitor - check when convenient")
    }
    # Priority 3: No alert (no stress)
    
    # Keep other alerts for decline risk, patchiness risk, gap score
    if (field_summary$highest_decline_risk %in% c("High", "Very-high")) {
      field_alerts <- c(field_alerts, "Growth decline observed")
    }
    if (field_summary$highest_patchiness_risk == "High") {
      field_alerts <- c(field_alerts, "High patchiness detected - recommend scouting")
    }
    if (field_summary$max_gap_score > 20) {
      field_alerts <- c(field_alerts, "Gaps present - recommend review")
    }
    
    # Only add alerts if there are any (skip fields with no alerts)
    if (length(field_alerts) > 0) {
      # Add to alerts list
      for (alert in field_alerts) {
        alerts_list[[length(alerts_list) + 1]] <- data.frame(
          Field = field_name,
          Alert = alert
        )
      }
    }
  }
  
  # Combine all alerts
  if (length(alerts_list) > 0) {
    alerts_df <- do.call(rbind, alerts_list)
    return(alerts_df)
  } else {
    return(data.frame(Field = character(), Alert = character()))
  }
}

# Generate and display alerts table
if (exists("field_details_table") && !is.null(field_details_table) && nrow(field_details_table) > 0) {
  # Adapter: Map normalized column names back to legacy names for generate_field_alerts()
  # (generates from the normalized schema created by normalize_field_details_columns + column_mappings)
  field_details_for_alerts <- field_details_table
  
  # Rename normalized columns back to legacy names (only if they exist)
  if ("Field_id" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(Field = Field_id)
  }
  if ("Mean_CI" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Mean CI` = Mean_CI)
  }
  if ("CV" %in% names(field_details_for_alerts) && !("CV Value" %in% names(field_details_for_alerts))) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`CV Value` = CV)
  }
  if ("TCH_Forecasted" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Yield Forecast (t/ha)` = TCH_Forecasted)
  }
  if ("Gap_Score" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Gap Score` = Gap_Score)
  }
  if ("Growth_Uniformity" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Growth Uniformity` = Growth_Uniformity)
  }
  if ("Decline_Risk" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Decline Risk` = Decline_Risk)
  }
  if ("Decline_Severity" %in% names(field_details_for_alerts) && !("Decline Risk" %in% names(field_details_for_alerts))) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Decline Risk` = Decline_Severity)
  }
  if ("Patchiness_Risk" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Patchiness Risk` = Patchiness_Risk)
  }
  if ("Morans_I" %in% names(field_details_for_alerts)) {
    field_details_for_alerts <- field_details_for_alerts %>% dplyr::rename(`Moran's I` = Morans_I)
  }
  
  alerts_data <- generate_field_alerts(field_details_for_alerts)
  if (!is.null(alerts_data) && nrow(alerts_data) > 0) {
    ft <- flextable(alerts_data) %>%
#      set_caption("Field Alerts Summary") %>%
      autofit()
    ft
  } else {
    cat("No alerts data available.\n")
  }
} else {
  cat("Note: Field details data not available for alerts generation. Run 80_calculate_kpis.R to generate KPI data.\n")
}
```

```{r data, message=TRUE, warning=TRUE, include=FALSE}
# Verify CI quadrant data is loaded from load_ci_data chunk
if (!exists("CI_quadrant")) {
  safe_log("CI_quadrant not found - this may affect field analysis reports", "WARNING")
  CI_quadrant <- NULL
} else if (is.null(CI_quadrant)) {
  safe_log("CI_quadrant data is NULL - field-level CI analysis will be skipped", "WARNING")
} else {
  safe_log("CI quadrant data verified for field-level analysis")
}
```


```{r load_field_boundaries, message=TRUE, warning=TRUE, include=FALSE}
# Load field boundaries from GeoJSON
tryCatch({
  boundaries_result <- load_field_boundaries(paths$data_dir)
  
  if (is.list(boundaries_result) && "field_boundaries_sf" %in% names(boundaries_result)) {
    field_boundaries_sf <- boundaries_result$field_boundaries_sf
  } else {
    field_boundaries_sf <- boundaries_result
  }
  
  if (nrow(field_boundaries_sf) == 0) {
    stop("No field boundaries loaded")
  }
  
  AllPivots0 <- field_boundaries_sf %>%
    dplyr::filter(!is.na(field), !is.na(sub_field))  # Filter out NA field names
  safe_log(paste("Successfully loaded", nrow(AllPivots0), "fields"))
  
  # Prepare merged field list for use in summaries
  AllPivots_merged <- AllPivots0 %>% 
    dplyr::group_by(field) %>% 
    dplyr::summarise(.groups = 'drop')
  
}, error = function(e) {
  stop("Error loading field boundaries: ", e$message)
})
```

```{r create_field_details_table, message=FALSE, warning=FALSE, include=FALSE}
# Create field_details_table from available data (now that AllPivots0 is defined)
# For projects without KPI data, create minimal table from field names/geometries

if (!exists("field_details_table") || is.null(field_details_table)) {
  tryCatch({
    if (!is.null(AllPivots0) && nrow(AllPivots0) > 0) {
      # Get field names from geometries
      field_names <- AllPivots0$field
      
      # Try to calculate field sizes (area) from geometry if available
      field_sizes <- if (!is.null(sf::st_geometry(AllPivots0)) && !all(sf::st_is_empty(sf::st_geometry(AllPivots0)))) {
        sf::st_area(sf::st_geometry(AllPivots0)) / 4046.86  # Convert m² to acres (1 acre = 4046.86 m²)
      } else {
        rep(NA_real_, length(field_names))
      }
      
      # Create minimal field details table with actual data we have + NAs for missing KPI columns
      # IMPORTANT: Use column names that match downstream code expectations (no spaces, match exact names)
      field_details_table <- tibble::tibble(
        Field_id = field_names,
        Acreage = as.numeric(field_sizes),
        Growth_Uniformity = NA_character_,
        TCH_Forecasted = NA_real_,
        Gap_Score = NA_real_,
        Decline_Risk = NA_character_,
        Patchiness_Risk = NA_character_,
        Mean_CI = NA_real_,
        CV = NA_real_,
        Morans_I = NA_real_
      )
      safe_log(paste("Created field_details_table from geometries for", nrow(field_details_table), "fields"))
    }
  }, error = function(e) {
    safe_log(paste("Error creating field_details_table from geometries:", e$message), "WARNING")
  })
}
```

```{r aggregate_farm_level_rasters, message=FALSE, warning=FALSE, include=FALSE}
# Aggregate per-field weekly mosaics into single farm-level rasters for visualization
# This creates on-the-fly mosaics for current week and historical weeks without saving intermediate files

tryCatch({
  safe_log("Starting farm-level raster aggregation for overview maps")
  
  # Helper function to safely aggregate mosaics for a specific week
  aggregate_mosaics_safe <- function(week_num, year_num, label) {
    tryCatch({
      safe_log(paste("Aggregating mosaics for", label, "(week", week_num, ",", year_num, ")"))
      
      # Call the utility function from 90_report_utils.R
      # This function reads all per-field mosaics and merges them into a single raster
      farm_mosaic <- aggregate_per_field_mosaics_to_farm_level(
        weekly_mosaic_dir = weekly_CI_mosaic,
        target_week = week_num,
        target_year = year_num
      )
      
      if (!is.null(farm_mosaic)) {
        safe_log(paste("✓ Successfully aggregated farm mosaic for", label, ""))
        return(farm_mosaic)
      } else {
        safe_log(paste("Warning: Farm mosaic is NULL for", label), "WARNING")
        return(NULL)
      }
    }, error = function(e) {
      safe_log(paste("Error aggregating mosaics for", label, ":", e$message), "WARNING")
      return(NULL)
    })
  }
  
  # Aggregate mosaics for three weeks: current, week-1, week-3
  farm_mosaic_current <- aggregate_mosaics_safe(current_week, current_iso_year, "current week")
  farm_mosaic_minus_1 <- aggregate_mosaics_safe(as.numeric(week_minus_1), week_minus_1_year, "week-1")
  farm_mosaic_minus_3 <- aggregate_mosaics_safe(as.numeric(week_minus_3), week_minus_3_year, "week-3")
  
  # Extract CI band (5th band, or named "CI") from each aggregated mosaic
  farm_ci_current <- NULL
  farm_ci_minus_1 <- NULL
  farm_ci_minus_3 <- NULL
  
  if (!is.null(farm_mosaic_current)) {
    if ("CI" %in% names(farm_mosaic_current)) {
      farm_ci_current <- farm_mosaic_current[["CI"]]
    } else if (terra::nlyr(farm_mosaic_current) >= 5) {
      farm_ci_current <- farm_mosaic_current[[5]]  # CI is typically band 5 (R,G,B,NIR,CI)
    }
    if (!is.null(farm_ci_current)) {
      safe_log("✓ Extracted CI band from current week mosaic")
    }
  }
  
  if (!is.null(farm_mosaic_minus_1)) {
    if ("CI" %in% names(farm_mosaic_minus_1)) {
      farm_ci_minus_1 <- farm_mosaic_minus_1[["CI"]]
    } else if (terra::nlyr(farm_mosaic_minus_1) >= 5) {
      farm_ci_minus_1 <- farm_mosaic_minus_1[[5]]
    }
    if (!is.null(farm_ci_minus_1)) {
      safe_log("✓ Extracted CI band from week-1 mosaic")
    }
  }
  
  if (!is.null(farm_mosaic_minus_3)) {
    if ("CI" %in% names(farm_mosaic_minus_3)) {
      farm_ci_minus_3 <- farm_mosaic_minus_3[["CI"]]
    } else if (terra::nlyr(farm_mosaic_minus_3) >= 5) {
      farm_ci_minus_3 <- farm_mosaic_minus_3[[5]]
    }
    if (!is.null(farm_ci_minus_3)) {
      safe_log("✓ Extracted CI band from week-3 mosaic")
    }
  }
  
  # Calculate difference rasters
  farm_ci_diff_week <- NULL
  if (!is.null(farm_ci_current) && !is.null(farm_ci_minus_1)) {
    farm_ci_diff_week <- farm_ci_current - farm_ci_minus_1
    safe_log("✓ Calculated week-over-week difference raster")
  } else {
    safe_log("Warning: Cannot calculate week-over-week difference - missing data", "WARNING")
  }

  # Reproject rasters and boundaries for ggplot basemap (OSM uses EPSG:4326)
  farm_ci_current_ll <- farm_ci_current
  farm_ci_diff_week_ll <- farm_ci_diff_week
  AllPivots0_ll <- AllPivots0
  target_crs <- "EPSG:4326"

  downsample_raster <- function(r, max_cells = 2000000) {
    if (is.null(r)) {
      return(NULL)
    }
    n_cells <- terra::ncell(r)
    if (!is.na(n_cells) && n_cells > max_cells) {
      fact <- ceiling(sqrt(n_cells / max_cells))
      safe_log(paste("Downsampling raster by factor", fact), "INFO")
      return(terra::aggregate(r, fact = fact, fun = mean, na.rm = TRUE))
    }
    r
  }

  if (!is.null(farm_ci_current) && !terra::is.lonlat(farm_ci_current)) {
    farm_ci_current_ll <- terra::project(farm_ci_current, target_crs, method = "bilinear")
  }
  if (!is.null(farm_ci_diff_week) && !terra::is.lonlat(farm_ci_diff_week)) {
    farm_ci_diff_week_ll <- terra::project(farm_ci_diff_week, target_crs, method = "bilinear")
  }
  if (!is.null(AllPivots0)) {
    AllPivots0_ll <- sf::st_transform(AllPivots0, 4326)
  }

  farm_ci_current_ll <- downsample_raster(farm_ci_current_ll)
  farm_ci_diff_week_ll <- downsample_raster(farm_ci_diff_week_ll)

  # Ensure boundaries align with raster extent to avoid plotting issues
  sf::sf_use_s2(FALSE)
  if (!is.null(AllPivots0_ll) && !is.null(farm_ci_current_ll)) {
    AllPivots0_ll <- sf::st_make_valid(AllPivots0_ll)
    crop_bbox_current <- sf::st_as_sfc(sf::st_bbox(terra::ext(farm_ci_current_ll), crs = 4326))
    AllPivots0_ll <- sf::st_intersection(AllPivots0_ll, crop_bbox_current)
    AllPivots0_ll <- sf::st_collection_extract(AllPivots0_ll, "POLYGON")
  }

  # Prepare boundary and label data frames for ggplot (avoid geom_sf coord resets)
  bounds_df <- NULL
  labels_df <- NULL
  if (!is.null(AllPivots0_ll)) {
    bounds_coords <- sf::st_coordinates(AllPivots0_ll)
    bounds_df <- as.data.frame(bounds_coords)
    bounds_df$group <- interaction(bounds_df$L1, bounds_df$L2, drop = TRUE)
    label_pts <- sf::st_point_on_surface(AllPivots0_ll)
    labels_df <- cbind(as.data.frame(sf::st_coordinates(label_pts)), sub_field = label_pts$sub_field)
  }
  
  # Log completion
  safe_log("Farm-level raster aggregation complete")
  
}, error = function(e) {
  safe_log(paste("Error in farm-level raster aggregation:", e$message), "ERROR")
})
```

\newpage

```{r render_farm_ci_map, echo=FALSE, message=FALSE, warning=FALSE, fig.height=6.8, fig.width=8.5, dpi=150, dev='png'}
# Create farm-level chlorophyll index map with OpenStreetMap basemap
tryCatch({
  if (!is.null(farm_ci_current_ll)) {
    safe_log("Rendering farm-level CI overview map")
    
    # Convert raster to data.frame for ggplot
    ci_df <- as.data.frame(farm_ci_current_ll, xy = TRUE, na.rm = FALSE)
    colnames(ci_df) <- c("x", "y", "ci_value")
    
    # Choose color palette based on colorblind_friendly parameter
    if (colorblind_friendly) {
      fill_scale <- ggplot2::scale_fill_viridis_c(
        name = "Chlorophyll Index (CI)",
        limits = c(1, 8),
        direction = -1,  # Reversed: green=high, yellow/red=low
        na.value = "transparent",
        oob = scales::squish
      )
    } else {
      # Use Red-Yellow-Green diverging palette (reversed for intuitive interpretation)
      fill_scale <- ggplot2::scale_fill_distiller(
        palette = "RdYlGn",
        name = "Chlorophyll Index (CI)",
        limits = c(1, 8),
        direction = 1,  # Standard direction for RdYlGn
        na.value = "transparent"
      )
    }
    
    # Build the map
    ci_ext <- terra::ext(farm_ci_current_ll)
    ci_bbox <- sf::st_bbox(c(xmin = ci_ext$xmin, xmax = ci_ext$xmax, ymin = ci_ext$ymin, ymax = ci_ext$ymax), crs = 4326)

    map <- ggplot2::ggplot() +
      ggspatial::annotation_map_tile(
        type = "osm",
        zoom = 14,
        alpha = 0.4
      ) +
      # Add raster layer with CI values
      ggplot2::geom_raster(
        data = ci_df,
        ggplot2::aes(x = x, y = y, fill = ci_value)
      ) +
      fill_scale +
      ggplot2::coord_sf(
        crs = 4326,
        xlim = c(ci_ext$xmin, ci_ext$xmax),
        ylim = c(ci_ext$ymin, ci_ext$ymax),
        expand = FALSE
      )

    if (!is.null(bounds_df)) {
      map <- map + ggplot2::geom_path(
        data = bounds_df,
        ggplot2::aes(x = X, y = Y, group = group),
        color = "black",
        linewidth = 0.4
      )
    }

    if (!is.null(labels_df)) {
      map <- map + ggplot2::geom_text(
        data = labels_df,
        ggplot2::aes(x = X, y = Y, label = sub_field),
        size = 3,
        color = "black"
      )
    }

    map <- map +
      # Add scale bar and theme
      ggspatial::annotation_scale(
        location = "tr",
        width_hint = 0.25
      ) +
      ggplot2::theme_void() +
      ggplot2::theme(
        legend.position = "right",
        legend.direction = "vertical",
        legend.title = ggplot2::element_text(size = 10),
        legend.text = ggplot2::element_text(size = 9),
        plot.title = ggplot2::element_text(hjust = 0.5, size = 12, face = "bold"),
        panel.background = ggplot2::element_rect(fill = "white", color = NA)
      ) +
      ggplot2::labs(
        title = paste("Current Week CI Overview - Week", current_week, "of", current_iso_year)
      )
    
    # Print the map
    print(map)
    safe_log("✓ Farm-level CI map rendered successfully")
    
  } else {
    safe_log("Farm-level CI raster not available - skipping overview map", "WARNING")
    plot(1, type = "n", axes = FALSE, xlab = "", ylab = "")
    text(1, 1, "Farm-level CI overview map not available", cex = 1.5, col = "gray")
  }
}, error = function(e) {
  safe_log(paste("Error rendering farm CI overview map:", e$message), "ERROR")
  plot(1, type = "n", axes = FALSE, xlab = "", ylab = "")
  text(1, 1, "Error creating CI overview map", cex = 1.5, col = "red")
})
```

```{r render_farm_ci_diff_map, echo=FALSE, message=FALSE, warning=FALSE, fig.height=6.8, fig.width=8.5, dpi=150, dev='png'}
# Create farm-level CI difference map (week-over-week change)
tryCatch({
  if (!is.null(farm_ci_diff_week_ll)) {
    safe_log("Rendering farm-level CI difference map (week-over-week)")
    
    # Convert difference raster to data.frame for ggplot
    diff_df <- as.data.frame(farm_ci_diff_week_ll, xy = TRUE, na.rm = FALSE)
    colnames(diff_df) <- c("x", "y", "ci_diff")
    
    # Determine color palette based on colorblind_friendly parameter
    if (colorblind_friendly) {
      # Use plasma for colorblind-friendly diverging visualization
      fill_scale <- ggplot2::scale_fill_viridis_c(
        name = "CI Change (Week-over-Week)",
        option = "plasma",
        limits = c(-3, 3),
        na.value = "transparent",
        oob = scales::squish
      )
    } else {
      # Use Red-Blue diverging palette (red=decline, blue=increase)
      fill_scale <- ggplot2::scale_fill_distiller(
        palette = "RdBu",
        name = "CI Change (Week-over-Week)",
        limits = c(-3, 3),
        direction = 1,
        na.value = "transparent"
      )
    }
    
    # Build the map
    diff_ext <- terra::ext(farm_ci_diff_week_ll)
    diff_bbox <- sf::st_bbox(c(xmin = diff_ext$xmin, xmax = diff_ext$xmax, ymin = diff_ext$ymin, ymax = diff_ext$ymax), crs = 4326)

    map <- ggplot2::ggplot() +
      ggspatial::annotation_map_tile(
        type = "osm",
        zoom = 12,
        alpha = 0.4
      ) +
      # Add raster layer with difference values
      ggplot2::geom_raster(
        data = diff_df,
        ggplot2::aes(x = x, y = y, fill = ci_diff)
      ) +
      fill_scale +
      ggplot2::coord_sf(
        crs = 4326,
        xlim = c(diff_ext$xmin, diff_ext$xmax),
        ylim = c(diff_ext$ymin, diff_ext$ymax),
        expand = FALSE
      )

    if (!is.null(bounds_df)) {
      map <- map + ggplot2::geom_path(
        data = bounds_df,
        ggplot2::aes(x = X, y = Y, group = group),
        color = "black",
        linewidth = 0.4
      )
    }

    if (!is.null(labels_df)) {
      map <- map + ggplot2::geom_text(
        data = labels_df,
        ggplot2::aes(x = X, y = Y, label = sub_field),
        size = 3,
        color = "black"
      )
    }

    map <- map +
      # Add scale bar and theme
      ggspatial::annotation_scale(
        location = "tr",
        width_hint = 0.25
      ) +
      ggplot2::theme_void() +
      ggplot2::theme(
        legend.position = "right",
        legend.direction = "vertical",
        legend.title = ggplot2::element_text(size = 10),
        legend.text = ggplot2::element_text(size = 9),
        plot.title = ggplot2::element_text(hjust = 0.5, size = 12, face = "bold"),
        panel.background = ggplot2::element_rect(fill = "white", color = NA)
      ) +
      ggplot2::labs(
        title = paste("Weekly CI Change - Week", current_week, "vs Week", week_minus_1)
      )
    
    # Print the map
    print(map)
    safe_log("✓ Farm-level CI difference map rendered successfully")
    
  } else {
    safe_log("Farm-level difference raster not available - skipping difference map", "WARNING")
    plot(1, type = "n", axes = FALSE, xlab = "", ylab = "")
    text(1, 1, "Week-over-week difference map not available", cex = 1.5, col = "gray")
  }
}, error = function(e) {
  safe_log(paste("Error rendering farm CI difference map:", e$message), "ERROR")
  plot(1, type = "n", axes = FALSE, xlab = "", ylab = "")
  text(1, 1, "Error creating CI difference map", cex = 1.5, col = "red")
})
```

# Section 2: Field-by-Field Analysis

## Overview of Field-Level Insights
This section provides detailed, field-specific analyses including chlorophyll index maps, trend graphs, and performance metrics. Each field is analyzed individually to support targeted interventions.

**Key Elements per Field:**
- Current and historical CI maps                                      
- Week-over-week change visualizations                                           
- Cumulative growth trends                                 
- Field-specific KPI summaries                               

*Navigate to the following pages for individual field reports.*

\newpage

```{r generate_field_visualizations, echo=FALSE, fig.height=3.8, fig.width=10, dev='png', dpi=150, results='asis'}
# Generate detailed visualizations for each field using purrr::walk

tryCatch({
   # Prepare merged field list and week/year info
   AllPivots_merged <- AllPivots0 %>% 
    dplyr::filter(!is.na(field), !is.na(sub_field)) %>%
    dplyr::group_by(field) %>% 
    dplyr::summarise(.groups = 'drop')
  
   # Helper to get week/year from a date
   get_week_year <- function(date) {
    list(
      week = as.numeric(format(date, "%V")),
      year = as.numeric(format(date, "%G"))
    )
  }
  
  # Calculate week/year for current and historical weeks
  current_ww <- get_week_year(as.Date(today))
  minus_1_ww <- get_week_year(as.Date(today) - lubridate::weeks(1))
  minus_2_ww <- get_week_year(as.Date(today) - lubridate::weeks(2))
  minus_3_ww <- get_week_year(as.Date(today) - lubridate::weeks(3))
  
  message(paste("Processing", nrow(AllPivots_merged), "fields for weeks:", 
                current_ww$week, minus_1_ww$week, minus_2_ww$week, minus_3_ww$week))
  
  # Helper function to safely load per-field mosaic if it exists
  load_per_field_mosaic <- function(base_dir, field_name, week, year) {
    path <- file.path(base_dir, field_name, paste0("week_", sprintf("%02d", week), "_", year, ".tif"))
    if (file.exists(path)) {
      tryCatch({
        rast_obj <- terra::rast(path)
        # Extract CI band if present, otherwise first band
        if ("CI" %in% names(rast_obj)) {
          return(rast_obj[["CI"]])
        } else if (nlyr(rast_obj) > 0) {
          return(rast_obj[[1]])
        }
      }, error = function(e) {
        message(paste("Warning: Could not load", path, ":", e$message))
        return(NULL)
      })
    }
    return(NULL)
  }
  
  # Iterate through fields using purrr::walk
  purrr::walk(AllPivots_merged$field, function(field_name) {
    tryCatch({
      message(paste("Processing field:", field_name))
      
      # Load per-field rasters for all 4 weeks
      field_CI <- load_per_field_mosaic(weekly_CI_mosaic, field_name, 
                                        current_ww$week, current_ww$year)
      field_CI_m1 <- load_per_field_mosaic(weekly_CI_mosaic, field_name, 
                                           minus_1_ww$week, minus_1_ww$year)
      field_CI_m2 <- load_per_field_mosaic(weekly_CI_mosaic, field_name, 
                                           minus_2_ww$week, minus_2_ww$year)
      field_CI_m3 <- load_per_field_mosaic(weekly_CI_mosaic, field_name, 
                                           minus_3_ww$week, minus_3_ww$year)
      
      # Calculate difference rasters
      last_week_diff <- if (!is.null(field_CI) && !is.null(field_CI_m1)) {
        field_CI - field_CI_m1
      } else {
        NULL
      }
      
      three_week_diff <- if (!is.null(field_CI) && !is.null(field_CI_m3)) {
        field_CI - field_CI_m3
      } else {
        NULL
      }
      
      # Call ci_plot with field-specific rasters
      if (!is.null(field_CI)) {
        ci_plot(  
          pivotName = field_name,
          field_boundaries = AllPivots0,
          current_ci = field_CI,
          ci_minus_1 = field_CI_m1,
          ci_minus_2 = field_CI_m2,
          last_week_diff = last_week_diff,
          three_week_diff = three_week_diff,
          harvesting_data = harvesting_data,
          week = week,
          week_minus_1 = week_minus_1,
          week_minus_2 = week_minus_2,
          week_minus_3 = week_minus_3,
          borders = borders,
          colorblind_friendly = colorblind_friendly
        )
        #cat("\n\n")
      } else {
        message(paste("Warning: No raster data found for field", field_name))
      }
      
      # Handle CI quadrant data filter for special cases
      ci_quadrant_data <- if (project_dir == "esa" && field_name == "00F25") {
        CI_quadrant %>%
          dplyr::filter(field == "00F25") %>%
          dplyr::arrange(DAH) %>%
          dplyr::group_by(DAH) %>%
          dplyr::slice(1) %>%
          dplyr::ungroup()
      } else {
        CI_quadrant
      }
      
      # Call cum_ci_plot for trend analysis
      if (!is.null(CI_quadrant)) {
        cum_ci_plot(
          pivotName = field_name,
          ci_quadrant_data = ci_quadrant_data,
          plot_type = ci_plot_type,
          facet_on = facet_by_season,
          x_unit = x_axis_unit,
          colorblind_friendly = colorblind_friendly,
          show_benchmarks = TRUE,
          estate_name = project_dir,
          benchmark_percentiles = c(10, 50, 90),
          benchmark_data = benchmarks
        )
        #cat("\n")
      }
      
      # Add field-specific KPI summary if available
      if (exists("field_details_table") && !is.null(field_details_table) && nrow(field_details_table) > 0) {
        field_kpi <- field_details_table %>%
          dplyr::filter(Field_id == field_name)
        
        if (nrow(field_kpi) > 0) {
          # Format KPIs as compact single line (no interpretations, just values)
          kpi_parts <- c(
            sprintf("**CV:** %.2f", field_kpi$CV),
            sprintf("**Mean CI:** %.2f", field_kpi$Mean_CI)
          )
          
          # Add Weekly_CI_Change if available (note: capital C and I)
          if (!is.null(field_kpi$Weekly_CI_Change) && !is.na(field_kpi$Weekly_CI_Change)) {
            change_sign <- ifelse(field_kpi$Weekly_CI_Change >= 0, "+", "")
            kpi_parts <- c(kpi_parts, sprintf("**Δ CI:** %s%.2f", change_sign, field_kpi$Weekly_CI_Change))
          }
          
          # Compact trend display with symbols
          trend_compact <- case_when(
            grepl("Strong growth", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↑↑",
            grepl("Growth|Increasing", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↑",
            grepl("Stable|No growth", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "→",
            grepl("Slight decline", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↓",
            grepl("Strong decline|Severe", field_kpi$Trend_Interpretation, ignore.case = TRUE) ~ "↓↓",
            TRUE ~ field_kpi$Trend_Interpretation
          )
          kpi_parts <- c(kpi_parts, sprintf("**Trend:** %s", trend_compact))
          
          if (!is.na(field_kpi$TCH_Forecasted) && field_kpi$TCH_Forecasted > 0) {
            kpi_parts <- c(kpi_parts, sprintf("**Yield:** %.1f t/ha", field_kpi$TCH_Forecasted))
          }
          
          kpi_parts <- c(
            kpi_parts,
            sprintf("**Gap:** %.0f", field_kpi$Gap_Score),
            sprintf("**Patchiness:** %s", field_kpi$Patchiness_Risk),
            sprintf("**Decline:** %s", field_kpi$Decline_Severity)
          )
          
          cat(paste(kpi_parts, collapse = " | "), "\n\n")  # Double newline for markdown paragraph break
        }
      }
      
    }, error = function(e) {
      safe_log(paste("Error generating plots for field", field_name, ":", e$message), "ERROR")
      cat("\\newpage\n\n")
      cat("# Error generating plots for field ", field_name, "\n\n")
      cat(paste("Error:", e$message), "\n\n")
    })
  })
  
}, error = function(e) {
  safe_log(paste("Error in field visualization section:", e$message), "ERROR")
  cat("Error generating field plots. See log for details.\n\n")
})
```

```{r generate_subarea_visualizations, echo=FALSE, fig.height=3.8, fig.width=6.5, dpi=150, message=FALSE, warning=FALSE, results='asis', eval=FALSE}
# Alternative visualization grouped by sub-area (disabled by default)
tryCatch({
  # Group pivots by sub-area
  pivots_grouped <- AllPivots0 
  
  # Iterate over each subgroup
  for (subgroup in unique(pivots_grouped$sub_area)) {
    # Add subgroup heading
    cat("\n")
    cat("## Subgroup: ", subgroup, "\n")
    
    # Filter data for current subgroup
    subset_data <- dplyr::filter(pivots_grouped, sub_area == subgroup)
    
    # Generate visualizations for each field in the subgroup
    purrr::walk(subset_data$field, function(field_name) {
      cat("\n")
      ci_plot(field_name)
      cat("\n")
      cum_ci_plot(field_name)
      cat("\n")
    })
    
    # Add page break after each subgroup
    cat("\\newpage\n")
  }
}, error = function(e) {
  safe_log(paste("Error in subarea visualization section:", e$message), "ERROR")
  cat("Error generating subarea plots. See log for details.\n")
})
```

\newpage
##  Detailed Field Performance Summary by Field

The following table provides a comprehensive overview of all monitored fields with their key performance metrics from the KPI analysis.

```{r detailed_field_table, echo=FALSE, results='asis'}
# Detailed field performance table

if (!exists("field_details_table") || is.null(field_details_table) || nrow(field_details_table) == 0) {
  safe_log("No field details available for table", "WARNING")
  cat("No field-level KPI data available for this report period.\n")
  
} else {
  # Calculate field sizes from boundaries (convert to acres)
  field_sizes_source <- if (exists("AllPivots_merged") && inherits(AllPivots_merged, "sf")) AllPivots_merged else AllPivots0
  field_sizes_df <- field_sizes_source %>%
    mutate(
      field_size_acres = as.numeric(sf::st_area(geometry) / 4046.86)  # m² to acres
    ) %>%
    sf::st_drop_geometry() %>%
    select(field, field_size_acres)
  
  # Get field ages from CI quadrant if available
  field_ages_df <- if (!is.null(CI_quadrant) && nrow(CI_quadrant) > 0) {
    tryCatch({
      # Get current season and age for each field
      CI_quadrant %>%
        filter(Date <= as.Date(report_date)) %>%
        group_by(field, season) %>%
        summarise(last_date = max(Date), last_dah = max(DAH), .groups = 'drop') %>%
        group_by(field) %>%
        filter(season == max(season)) %>%
        select(field, Age_days = last_dah)
    }, error = function(e) {
      data.frame(field = character(), Age_days = numeric())
    })
  } else {
    data.frame(field = character(), Age_days = numeric())
  }
  
  # Join field sizes and ages to KPI data, simplified column selection
  # DEFENSIVE: Normalize field_details_table column structure before joining
  # Uses shared normalization function to ensure Field_id exists and all expected columns are present
  field_details_table <- normalize_field_details_columns(field_details_table)
  
  field_details_clean <- field_details_table %>%
    left_join(field_sizes_df, by = c("Field_id" = "field")) %>%
    left_join(field_ages_df, by = c("Field_id" = "field")) %>%
    mutate(
      # Only show yield forecast for fields >= 240 days old
      TCH_Forecasted = if_else(is.na(Age_days) | Age_days < 240, NA_real_, TCH_Forecasted),
      # Round numeric columns
      field_size_acres = round(field_size_acres, 1),
      Mean_CI = round(Mean_CI, 2),
      CV = round(CV, 2),
      Gap_Score = round(Gap_Score, 2),
      TCH_Forecasted = round(TCH_Forecasted, 1)
    )
  
  # Add Weekly_CI_Change if it exists in the data (note: capital C and I)
  if ("Weekly_CI_Change" %in% names(field_details_clean)) {
    field_details_clean <- field_details_clean %>%
      mutate(Weekly_CI_Change = round(Weekly_CI_Change, 2)) %>%
      select(
        Field = Field_id,
        `Field Size (acres)` = field_size_acres,
        `Mean CI` = Mean_CI,
        `Weekly CI Change` = Weekly_CI_Change,
        `Yield Forecast (t/ha)` = TCH_Forecasted,
        `Gap Score %` = Gap_Score,
        `Decline Risk` = Decline_Severity,
        `Patchiness Risk` = Patchiness_Risk,
        `CV Value` = CV
      )
  } else {
    field_details_clean <- field_details_clean %>%
      select(
        Field = Field_id,
        `Field Size (acres)` = field_size_acres,
        `Mean CI` = Mean_CI,
        `Yield Forecast (t/ha)` = TCH_Forecasted,
        `Gap Score %` = Gap_Score,
        `Decline Risk` = Decline_Severity,
        `Patchiness Risk` = Patchiness_Risk,
        `CV Value` = CV
      )
  }
  
  # Display the cleaned field table with flextable (fit to page width)
  ft <- flextable(field_details_clean) %>%
    set_caption("Detailed Field Performance Summary") %>%
    theme_booktabs() %>%
    set_table_properties(width = 1, layout = "autofit")  # Fit to 100% page width with auto-adjust
  
  knit_print(ft)
}
```

\newpage

# Section 3: Report Methodology and Definitions

## About This Report

This automated report provides weekly analysis of sugarcane crop health using satellite-derived Chlorophyll Index (CI) measurements. The analysis supports: 

• Scouting of growth related issues that are in need of attention  
• Timely actions can be taken such that negative impact is reduced  
• Monitoring of the crop growth rates of the farm, providing evidence of performance  
• Planning of harvest moment and mill logistics is supported such that optimal tonnage and sucrose levels can be harvested.   

The base of the report is the Chlorophyll Index. The chlorophyll index identifies:   
• Field-level crop health variations => target problem area's   
• Weekly changes in crop vigor => scout for diseases and stress  
• Areas requiring attention by the agricultural and irrigation teams   
• Growth patterns across different field sections   

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

### Explanation of the Report

This report provides a detailed analysis (3x3m of resolution) of your sugarcane fields based on satellite imagery. It supports you monitor crop health and development throughout the growing season. The data is processed weekly to give you timely insights for optimal farm management decisions.

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

### What You'll Find in This Report:

1. **Key Performance Indicators (KPIs):**  
   The report provides a farm-wide analysis based on weekly Chlorophyll Index (CI) measurements. Five comprehensive KPIs are calculated field by field to assess crop health:

   - **KPI 1: Field Uniformity** — Measures how consistently crop is developing across the field
     - **Metric:** Coefficient of Variation (CV) of CI pixel values
     - **Calculation:** CV = (Standard Deviation of CI) / (Mean CI)
     - **Categories:**
       - **Excellent:** CV < 0.08 (very uniform growth, minimal intervention needed)
       - **Good:** CV < 0.15 (acceptable uniformity, routine monitoring)
       - **Acceptable:** CV < 0.25 (moderate variation, monitor irrigation/fertility)
       - **Poor:** CV < 0.4 (high variation, investigate management issues)
       - **Very poor:** CV ≥ 0.4 (severe variation, immediate field scout required)
     - **Why it matters:** Uniform fields are easier to manage and typically produce better yields. Uneven growth suggests irrigation problems, fertility gaps, pests, or disease.
   
   - **KPI 2: Area Change (Weekly Growth)** — Tracks week-over-week CI changes to detect rapid improvements or declines
     - **Calculation:** Current Mean CI − Previous Mean CI (absolute change in CI units)
     - **Categories:**
       - **Rapid growth:** > +0.5 (excellent weekly progress)
       - **Positive growth:** +0.2 to +0.5 (steady improvement)
       - **Stable:** −0.2 to +0.2 (field maintained, no significant change)
       - **Declining:** −0.5 to −0.2 (slow decline, warrant closer monitoring)
       - **Rapid decline:** < −0.5 (alert: urgent issue requiring investigation)
     - **Why it matters:** Week-to-week changes reveal developing problems early, enabling timely intervention.
   
   - **KPI 3: TCH Forecasted (Yield Prediction)** — Predicts final harvest tonnage for mature fields
     - **Applies to:** Fields ≥ 240 days old (mature stage)
     - **Method:** Random Forest machine learning model trained on historical harvest data and CI trajectories
     - **Inputs:** Days after harvest (DAH) and CI growth rate (CI_per_day)
     - **Output:** Predicted tons of cane per hectare (t/ha)
     - **Why it matters:** Helps plan harvest timing, mill throughput, and revenue forecasting for mature crops.
   
   - **KPI 4: Growth Decline (4-Week Trend)** — Assesses short-term growth trajectory using linear regression
     - **Calculation:** Linear slope of CI values over the previous 4 weeks
     - **Categories:**
       - **Strong growth:** Slope > 0.1 CI units/week (excellent sustained progress)
       - **Weak growth:** Slope 0–0.1 (slow improvement, monitor closely)
       - **Slight decline:** Slope −0.1–0 (low severity, non-urgent observation)
       - **Moderate decline:** Slope −0.3 to −0.1 (medium severity, scouting recommended)
       - **Strong decline:** Slope < −0.3 (high severity, immediate field investigation required)
     - **Why it matters:** Trend analysis reveals whether crop is accelerating, stalling, or stressed over time.
   
   - **KPI 5: Field Patchiness (Heterogeneity)** — Combines two complementary spatial metrics for comprehensive heterogeneity assessment
     - **Metric 1: Gini Coefficient** — Statistical measure of distribution inequality in CI pixel values
       - **Formula:** (2 × Σ(i × sorted_CI)) / (n × Σ(sorted_CI)) − (n+1)/n
       - **Range:** 0 (perfectly uniform) to 1 (highly unequal)
       - **Interpretation:** Low Gini (< 0.15) = good uniformity; High Gini (> 0.3) = significant heterogeneity
     - **Metric 2: Moran's I** — Spatial autocorrelation indicating whether high/low areas are clustered or scattered
       - **Range:** −1 (dispersed pattern) to +1 (strong clustering)
       - **Thresholds:** Moran's I > 0.85 indicates clustered problem areas; < 0.75 suggests scattered issues
     - **Risk Determination (Gini + Moran's I Combined):**
       - **Minimal Risk:** Gini < 0.15 (excellent uniformity regardless of spatial pattern)
       - **Low Risk:** Gini 0.15–0.30, Moran's I < 0.85 (moderate variation, scattered distribution)
       - **Medium Risk:** Gini 0.15–0.30, Moran's I > 0.85 OR Gini 0.30–0.50, Moran's I < 0.85 (notable issues)
       - **High Risk:** Gini > 0.30, Moran's I > 0.85 (severe heterogeneity with localized clusters—urgent scouting needed)
     - **Why it matters:** High patchiness may indicate irrigation inefficiencies, localized pest pressure, fertility variation, or disease spread. Combined Gini + Moran's I reveals not just *how much* variation exists, but also *how it's distributed* spatially. CI reflects chlorophyll = nitrogen status + plant health + vigor. High CV/Patchiness often signals N gaps, water stress, pests (borers), or ratoon decline.

   - **Uniformity vs. Patchiness — What's the Difference?**
     Both KPIs measure variation, but they answer different questions and drive different management actions:
     - **Uniformity (CV-based)** answers: "*Is* growth even across the field?" — it detects whether a problem exists but not where.
     - **Patchiness (Gini + Moran's I)** answers: "*Where* are problems and how are they arranged?" — it reveals the spatial pattern.
     
     **Practical example:** Two fields both score "Poor" on Uniformity (CV = 0.25). However:
     - Field A has scattered low-CI patches (Moran's I = 0.6) → suggests *random* stress (disease pressure, uneven irrigation)
     - Field B has clustered low-CI in one corner (Moran's I = 0.95) → suggests *localized* problem (drainage, compaction, pest hotspot)
     
     Your scouting and remediation strategy should differ: Field A might need systemic irrigation adjustment or disease management; Field B might need soil remediation in the affected corner. **Patchiness tells you *where to focus your effort*.**
   
   - **KPI 6: Gap Score (Establishment Quality)** — Quantifies field gaps and areas of poor crop establishment
     - **Calculation Method:** Statistical outlier detection (2σ method)
       - Identifies pixels with CI below: **Median CI − (2 × Standard Deviation)**
       - Calculates: **Gap Score = (Outlier Pixels / Total Pixels) × 100**
       - Example: If 2 of 100 pixels fall below threshold → Gap Score = 2%
     - **Score Ranges & Interpretation:**
       - **0–10%:** Minimal gaps (excellent establishment, healthy field)
       - **10–25%:** Moderate gaps (monitor for expansion, coordinate with agronomy)
       - **≥ 25%:** Significant gaps (consider targeted replanting or rehabilitation)
     - **Why it matters:** Gap scores reveal areas of poor establishment that may indicate early growth problems or harvest-related residue issues. Lower is better (0–3% is typical for healthy fields).

2. **Overview Map: Growth on Farm:**  
   Provides a traffic light overview of field-by-field growth status for quick prioritization and reporting.

3. **Chlorophyll Index Overview Map:**  
   Shows current CI values for all fields, helping to identify high- and low-performing areas.

4. **Field-by-Field Analysis:**  
   Includes detailed maps, trend graphs, and performance metrics for each field.

5. **Yield Prediction:**  
   For mature crops (over 240 days), yield is predicted using current and historical CI data.

6. **Farm Overview Table:**  
   Presents numerical field-level results for all KPIs.

---

### Historical Benchmark Lines 

The CI time series graphs include historical benchmark lines for the 10th, 50th, and 90th percentiles of CI values across all fields and seasons.  
**Note:** These lines are now all rendered as solid lines (not dashed or dotted), with different colors for each percentile.  
- **10th Percentile:** Lower end of historical performance  
- **50th Percentile:** Median historical performance  
- **90th Percentile:** Upper end of historical performance  
Comparing the current season to these lines helps assess whether crop growth is below, at, or above historical norms.

\newpage
## Report Metadata

```{r report_metadata, echo=FALSE, results='asis'}
metadata_info <- data.frame(
  Metric = c("Report Generated", "Data Source", "Analysis Period", "Total Fields", "Next Update"),
  Value = c(
    format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
    paste("Project", toupper(project_dir)),
    paste("Week", current_week, "of", year),
    ifelse(exists("AllPivots0"), nrow(AllPivots0 %>% filter(!is.na(field)) %>% group_by(field) %>% summarise()), "Unknown"),
    "Next Wednesday"
  )
)

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.*