---
params:
  ref: "word-styles-reference-var1.docx"
  output_file: "CI_report.docx"
  report_date: "2026-02-18" #!r Sys.Date()
  data_dir: "tpc"
  mail_day: "Wednesday"
  borders: FALSE
  ci_plot_type: "both"
  colorblind_friendly: TRUE
  language: "en" # Language code for translations (e.g., "en", "es-mx", "pt-br")
  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
language <- params$language
```

```{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)
  
  # Translation handling
  library(jsonlite)   # For reading in the translation JSON
  library(glue)       # For easy variable formatting in texts
})

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

tryCatch({
  source("90_rainfall_utils.R")
}, error = function(e) {
  tryCatch({
    source(here::here("r_app", "90_rainfall_utils.R"))
  }, error = function(e) {
    message("Could not load 90_rainfall_utils.R - rain overlay disabled: ", 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
}
```

```{r load_translations, include = FALSE}
# Load the translations document from JSON
tryCatch({
  translations_json <- fromJSON("translations/translations_90.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 before)
  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
})

# tr_key() and map_trend_to_arrow() are defined in 90_report_utils.R (sourced above)
```

<!-- 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")`

`r tr_key("report_structure")`

`r tr_key("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("\n", tr_key("field_unif"))
    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(" -", tr_key("unif_status"))
      }
    }
  }
  
  # 2. Area change insights - group by interpretation
  if (!is.null(summary_tables$area_change) && nrow(summary_tables$area_change) > 0) {
    cat("\n\n", tr_key("field_area"))
    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(" -", tr_key("area_status"))
      }
    }
  }
  
  # 3. Growth trend insights - group by trend_interpretation
  if (!is.null(summary_tables$growth_decline) && nrow(summary_tables$growth_decline) > 0) {
    cat("\n\n", tr_key("growth_trend"))
    growth_counts <- summary_tables$growth_decline %>%
      dplyr::select(trend = trend_interpretation, count = field_count)
    
    for (i in seq_len(nrow(growth_counts))) {
      trend <- growth_counts$trend[i]
      count <- growth_counts$count[i]
      if (!is.na(trend) && !is.na(count) && count > 0) {
        cat(" -", tr_key("trend_status"))
      }
    }
  }
  
  # 4. Patchiness insights - group by patchiness_risk
  if (!is.null(summary_tables$patchiness) && nrow(summary_tables$patchiness) > 0) {
    cat("\n\n", tr_key("patch_risk"))
    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(" -", tr_key("patch_status"))
      }
    }
  }
  
  # 5. Total fields analyzed
  if (!is.null(summary_tables$uniformity) && "field_count" %in% names(summary_tables$uniformity)) {
    total_fields <- sum(summary_tables$uniformity$field_count, na.rm = TRUE)
  } else {
    total_fields <- NA_integer_
  }
  
  if (!is.na(total_fields)) {
    cat("\n\n", tr_key("tot_fields_analyzed"))
  }

  # Total area analyzed
  if (exists("field_details_table") && !is.null(field_details_table)) {
    area_col_name <- paste0("Area_", get_area_unit_label(AREA_UNIT_PREFERENCE))
    unit_label    <- get_area_unit_label(AREA_UNIT_PREFERENCE)
    if (area_col_name %in% names(field_details_table)) {
      total_area <- sum(field_details_table[[area_col_name]], na.rm = TRUE)
      cat("\n\n", tr_key("tot_area_analyzed"))
    }
  }

} else {
  cat(tr_key("kpi_na"))
}
```

\newpage

`r tr_key("section_i")`

`r tr_key("exec_summary")`\n\n

```{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_col = "area_sum"),
      area_change    = list(display = "Area Change",      level_col = "interpretation",     count_col = "field_count", area_col = "area_sum"),
      growth_decline = list(display = "4-Week Trend",     level_col = "trend_interpretation", count_col = "field_count", area_col = "area_sum"),
      patchiness     = list(display = "Field Patchiness", level_col = "gini_category",     count_col = "field_count", detail_col = "patchiness_risk", area_col = "area_sum"),
      tch_forecast   = list(display = "TCH Forecasted",   level_col = "tch_category",      count_col = "field_count", detail_col = "range", area_col = "area_sum"),
      gap_filling    = list(display = "Gaps",              level_col = "gap_level",         count_col = "field_count", area_col = "area_sum")
    )

    # Enrich summary_tables with area_sum from field_details_table (mirrors script 91 pattern)
    area_col_name <- paste0("Area_", get_area_unit_label(AREA_UNIT_PREFERENCE))
    unit_label    <- get_area_unit_label(AREA_UNIT_PREFERENCE)
    has_area <- exists("field_details_table") && !is.null(field_details_table) &&
                area_col_name %in% names(field_details_table)
    if (has_area) {
      fdt <- field_details_table
      if (!is.null(summary_tables$uniformity) && "Uniformity_Category" %in% names(fdt)) {
        summary_tables$uniformity <- summary_tables$uniformity %>%
          left_join(fdt %>% group_by(interpretation = Uniformity_Category) %>%
                      summarise(area_sum = sum(.data[[area_col_name]], na.rm = TRUE), .groups = "drop"),
                    by = "interpretation")
      }
      if (!is.null(summary_tables$area_change) && "Area_Change_Interpretation" %in% names(fdt)) {
        summary_tables$area_change <- summary_tables$area_change %>%
          left_join(fdt %>% group_by(interpretation = Area_Change_Interpretation) %>%
                      summarise(area_sum = sum(.data[[area_col_name]], na.rm = TRUE), .groups = "drop"),
                    by = "interpretation")
      }
      if (!is.null(summary_tables$growth_decline) && "Trend_Interpretation" %in% names(fdt)) {
        summary_tables$growth_decline <- summary_tables$growth_decline %>%
          left_join(fdt %>% group_by(trend_interpretation = Trend_Interpretation) %>%
                      summarise(area_sum = sum(.data[[area_col_name]], na.rm = TRUE), .groups = "drop"),
                    by = "trend_interpretation")
      }
      if (!is.null(summary_tables$patchiness) && all(c("Patchiness_Risk", "Gini_Coefficient") %in% names(fdt))) {
        summary_tables$patchiness <- summary_tables$patchiness %>%
          left_join(
            fdt %>%
              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(area_sum = sum(.data[[area_col_name]], na.rm = TRUE), .groups = "drop"),
            by = c("gini_category", "patchiness_risk")
          )
      }
      if (!is.null(summary_tables$gap_filling) && "Gap_Level" %in% names(fdt)) {
        summary_tables$gap_filling <- summary_tables$gap_filling %>%
          left_join(fdt %>% group_by(gap_level = Gap_Level) %>%
                      summarise(area_sum = sum(.data[[area_col_name]], na.rm = TRUE), .groups = "drop"),
                    by = "gap_level")
      }
      # TCH forecast: reproduce the same quartile logic used when building summary_tables so we
      # can assign each field to a tch_category and sum its area
      if (!is.null(summary_tables$tch_forecast) && "TCH_Forecasted" %in% names(fdt)) {
        tch_vals <- fdt %>% dplyr::filter(!is.na(TCH_Forecasted)) %>% dplyr::pull(TCH_Forecasted)
        if (length(tch_vals) > 0) {
          if (length(unique(tch_vals)) == 1) {
            area_tch <- fdt %>%
              dplyr::filter(!is.na(TCH_Forecasted)) %>%
              dplyr::summarise(area_sum = sum(.data[[area_col_name]], na.rm = TRUE)) %>%
              dplyr::mutate(tch_category = "All equal")
          } else {
            q25 <- quantile(tch_vals, 0.25, na.rm = TRUE)
            q75 <- quantile(tch_vals, 0.75, na.rm = TRUE)
            area_tch <- fdt %>%
              dplyr::filter(!is.na(TCH_Forecasted)) %>%
              dplyr::mutate(tch_category = dplyr::case_when(
                TCH_Forecasted >= q75 ~ "Top 25%",
                TCH_Forecasted >= q25 ~ "Middle 50%",
                TRUE                  ~ "Bottom 25%"
              )) %>%
              dplyr::group_by(tch_category) %>%
              dplyr::summarise(area_sum = sum(.data[[area_col_name]], na.rm = TRUE), .groups = "drop")
          }
          summary_tables$tch_forecast <- summary_tables$tch_forecast %>%
            left_join(area_tch, by = "tch_category")
        }
      }
    }

    standardize_kpi <- function(df, level_col, count_col, detail_col = NULL, area_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]]
      }

      area_vals <- if (!is.null(area_col) && area_col %in% names(df))
                     round(df[[area_col]], 1)
                   else
                     rep(NA_real_, nrow(df))

      df %>%
        dplyr::transmute(
          Level = if (level_col == "trend_interpretation") map_trend_to_arrow(display_level, include_text = TRUE) else as.character(display_level),
          Count = as.integer(round(as.numeric(.data[[count_col]]))),
          Area  = area_vals,
          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 and area_col if present 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,
                                  config[["area_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, Area, Percent)

      # Translate the table for visualization
      names(display_df) <- c(
        tr_key("KPI"), tr_key("Level"), tr_key("Count"),
        paste0(tr_key("Area"), " (", unit_label, ")"),
        tr_key("Percent")
      )
      display_df[, 1:2] <- lapply(display_df[, 1:2], function(col) sapply(col, tr_key))      
      ft <- flextable(display_df) %>%
        merge_v(j = tr_key("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(tr_key("no_kpi_table"))
    }
    
  }, error = function(e) {
    safe_log(paste("Error displaying KPI tables:", e$message), "WARNING")
    cat(tr_key("kpi_table_error"))
  })
  
} else {
  cat("Note: KPI summary tables have not been loaded. Detailed KPI analysis will be available once data is computed.\n")
}
```

\newpage
`r tr_key("field_alerts")`

```{r field_alerts_table, echo=FALSE, results='asis'}
# generate_field_alerts() is defined in 90_report_utils.R (sourced above).
# field_details_table has already been normalised by normalize_field_details_columns().

# Generate and display alerts table
if (exists("field_details_table") && !is.null(field_details_table) && nrow(field_details_table) > 0) {
  alerts_data <- generate_field_alerts(field_details_table)
  if (!is.null(alerts_data) && nrow(alerts_data) > 0) {
    ft <- flextable(alerts_data) %>%
#      set_caption("Field Alerts Summary") %>%
      autofit()
    ft
  } else {
    cat(tr_key("alerts_na"))
  }
} 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
      
      # Calculate field sizes (area) from geometry using unified function
      field_sizes <- if (!is.null(sf::st_geometry(AllPivots0)) && !all(sf::st_is_empty(sf::st_geometry(AllPivots0)))) {
        tryCatch({
          calculate_area_from_geometry(AllPivots0, unit = AREA_UNIT_PREFERENCE)
        }, error = function(e) {
          safe_log(paste("Warning: Could not calculate area from geometry:", e$message), "WARNING")
          rep(NA_real_, length(field_names))
        })
      } 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)
      unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE)
      area_col_name <- paste0("Area_", unit_label)
      
      field_details_table <- tibble::tibble(
        Field_id = field_names,
        !! area_col_name := 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")
  
  # Aggregate per-field mosaics into farm-level rasters for current, week-1, week-3
  # aggregate_per_field_mosaics_to_farm_level() is defined in 90_report_utils.R (sourced above)
  farm_mosaic_current <- aggregate_per_field_mosaics_to_farm_level(
    weekly_mosaic_dir = weekly_CI_mosaic,
    target_week = current_week,
    target_year = current_iso_year
  )
  farm_mosaic_minus_1 <- aggregate_per_field_mosaics_to_farm_level(
    weekly_mosaic_dir = weekly_CI_mosaic,
    target_week = as.numeric(week_minus_1),
    target_year = week_minus_1_year
  )
  farm_mosaic_minus_3 <- aggregate_per_field_mosaics_to_farm_level(
    weekly_mosaic_dir = weekly_CI_mosaic,
    target_week = as.numeric(week_minus_3),
    target_year = week_minus_3_year
  )
  
  # 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() is defined in 90_report_utils.R (sourced above)

  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 = tr_key("legend_ci"),
        limits = c(1, 8),
        direction = 1,
        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 = tr_key("legend_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 = tr_key("ci_overview_title")
      )
    
    # 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, fig.height=6.8, fig.width=8.5, message=FALSE, warning=FALSE, dev='png', dpi=150}
# 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 = tr_key("legend_ci_change"),
        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 = tr_key("legend_ci_change"),
        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 = tr_key("ci_change_title")
      )
    
    # 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")
})
```

\newpage

`r tr_key("section_ii")`

\newpage

```{r generate_field_visualizations, echo=FALSE, fig.height=3.7, 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')
  
  # get_week_year() is defined in 90_report_utils.R (sourced above)

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


  # load_per_field_mosaic() is defined in 90_report_utils.R (sourced above)

  # --- Fetch rain for all fields (one API call per 0.25° tile) ---
  all_rain_data <- NULL
  if (exists("rain_fetch_for_fields") && !is.null(CI_quadrant) && nrow(CI_quadrant) > 0) {
    tryCatch({
      # Compute field centroids from AllPivots0 (already-loaded sf object)
      field_centroids <- AllPivots0 %>%
        dplyr::group_by(field) %>%
        dplyr::summarise(geometry = sf::st_union(geometry), .groups = "drop") %>%
        sf::st_centroid() %>%
        dplyr::mutate(
          longitude = sf::st_coordinates(geometry)[, 1],
          latitude  = sf::st_coordinates(geometry)[, 2]
        ) %>%
        sf::st_drop_geometry() %>%
        dplyr::select(field_id = field, latitude, longitude)

      # Determine current-season date range across all fields
      rain_start <- CI_quadrant %>%
        dplyr::group_by(field) %>%
        dplyr::filter(season == max(season)) %>%
        dplyr::summarise(s = min(Date, na.rm = TRUE), .groups = "drop") %>%
        dplyr::pull(s) %>%
        min(na.rm = TRUE)
      rain_end <- as.Date(today)

      message(paste("Fetching rain data from", rain_start, "to", rain_end,
                    "for", nrow(field_centroids), "fields"))

      all_rain_data <- rain_fetch_for_fields(
        centroids_df = field_centroids,
        start_date   = rain_start,
        end_date     = rain_end
      )
      message(paste("Rain data fetched:", nrow(all_rain_data), "rows"))
    }, error = function(e) {
      message("Rain fetch failed - overlay disabled: ", e$message)
      all_rain_data <<- 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)) {
        field_rain <- if (!is.null(all_rain_data)) {
          all_rain_data %>%
            dplyr::filter(field_id == field_name) %>%
            dplyr::select(date, rain_mm)
        } else {
          NULL
        }

        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,
          rain_data = field_rain
        )
        #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("**%s:** %.2f", tr_key("cv_value"), field_kpi$CV),
            sprintf("**%s:** %.2f", tr_key("mean_ci"), field_kpi$Mean_CI)
          )

          # Prepend area as first item (static field attribute)
          if (area_col_name %in% names(field_kpi) && !is.na(field_kpi[[area_col_name]])) {
            kpi_parts <- c(
              sprintf("**%s:** %.1f %s", tr_key("Area"), field_kpi[[area_col_name]], unit_label),
              kpi_parts
            )
          }
          
          # 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("**Δ%s:** %s%.2f", tr_key("CI"), change_sign, field_kpi$Weekly_CI_Change))
          }
          
          # Compact trend display with symbols (arrows only)
          trend_compact <- map_trend_to_arrow(field_kpi$Trend_Interpretation, include_text = FALSE)
          
          # Log warning if trend value was unmatched (fallback to em-dash)
          if (!is.na(trend_compact) && trend_compact == "—") {
            warning(paste0("Trend_Interpretation mismatch for field '", field_name, "': '", 
                          field_kpi$Trend_Interpretation, "' did not match any expected pattern. ",
                          "Check 80_utils_agronomic_support.R trend categories or map_trend_to_arrow() patterns."))
          }
          
          kpi_parts <- c(kpi_parts, sprintf("**%s:** %s", tr_key("Trend"), trend_compact))
          
          if (!is.na(field_kpi$TCH_Forecasted) && field_kpi$TCH_Forecasted > 0) {
            kpi_parts <- c(kpi_parts, sprintf("**%s:** %.1f t/ha", tr_key("Yield"), field_kpi$TCH_Forecasted))
          }
          
          kpi_parts <- c(
            kpi_parts,
            sprintf("**%s:** %.0f%%", tr_key("Gaps"), field_kpi$Gap_Score),
            sprintf("**%s:** %s", tr_key("Patchiness"), tr_key(field_kpi$Patchiness_Risk))
          )
          
          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")
})
```

\newpage

`r tr_key("detailed_field")`

```{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(tr_key("no_field_data"))
  
} else {
  # Get area column from KPI data (e.g., Area_ha or Area_ac based on preference)
  # If not available, calculate from boundaries
  unit_label <- get_area_unit_label(AREA_UNIT_PREFERENCE)
  area_col_name <- paste0("Area_", unit_label)
  
  # Check if area column exists in field_details_table
  if (area_col_name %in% names(field_details_table)) {
    # Use area from KPI data
    field_sizes_df <- field_details_table %>%
      select(Field_id, !! area_col_name) %>%
      rename(field_size_area = !! area_col_name) %>%
      mutate(field = Field_id) %>%
      select(field, field_size_area)
  } else {
    # Fallback: calculate field sizes from boundaries using unified function
    field_sizes_source <- if (exists("AllPivots_merged") && inherits(AllPivots_merged, "sf")) AllPivots_merged else AllPivots0
    field_sizes_df <- tryCatch({
      field_areas <- calculate_area_from_geometry(field_sizes_source, unit = AREA_UNIT_PREFERENCE)
      data.frame(
        field = field_sizes_source$field,
        field_size_area = as.numeric(field_areas),
        stringsAsFactors = FALSE
      )
    }, error = function(e) {
      safe_log(paste("Warning: Could not calculate field areas:", e$message), "WARNING")
      data.frame(field = character(), field_size_area = numeric())
    })
  }
  
  # 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 (make sure field_size_area is rounded before rename)
      field_size_area = round(field_size_area, 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)
  # Replace Decline_Severity with Trend_Interpretation (arrows only)
  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 = field_size_area,
        mean_ci = Mean_CI,
        weekly_ci_change = Weekly_CI_Change,
        yield_forecast = TCH_Forecasted,
        gap_score = Gap_Score,
        trend = Trend_Interpretation,
        patchiness_risk = Patchiness_Risk,
        cv_value = CV
      )
  } else {
    field_details_clean <- field_details_clean %>%
      select(
        field = Field_id,
        field_size = field_size_area,
        mean_ci = Mean_CI,
        yield_forecast = TCH_Forecasted,
        gap_score = Gap_Score,
        trend = Trend_Interpretation,
        patchiness_risk = Patchiness_Risk,
        cv_value = CV
      )
  }
  
  # Convert trend to arrows only (canonical shared helper)
  # Translate patchiness_risk levels
  field_details_clean <- field_details_clean %>%
    mutate(
      # Map trend categories to arrows only using shared helper
      trend = map_trend_to_arrow(trend, include_text = FALSE),
      patchiness_risk = sapply(patchiness_risk, tr_key)
    )
  
  # Log warnings for any unmatched trend values (em-dash fallback)
  unmatched_trends <- field_details_clean %>%
    filter(trend == "—") %>%
    pull(field)
  
  if (length(unmatched_trends) > 0) {
    for (field_with_mismatch in unmatched_trends) {
      warning(paste0("Trend_Interpretation mismatch in field_details_table for field '", 
                     field_with_mismatch, "': value did not match expected trend patterns. ",
                     "Check 80_utils_agronomic_support.R trend categories or map_trend_to_arrow() patterns."))
    }
  }
  
  # Translation labels for flextable
  header_labels <- list(
    field = tr_key("field"),
    field_size = tr_key("field_size"),
    mean_ci = tr_key("mean_ci"),
    weekly_ci_change = tr_key("weekly_ci_change"),
    yield_forecast = tr_key("yield_forecast"),
    gap_score = tr_key("gap_score"),
    trend = tr_key("Trend"),
    patchiness_risk = tr_key("patchiness_risk"),
    cv_value = tr_key("cv_value")
  )
  
  # Display the cleaned field table with flextable (fit to page width)
  ft <- flextable(field_details_clean) %>%
    set_header_labels(values = header_labels) %>%
    set_caption(tr_key("detailed_field_caption")) %>%
    theme_booktabs() %>%
    set_table_properties(width = 1, layout = "autofit")  # Fit to 100% page width with auto-adjust
  
  knit_print(ft)
} 
```

\newpage

`r tr_key("section_iii")`

```{r include=FALSE}
# Tries to get the CI graph in different language, otherwise falls back on English
target_img <- paste0("CI_graph_example_", language, ".png")
img_path <- ifelse(file.exists(target_img), target_img, "CI_graph_example.png")
```

<div align="center">
![`r tr_key("ci_caption")`](`r img_path`){width=4in}
</div>

`r tr_key("sec_iii_1")`
   
    - `r tr_key("kpi_i")`
        - `r tr_key("kpi_i_metric")`
        - `r tr_key("kpi_i_calc")`
        - `r tr_key("kpi_categories")`
            - `r tr_key("kpi_i_excellent")`
            - `r tr_key("kpi_i_good")`
            - `r tr_key("kpi_i_accept")`
            - `r tr_key("kpi_i_poor")`
            - `r tr_key("kpi_i_verypoor")`
        - `r tr_key("kpi_i_why")` 

    - `r tr_key("kpi_ii")`
        - `r tr_key("kpi_ii_calc")`
        - `r tr_key("kpi_categories")`
            - `r tr_key("kpi_ii_rapid")`
            - `r tr_key("kpi_ii_positive")`
            - `r tr_key("kpi_ii_stable")`
            - `r tr_key("kpi_ii_declining")`
            - `r tr_key("kpi_ii_rapid_decline")`
        - `r tr_key("kpi_ii_why")` 

    - `r tr_key("kpi_iii")`
        - `r tr_key("kpi_iii_applies")`
        - `r tr_key("kpi_iii_method")`
        - `r tr_key("kpi_iii_input")`
        - `r tr_key("kpi_iii_output")`
        - `r tr_key("kpi_iii_why")`
   
    - `r tr_key("kpi_iv")`
        - `r tr_key("kpi_iv_calc")`
        - `r tr_key("kpi_categories")`
            - `r tr_key("kpi_iv_str_improve")`
            - `r tr_key("kpi_iv_weak_improve")`
            - `r tr_key("kpi_iv_stable")`
            - `r tr_key("kpi_iv_weak_decline")`
            - `r tr_key("kpi_iv_str_decline")`
        - `r tr_key("kpi_iv_why")`
   
    - `r tr_key("kpi_v")`
        - `r tr_key("kpi_v_met1")` 
            - `r tr_key("kpi_v_form")`
            - `r tr_key("kpi_v_range")`
            - `r tr_key("kpi_v_interpretation")`
        - `r tr_key("kpi_v_met2")`
            - `r tr_key("kpi_v_met2_range")`
            - `r tr_key("kpi_v_thresh")`
        - `r tr_key("kpi_v_risk")`
            - `r tr_key("kpi_v_minimal")`
            - `r tr_key("kpi_v_low")`
            - `r tr_key("kpi_v_medium")`
            - `r tr_key("kpi_v_high")`
        - `r tr_key("kpi_v_why")` 
     
    - `r tr_key("kpi_vi")`
        - `r tr_key("kpi_vi_calc")`
            - `r tr_key("kpi_vi_identify")`
            - `r tr_key("kpi_vi_calculates")`
            - `r tr_key("kpi_vi_example")`
        - `r tr_key("kpi_vi_scores")`
            - `r tr_key("kpi_vi_0")`
            - `r tr_key("kpi_vi_10")`
            - `r tr_key("kpi_vi_25")`
        - `r tr_key("kpi_vi_why")`

`r tr_key("sec_iii_2_to_6")`

---

`r tr_key("hist_benchmark")`

`r tr_key("unif_v_patch")`

- `r tr_key("unif")`
- `r tr_key("patch")`

`r tr_key("practical_example")`

- `r tr_key("field_a")`
- `r tr_key("field_b")`

`r tr_key("scouting")`

\newpage
`r tr_key("metadata")`

```{r report_metadata, echo=FALSE, results='asis'}
metadata_info <- data.frame(
  Metric = c(tr_key("report_gen"), tr_key("data_source"), tr_key("analysis_period"), tr_key("tot_fields"), tr_key("next_update")),
  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),
    if (exists("AllPivots0")) { nrow(AllPivots0 %>% filter(!is.na(field)) %>% group_by(field) %>% summarise()) } else { tr_key("unknown") },
    tr_key("next_wed")
  )
)

# Set names of columns according to localisation
names(metadata_info) <- c(tr_key("metric"), tr_key("value"))

ft <- flextable(metadata_info) %>%
  set_caption(tr_key("metadata_caption")) %>%
  autofit()

ft
```

`r tr_key("disclaimer")`
