from __future__ import annotations

import io
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Iterable

import pandas as pd
import matplotlib.pyplot as plt
from docx import Document
from docx.enum.section import WD_ORIENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Inches, Pt, RGBColor

from src.analysis.grouping import create_turbine_groups
from src.analysis.histogram import create_lightning_histogram_pages
from src.analysis.risk import calculate_turbine_risks
from src.analysis.statistics import calculate_lightning_statistics
from src.config import config
from src.reporting.precompute import precompute_group_distances_and_rings
from src.reporting.docx_sections import (
	_build_current_vs_distance_chart,
	build_group_lightning_table_data,
	build_risk_table_data,
)
from src.utils import (
	format_datetime_ddmmyyyy_hhmm,
	format_datetime_to_local_display,
	get_utc_offset_label,
	now_in_timezone,
	get_analysis_radius_m,
)
from src.analysis.geospatial import haversine_distance
from src.visualization.maps import plot_cloud_to_ground_coordinate_plane, plot_intercloud_coordinate_plane
from src.visualization.storm_cells import (
	create_storm_cells_map,
	create_storm_cells_summary,
	calculate_storm_cell_centroid,
	filter_storm_data_by_date_range,
	filter_storm_data_by_turbine_proximity,
	load_storm_data_from_json,
)
from src.reporting.gemini_commentary import generate_gemini_paragraph
from src.utils import get_risk_definition_by_fixed_intervals


@dataclass(frozen=True)
class DocxPageConfig:
	left_margin_cm: float = 2.0
	right_margin_cm: float = 2.0
	top_margin_cm: float = 3.0
	bottom_margin_cm: float = 2.0
	page_width_cm: float = 21.0
	page_height_cm: float = 29.7


def _resolve_logo_path(filename: str) -> str:
	base = os.path.dirname(os.path.abspath(__file__))
	return os.path.join(base, "..", "..", "logo", filename)


def _set_document_page_setup(doc: Document, cfg: DocxPageConfig) -> None:
	section = doc.sections[0]
	section.orientation = WD_ORIENT.PORTRAIT
	section.page_width = Cm(cfg.page_width_cm)
	section.page_height = Cm(cfg.page_height_cm)
	section.left_margin = Cm(cfg.left_margin_cm)
	section.right_margin = Cm(cfg.right_margin_cm)
	section.top_margin = Cm(cfg.top_margin_cm)
	section.bottom_margin = Cm(cfg.bottom_margin_cm)


def _content_width_inches(doc: Document) -> float:
	section = doc.sections[0]
	return section.page_width.inches - section.left_margin.inches - section.right_margin.inches


def _add_header_logo(doc: Document) -> None:
	logo_path = _resolve_logo_path("iklim.png")
	if not os.path.isfile(logo_path):
		return
	section = doc.sections[0]
	header = section.header
	if header.is_linked_to_previous:
		header.is_linked_to_previous = False
	paragraph = header.paragraphs[0] if header.paragraphs else header.add_paragraph()
	paragraph.text = ""
	run = paragraph.add_run()
	run.add_picture(logo_path, width=Inches(1.2))
	paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT


def _add_footer_page_number_left(doc: Document) -> None:
	section = doc.sections[0]
	section.different_first_page_header_footer = True

	footer = section.footer
	if footer.is_linked_to_previous:
		footer.is_linked_to_previous = False

	first_footer = section.first_page_footer
	if first_footer.is_linked_to_previous:
		first_footer.is_linked_to_previous = False
	for p in first_footer.paragraphs:
		p.text = ""

	p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
	p.alignment = WD_ALIGN_PARAGRAPH.LEFT
	p.text = ""
	run = p.add_run()

	fld_begin = OxmlElement("w:fldChar")
	fld_begin.set(qn("w:fldCharType"), "begin")

	instr = OxmlElement("w:instrText")
	instr.set(qn("xml:space"), "preserve")
	instr.text = " PAGE "

	fld_sep = OxmlElement("w:fldChar")
	fld_sep.set(qn("w:fldCharType"), "separate")

	fld_end = OxmlElement("w:fldChar")
	fld_end.set(qn("w:fldCharType"), "end")

	run._r.append(fld_begin)
	run._r.append(instr)
	run._r.append(fld_sep)
	run._r.append(fld_end)


def _add_cover_logo_centered(doc: Document) -> None:
	logo_path = _resolve_logo_path("iklim.png")
	if not os.path.isfile(logo_path):
		return
	p = doc.add_paragraph()
	p.alignment = WD_ALIGN_PARAGRAPH.CENTER
	p.paragraph_format.space_before = Pt(150)
	run = p.add_run()
	run.add_picture(logo_path, width=Inches(3.2))


def _add_title(doc: Document, text: str, size_pt: int = 20, bold: bool = True, align: WD_ALIGN_PARAGRAPH = WD_ALIGN_PARAGRAPH.LEFT) -> None:
	p = doc.add_paragraph()
	p.alignment = align
	r = p.add_run(text)
	r.bold = bold
	r.font.size = Pt(size_pt)


def _add_paragraph(doc: Document, text: str, size_pt: int = 11, align: WD_ALIGN_PARAGRAPH = WD_ALIGN_PARAGRAPH.LEFT, bold: bool = False) -> None:
	p = doc.add_paragraph()
	p.alignment = align
	r = p.add_run(text)
	r.bold = bold
	r.font.size = Pt(size_pt)


def _add_bullets(doc: Document, items: Iterable[str], size_pt: int = 10) -> None:
	for item in items:
		p = doc.add_paragraph(style="List Bullet")
		r = p.add_run(str(item))
		r.font.size = Pt(size_pt)


def _lighten_hex_color(hex_color: str, factor: float = 0.6) -> str:
	s = hex_color.strip()
	if s.startswith("#"):
		s = s[1:]
	if len(s) != 6:
		return hex_color
	try:
		r = int(s[0:2], 16)
		g = int(s[2:4], 16)
		b = int(s[4:6], 16)
	except ValueError:
		return hex_color
	f = max(0.0, min(1.0, factor))
	r_l = int(r + (255 - r) * f)
	g_l = int(g + (255 - g) * f)
	b_l = int(b + (255 - b) * f)
	return f"{r_l:02X}{g_l:02X}{b_l:02X}"


def _shade_paragraph(p, hex_color: str) -> None:
	hex_color = _lighten_hex_color(hex_color, factor=0.6)
	p_pr = p._p.get_or_add_pPr()
	shd = OxmlElement("w:shd")
	shd.set(qn("w:val"), "clear")
	shd.set(qn("w:color"), "auto")
	shd.set(qn("w:fill"), hex_color)
	p_pr.append(shd)


def _add_ring_bullet(doc: Document, ring_label: str, ring_color: str | None, size_pt: int = 10) -> None:
	p = doc.add_paragraph(style="List Bullet")
	if ring_color:
		hex_color = _color_to_hex(ring_color)
		if hex_color:
			_shade_paragraph(p, hex_color)
	r = p.add_run(str(ring_label))
	r.font.size = Pt(size_pt)


def _ring_color_for_label(label: str) -> str | None:
	s = str(label)
	if ":" in s:
		s = s.split(":", 1)[0]
	s = s.strip().lower().replace("km", "")
	if "-" not in s:
		return None
	parts = s.split("-", 1)
	try:
		upper_km = float(parts[1])
	except Exception:
		return None
	rings_km = [float(r) / 1000.0 for r in (getattr(config, "distance_rings", None) or [])]
	for idx, rk in enumerate(rings_km):
		if abs(rk - upper_km) < 1e-6:
			colors = getattr(config, "ring_colors", None) or []
			return colors[idx] if idx < len(colors) else None
	return None


def _add_risk_color_legend(doc: Document, size_pt: int = 10) -> None:
	from src.utils import get_risk_definition_by_fixed_intervals, get_turbine_color_by_fixed_intervals

	legend_items: list[tuple[str, str]] = [
		("Very Low Risk (< 0.1)", get_turbine_color_by_fixed_intervals(0.05)),
		("Low Risk (0.1 - 0.2)", get_turbine_color_by_fixed_intervals(0.15)),
		("Med-Low Risk (0.2 - 0.4)", get_turbine_color_by_fixed_intervals(0.30)),
		("Medium Risk (0.4 - 0.6)", get_turbine_color_by_fixed_intervals(0.50)),
		("Med-High Risk (0.6 - 0.8)", get_turbine_color_by_fixed_intervals(0.70)),
		("High Risk (0.8 - 1.0)", get_turbine_color_by_fixed_intervals(0.90)),
		("Very High Risk (1.0 - 1.2)", get_turbine_color_by_fixed_intervals(1.10)),
		("Critical Risk (1.2 - 1.4)", get_turbine_color_by_fixed_intervals(1.30)),
		("Maximum Risk (≥ 1.4)", get_turbine_color_by_fixed_intervals(1.50)),
	]

	_add_paragraph(doc, "Risk Color Legend (Log Risk):", size_pt=size_pt, bold=True)
	for label, color in legend_items:
		p = doc.add_paragraph()
		c = str(color).strip()
		if c.startswith("#"):
			c = c[1:]
		if len(c) == 6:
			marker = p.add_run("■ ")
			marker.font.size = Pt(size_pt)
			marker.font.color.rgb = RGBColor.from_string(c.upper())
		r = p.add_run(label)
		r.font.size = Pt(size_pt)


def _add_image_from_bytes(doc: Document, png_bytes: bytes, width_inches: float) -> None:
	stream = io.BytesIO(png_bytes)
	doc.add_picture(stream, width=Inches(width_inches))


def _render_math_to_png_bytes(latex: str, font_size: int = 18, dpi: int = 300) -> bytes:
	fig = plt.figure(figsize=(0.01, 0.01), dpi=dpi)
	fig.patch.set_alpha(0.0)
	ax = fig.add_axes([0, 0, 1, 1])
	ax.axis("off")
	text = ax.text(
		0,
		0,
		f"${latex}$",
		fontsize=font_size,
		ha="left",
		va="bottom",
	)
	fig.canvas.draw()
	bbox = text.get_window_extent(renderer=fig.canvas.get_renderer()).expanded(1.05, 1.25)
	width_in = bbox.width / dpi
	height_in = bbox.height / dpi
	fig.set_size_inches(width_in, height_in)
	ax.set_position([0, 0, 1, 1])
	buf = io.BytesIO()
	fig.savefig(buf, format="png", dpi=dpi, transparent=True, bbox_inches="tight", pad_inches=0.02)
	plt.close(fig)
	return buf.getvalue()


def _add_math_formula(
	doc: Document,
	latex: str,
	width_inches: float,
	align: WD_ALIGN_PARAGRAPH = WD_ALIGN_PARAGRAPH.LEFT,
	font_size: int = 18,
) -> None:
	png = _render_math_to_png_bytes(latex, font_size=font_size)
	p = doc.add_paragraph()
	p.alignment = align
	run = p.add_run()
	run.add_picture(io.BytesIO(png), width=Inches(width_inches))


_NAMED_COLOR_HEX: dict[str, str] = {
	"white": "FFFFFF",
	"lightgrey": "D9D9D9",
	"lightgray": "D9D9D9",
	"grey": "BFBFBF",
	"gray": "BFBFBF",
	"purple": "800080",
	"red": "FF0000",
	"orange": "FFA500",
	"coral": "FF7F50",
	"green": "008000",
	"blue": "0000FF",
	"pink": "FFC0CB",
	"yellow": "FFFF00",
	"lightgreen": "90EE90",
	"lightblue": "ADD8E6",
}


def _color_to_hex(color: str | None) -> str | None:
	if not color:
		return None
	s = str(color).strip()
	if not s:
		return None
	if s.startswith("#") and len(s) == 7:
		return s[1:].upper()
	if s.startswith("rgb"):
		try:
			inside = s[s.find("(") + 1 : s.find(")")]
			parts = [p.strip() for p in inside.split(",")]
			if len(parts) >= 3:
				r, g, b = (max(0, min(255, int(float(parts[0])))),
						   max(0, min(255, int(float(parts[1])))),
						   max(0, min(255, int(float(parts[2])))))
				return f"{r:02X}{g:02X}{b:02X}"
		except Exception:
			return None
	key = s.lower()
	return _NAMED_COLOR_HEX.get(key)


def _shade_cell(cell, hex_color: str) -> None:
	tc_pr = cell._tc.get_or_add_tcPr()
	shd = OxmlElement("w:shd")
	shd.set(qn("w:val"), "clear")
	shd.set(qn("w:color"), "auto")
	shd.set(qn("w:fill"), hex_color)
	tc_pr.append(shd)


def _fit_one_line_table_layout(
	table_data: list[list[str]],
	available_width_cm: float,
	min_col_widths_cm: list[float],
	max_font_pt: int = 10,
	min_font_pt: int = 6,
) -> tuple[int, list[float]]:
	cols = len(table_data[0]) if table_data else 0
	if cols == 0:
		return max_font_pt, []

	max_lens = [
		max((len(str(row[c])) for row in table_data if c < len(row)), default=0)
		for c in range(cols)
	]

	def chars_per_cm(font_pt: int) -> float:
		return 45.0 / float(font_pt)

	for font_pt in range(max_font_pt, min_font_pt - 1, -1):
		cpc = chars_per_cm(font_pt)
		required = [
			max(min_col_widths_cm[c], (max_lens[c] / cpc) * 1.05) for c in range(cols)
		]
		total_required = sum(required)
		if total_required <= available_width_cm:
			extra = available_width_cm - total_required
			weight_sum = float(sum(max_lens) or 1)
			widths = [
				required[c] + extra * (max_lens[c] / weight_sum) for c in range(cols)
			]
			return font_pt, widths

	font_pt = min_font_pt
	cpc = chars_per_cm(font_pt)
	required = [
		max(min_col_widths_cm[c], (max_lens[c] / cpc) * 1.05) for c in range(cols)
	]
	total_required = sum(required) or 1.0
	scale = available_width_cm / total_required
	return font_pt, [w * scale for w in required]


def _add_table(
	doc: Document,
	table_data: list[list[str]],
	row_colors: list[str] | None = None,
	header_bold: bool = True,
	column_widths_cm: list[float] | None = None,
	font_size_pt: float | None = None,
	autofit: bool | None = None,
) -> None:
	if not table_data:
		return
	rows = len(table_data)
	cols = len(table_data[0])
	t = doc.add_table(rows=rows, cols=cols)
	t.style = "Table Grid"
	if autofit is not None:
		t.autofit = bool(autofit)
	for r_idx, row in enumerate(table_data):
		for c_idx, val in enumerate(row):
			cell = t.cell(r_idx, c_idx)
			cell.text = str(val)
			if font_size_pt is not None:
				for p in cell.paragraphs:
					p.paragraph_format.space_after = Pt(0)
					p.paragraph_format.space_before = Pt(0)
					p.paragraph_format.line_spacing = 1.0
					for run in p.runs:
						run.font.size = Pt(font_size_pt)
			if r_idx == 0 and header_bold:
				for run in cell.paragraphs[0].runs:
					run.bold = True
			if row_colors and r_idx < len(row_colors):
				hex_color = _color_to_hex(row_colors[r_idx])
				if hex_color and hex_color != "FFFFFF":
					_shade_cell(cell, hex_color)
	if column_widths_cm and len(column_widths_cm) == cols:
		for c_idx, w in enumerate(column_widths_cm):
			for r_idx in range(rows):
				t.cell(r_idx, c_idx).width = Cm(w)


def create_docx_report(
	docx_path: str,
	turbine_df: pd.DataFrame,
	lightning_df: pd.DataFrame,
	storm_data_path: str | None = None,
	storm_data_records: list[dict[str, Any]] | dict[str, Any] | None = None,
) -> None:
	start_date = config.analysis_start_date
	end_date = config.analysis_end_date

	doc = Document()
	_set_document_page_setup(doc, DocxPageConfig())
	content_width = _content_width_inches(doc)
	_add_footer_page_number_left(doc)

	# Cover
	_add_cover_logo_centered(doc)
	_add_title(doc, "Lightning Activity Report for", size_pt=22, align=WD_ALIGN_PARAGRAPH.CENTER)
	_add_title(doc, f"{config.wind_farm_name or ''}", size_pt=22, align=WD_ALIGN_PARAGRAPH.CENTER)

	period_has_time = (start_date and " " in str(start_date) and ":" in str(start_date)) or (end_date and " " in str(end_date) and ":" in str(end_date))
	if period_has_time:
		utc_label = get_utc_offset_label(getattr(config, "timezone", None))
		period_label = f"Analyzed Period ({utc_label}):" if utc_label else "Analyzed Period (local time):"
	else:
		period_label = "Analyzed Period:"

	_add_paragraph(doc, period_label, size_pt=12, align=WD_ALIGN_PARAGRAPH.CENTER, bold=True)
	_add_paragraph(doc, f"{start_date or ''} - {end_date or ''}", size_pt=12, align=WD_ALIGN_PARAGRAPH.CENTER)
	centroid_lat = float(turbine_df["lat"].mean()) if len(turbine_df) > 0 else 0.0
	centroid_lng = float(turbine_df["lng"].mean()) if len(turbine_df) > 0 else 0.0
	_add_paragraph(doc, "Centroid Coordinates:", size_pt=12, align=WD_ALIGN_PARAGRAPH.CENTER, bold=True)
	_add_paragraph(doc, f"Latitude: {centroid_lat:.5f}", size_pt=12, align=WD_ALIGN_PARAGRAPH.CENTER)
	_add_paragraph(doc, f"Longitude: {centroid_lng:.5f}", size_pt=12, align=WD_ALIGN_PARAGRAPH.CENTER)
	_add_paragraph(doc, f"Report Generated: {format_datetime_ddmmyyyy_hhmm(now_in_timezone(getattr(config, 'timezone', None)))}", size_pt=11, align=WD_ALIGN_PARAGRAPH.CENTER)
	_add_paragraph(doc, "This report provides comprehensive lightning and storm activity analysis for wind farm", size_pt=10, align=WD_ALIGN_PARAGRAPH.CENTER)
	_add_paragraph(doc, "operations and safety assessment. Each section includes detailed explanations to help", size_pt=10, align=WD_ALIGN_PARAGRAPH.CENTER)
	_add_paragraph(doc, "you understand the data and make informed decisions about turbine safety", size_pt=10, align=WD_ALIGN_PARAGRAPH.CENTER)
	doc.add_page_break()

	_add_header_logo(doc)

	# Optional storm data load/filter (same behavior as PDF)
	storm_data: list[dict[str, Any]] = []
	if storm_data_records is not None:
		# `storm_records` may come back as:
		# - list[dict] (expected), or
		# - dict[id, list[dict]] (common for APIs)
		# If we naively do `list(storm_data_records)` we would get dict keys (strings),
		# which later breaks `.get(...)` access in storm filtering helpers.
		if isinstance(storm_data_records, list):
			storm_data = [s for s in storm_data_records if isinstance(s, dict)]
		else:
			normalized: list[dict[str, Any]] = []
			for value in storm_data_records.values():
				if isinstance(value, dict):
					normalized.append(value)
				elif isinstance(value, list):
					normalized.extend([item for item in value if isinstance(item, dict)])
			storm_data = normalized
	elif storm_data_path:
		storm_data = load_storm_data_from_json(storm_data_path)

	if storm_data:
		storm_data = filter_storm_data_by_date_range(storm_data, start_date, end_date)
		storm_data = filter_storm_data_by_turbine_proximity(storm_data, turbine_df)

	# Compute risks and groups
	turbine_df = calculate_turbine_risks(turbine_df, lightning_df)
	group_data = create_turbine_groups(turbine_df)
	stats = calculate_lightning_statistics(lightning_df, centroid_lat, centroid_lng, start_date, end_date)
	histogram_figs = create_lightning_histogram_pages(lightning_df, centroid_lat, centroid_lng)

	# Turbine information
	_add_title(doc, "Turbine Information", size_pt=16, align=WD_ALIGN_PARAGRAPH.LEFT)
	_add_paragraph(doc, "This table contains detailed information about all turbines in the wind farm.", size_pt=10)
	turb_rows: list[list[str]] = [[
		"Turbine", "Lat", "Lng", "Unit Power (MWm)", "Unit Power (MWe)", "Tower Height (m)", "Rotor Diameter (m)", "Altitude (m)",
	]]
	for _, t in turbine_df.iterrows():
		turb_rows.append([
			str(t.get("name", "N/A")),
			f"{t.get('lat', 0):.4f}",
			f"{t.get('lng', 0):.4f}",
			str(t.get("unit_power_mwm", "N/A")),
			str(t.get("unit_power_mwe", "N/A")),
			str(t.get("tower_height_m", "N/A")),
			str(t.get("turbine_rotor_blade_diameter", "N/A")),
			str(t.get("altitude", "N/A")),
		])
	_add_table(doc, turb_rows)

	# Gemini commentary (single API call per report run; falls back deterministically if Gemini is unavailable)
	analysis_radius_km = float(get_analysis_radius_m()) / 1000.0 if get_analysis_radius_m() > 0 else float(max(config.distance_rings) / 1000.0)
	analysis_period_str = f"{start_date or ''} - {end_date or ''}".strip(" -")

	ring_stats: dict[str, Any] = stats.get("lightning_by_distance_rings") or {}
	# Align the "outermost" ring used for commentary with the analysis radius used in stats.
	outermost_upper_km = analysis_radius_km

	def _ring_upper_km(label: str) -> float | None:
		s = str(label).strip()
		if ":" in s:
			s = s.split(":", 1)[0]
		s = s.lower().replace("km", "").strip()
		# Expected formats: "0-2.0" or "2.0-4.0"
		if "-" not in s:
			return None
		try:
			upper = float(s.split("-", 1)[1])
			return upper
		except Exception:
			return None

	rings = []
	for ring_name, ring_data in ring_stats.items():
		total = int(ring_data.get("total", 0))
		cg_count = int(ring_data.get("cloud_to_ground", 0))
		ic_count = int(ring_data.get("intercloud", 0))
		rings.append((ring_name, total, cg_count, ic_count))

	# Rank "top rings" by cloud-to-ground contribution (turbine risk score is CG-based).
	best_rings_desc = sorted(rings, key=lambda x: x[2], reverse=True)
	outermost_ring = None
	for ring in rings:
		upper_km = _ring_upper_km(ring[0])
		if upper_km is not None and abs(upper_km - outermost_upper_km) < 1e-6:
			outermost_ring = ring
			break
	if outermost_ring is None and rings:
		outermost_ring = max(rings, key=lambda r: _ring_upper_km(r[0]) or 0.0)

	top_rings: list[tuple[str, int, int, int]] = []
	for ring in best_rings_desc:
		if outermost_ring is not None and ring[0] == outermost_ring[0]:
			continue
		top_rings.append(ring)
		if len(top_rings) >= 2:
			break
	if outermost_ring is not None:
		top_rings.append(outermost_ring)

	risk_log_max = None
	max_risk_definition = None
	top_turbine_name: str | None = None
	top_turbine_risk_log: float | None = None
	turbine_risk_counts: dict[str, int] = {}
	if "risk_log" in turbine_df.columns and len(turbine_df) > 0:
		risk_log_max = float(turbine_df["risk_log"].max())
		top_idx = turbine_df["risk_log"].idxmax()
		top_turbine = turbine_df.loc[top_idx]
		top_turbine_name = str(top_turbine.get("name", "N/A"))
		top_turbine_risk_log = float(top_turbine.get("risk_log", risk_log_max))
		max_risk_definition = get_risk_definition_by_fixed_intervals(risk_log_max)
		for rl in turbine_df["risk_log"].tolist():
			defn = get_risk_definition_by_fixed_intervals(float(rl))
			turbine_risk_counts[defn] = turbine_risk_counts.get(defn, 0) + 1

	storm_summary = create_storm_cells_summary(storm_data) if storm_data else None

	# Storm interaction with the highest-risk turbine: we approximate "over the turbine"
	# by checking whether the storm cell centroid is within a small distance threshold.
	# This provides a robust summary even without polygon point-in-polygon tests.
	turbine_count = len(turbine_df)
	is_single_turbine_report = turbine_count == 1
	storm_over_turbine = None
	storm_near_turbine_count = 0
	storm_closest_distance_km: float | None = None
	storm_over_threshold_km = 1.0

	if storm_data and top_turbine_name is not None and "risk_log" in turbine_df.columns and len(turbine_df) > 0:
		# Use the coordinates of the highest-risk turbine for storm-distance calculations.
		if "lat" in turbine_df.columns and "lng" in turbine_df.columns and top_turbine_name is not None:
			top_row = turbine_df.loc[turbine_df["name"] == top_turbine_name].head(1) if "name" in turbine_df.columns else None
			if top_row is not None and len(top_row) > 0:
				top_lat = float(top_row.iloc[0]["lat"])
				top_lng = float(top_row.iloc[0]["lng"])
				min_dist = float("inf")
				near_count = 0
				for storm in storm_data:
					wkt_string = storm.get("cell_polygon_wkt", "") or ""
					if not wkt_string:
						continue
					centroid = calculate_storm_cell_centroid(wkt_string)
					if centroid is None:
						continue
					c_lat, c_lng = centroid
					dist_km = haversine_distance(top_lat, top_lng, c_lat, c_lng) / 1000.0
					if dist_km < min_dist:
						min_dist = dist_km
					if dist_km <= storm_over_threshold_km:
						near_count += 1
				if min_dist != float("inf"):
					storm_closest_distance_km = min_dist
					storm_near_turbine_count = near_count
					storm_over_turbine = near_count > 0

	commentary_context: dict[str, Any] = {
		"analysis_period": analysis_period_str,
		"analysis_radius_km": round(float(analysis_radius_km), 1) if analysis_radius_km is not None else None,
		"total_events": int(stats.get("total_events", 0) or 0),
		"total_lightning_per_km2": round(float(stats.get("total_lightning_per_km2", 0) or 0), 3),
		"top_rings": top_rings,
		"max_risk_log": round(float(risk_log_max), 3) if risk_log_max is not None else None,
		"max_risk_definition": max_risk_definition,
		"top_turbine_name": top_turbine_name,
		"top_turbine_risk_log": round(float(top_turbine_risk_log), 3) if top_turbine_risk_log is not None else None,
		"turbine_count": turbine_count,
		"is_single_turbine_report": is_single_turbine_report,
		"storm_over_turbine": storm_over_turbine,
		"storm_near_turbine_count": storm_near_turbine_count,
		"storm_closest_distance_km": round(float(storm_closest_distance_km), 1) if storm_closest_distance_km is not None else None,
		"storm_over_threshold_km": storm_over_threshold_km,
		"turbine_risk_counts": turbine_risk_counts,
		"storm_summary": storm_summary,
	}

	commentary_text = generate_gemini_paragraph(commentary_context)
	commentary_text = commentary_text.strip()
	# Defensive cleanup: sometimes models may prepend a heading/label.
	for prefix in ("Gemini Commentary:", "Gemini commentary:", "Commentary:", "Commentary"):
		if commentary_text.startswith(prefix):
			commentary_text = commentary_text[len(prefix):].strip()
			break
	# Keep the commentary close to the Turbine Information table (same page if possible).
	doc.add_paragraph("")
	doc.add_paragraph("")
	_add_title(doc, "Report Summary", size_pt=14, bold=True, align=WD_ALIGN_PARAGRAPH.LEFT)
	# DOCX line spacing for the Gemini commentary paragraph.
	p = doc.add_paragraph()
	p.alignment = WD_ALIGN_PARAGRAPH.LEFT
	r = p.add_run(commentary_text)
	r.font.size = Pt(10)
	p.paragraph_format.line_spacing = 1.5

	doc.add_page_break()

	# Group maps + charts + risk tables
	for group_info in group_data["groups"]:
		centroid_row = pd.Series({
			"lat": group_info["centroid_lat"],
			"lng": group_info["centroid_lng"],
			"name": f"Group {group_info['group_id'] + 1}",
		})
		pre = precompute_group_distances_and_rings(centroid_row["lat"], centroid_row["lng"], lightning_df, config.distance_rings)

		_add_title(doc, "Cloud-to-Ground Lightnings", size_pt=14)
		if start_date and end_date:
			_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
		cg_fig = plot_cloud_to_ground_coordinate_plane(centroid_row, lightning_df, turbine_df, storm_data, precomputed=pre)
		_add_image_from_bytes(doc, cg_fig.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width)
		doc.add_page_break()

		_add_title(doc, "Cloud-to-Ground — Current vs Distance", size_pt=14)
		if start_date and end_date:
			_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
		cg_chart = _build_current_vs_distance_chart(
			lightning_df,
			pre["dists_km"],
			pre["mask_within"],
			"cg",
			"Cloud-to-Ground Lightning — Current vs Distance from Centroid",
			900,
			650,
		)
		if cg_chart is not None:
			_add_image_from_bytes(doc, cg_chart.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width)
		doc.add_page_break()

		_add_title(doc, "Intercloud Lightnings", size_pt=14)
		if start_date and end_date:
			_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
		ic_fig = plot_intercloud_coordinate_plane(centroid_row, lightning_df, turbine_df, storm_data, precomputed=pre)
		_add_image_from_bytes(doc, ic_fig.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width)
		doc.add_page_break()

		_add_title(doc, "Intercloud — Current vs Distance", size_pt=14)
		if start_date and end_date:
			_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
		ic_chart = _build_current_vs_distance_chart(
			lightning_df,
			pre["dists_km"],
			pre["mask_within"],
			"ic",
			"Intercloud Lightning — Current vs Distance from Centroid",
			900,
			650,
		)
		if ic_chart is not None:
			_add_image_from_bytes(doc, ic_chart.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width)
		doc.add_page_break()

		risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, group_info)
		if risk_table_data and len(risk_table_data) > 1:
			_add_title(doc, "Turbine Risk Assessment", size_pt=14)
			_add_table(doc, risk_table_data, row_colors=risk_row_colors)
			doc.add_page_break()

	# Daily breakdown
	_add_title(doc, "Lightning Breakdown by Distance Rings", size_pt=14)
	if start_date and end_date:
		_add_paragraph(doc, f"Period: {start_date} - {end_date} (Total: {stats.get('total_events', 0)} lightning events)", size_pt=10, bold=True)
	closest_km = (config.distance_rings[0] / 1000.0) if getattr(config, "distance_rings", None) else 1.0

	analysis_radius_km = get_analysis_radius_m() / 1000.0 if get_analysis_radius_m() > 0 else closest_km
	_add_paragraph(doc, f"Area covered within {analysis_radius_km:.1f} km radius: {stats.get('area_km2', 0):.1f} km²", size_pt=10)
	_add_paragraph(doc, f"Total lightnings within {analysis_radius_km:.1f} km radius: {stats.get('total_events', 0)} events", size_pt=10)
	_add_paragraph(doc, f"Total lightning density: {stats.get('total_lightning_per_km2', 0):.3f} events/km²", size_pt=10)
	_add_paragraph(
		doc,
		f"(Calculation: {stats.get('total_events', 0)} total lightnings / {stats.get('area_km2', 0):.1f} km² area)",
		size_pt=9,
	)

	period_days = float(stats.get("period_days", 0.0) or 0.0)
	if period_days >= 1.0:
		_add_paragraph(doc, f"Daily equivalent density: {stats.get('daily_lightning_per_km2', 0):.3f} events/km²/day", size_pt=10)
		if period_days == 1.0:
			days_label = "1 day in the period"
		elif period_days != int(period_days):
			days_label = f"{period_days:.1f} days in the period"
		else:
			days_label = f"{int(period_days)} days in the period"
		_add_paragraph(
			doc,
			f"(Calculation: {stats.get('total_events', 0)} total lightnings / {stats.get('area_km2', 0):.1f} km² area / {days_label})",
			size_pt=9,
		)

	_add_paragraph(doc, "This section shows lightning events over the analyzed period by distance from turbines.", size_pt=10)
	_add_paragraph(doc, f"Higher counts in closer rings (0-{closest_km:.1f}km) indicate elevated risk to turbine operations.", size_pt=10)
	ring_items: list[str] = []
	for ring_name, ring_data in sorted((stats.get("lightning_by_distance_rings") or {}).items()):
		if ring_data.get("total", 0) > 0:
			ring_items.append(f"{ring_name}: {ring_data['total']} total ({ring_data['cloud_to_ground']} cloud-to-ground, {ring_data['intercloud']} intercloud)")
	if ring_items:
		for item in ring_items:
			_add_ring_bullet(doc, item, _ring_color_for_label(item), size_pt=10)
	doc.add_page_break()

	# Histograms
	for idx, fig in enumerate(histogram_figs):
		if idx == 0:
			_add_title(doc, "Frequent Lightning Activity Report", size_pt=14)
			if start_date and end_date:
				_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10, bold=True)
			analysis_radius_km = get_analysis_radius_m() / 1000.0 if get_analysis_radius_m() > 0 else closest_km
			_add_paragraph(doc, "Lightning Activity Overview:", size_pt=11, bold=True)
			_add_paragraph(
				doc,
				f"The chart below shows lightning activity patterns over time within {analysis_radius_km:.1f} km radius, helping identify high-risk periods and concentrated storm events. Peaks indicate intense lightning activity requiring attention.",
				size_pt=10,
			)
			_add_paragraph(
				doc,
				'For detailed information go to the "Frequent Lightning Activity Period Detection Algorithm" section in the appendix.',
				size_pt=10,
			)
		else:
			_add_title(doc, "Frequent Lightning Activity Periods", size_pt=14)
		_add_image_from_bytes(doc, fig.to_image(format="png", width=1400, height=900, scale=2, engine="kaleido"), content_width)
		doc.add_page_break()

	# Storm cells (summary per day)
	if storm_data:
		_add_title(doc, "Storm Cells Analysis Summary", size_pt=16)
		_add_paragraph(doc, f"{start_date or ''} - {end_date or ''}", size_pt=12, align=WD_ALIGN_PARAGRAPH.LEFT, bold=True)
		_add_paragraph(doc, "Storm Cells Overview:", size_pt=11, bold=True)
		_add_paragraph(
			doc,
			"The following pages show storm cell boundaries organized by day during the analysis period.",
			size_pt=10,
		)
		_add_paragraph(
			doc,
			"Each cell represents a storm system with defined boundaries and storm severity levels.",
			size_pt=10,
		)

		summary = create_storm_cells_summary(storm_data)
		_add_paragraph(doc, "Storm Cells Summary:", size_pt=11, bold=True)
		_add_paragraph(doc, f"Total storm cells: {summary.get('total_cells', 0)}", size_pt=10)

		severity_counts: dict[str, int] = summary.get("severity_counts", {}) or {}
		_add_paragraph(doc, "Severity breakdown:", size_pt=10, bold=True)
		_add_bullets(doc, [f"{severity}: {count} cells" for severity, count in severity_counts.items()], size_pt=10)

		_add_paragraph(doc, f"Average direction: {summary.get('avg_direction', 0):.1f}°", size_pt=10)
		_add_paragraph(doc, f"Average speed: {summary.get('avg_speed', 0):.1f} km/h", size_pt=10)

		daily_breakdown: dict[str, int] = summary.get("daily_breakdown", {}) or {}
		_add_paragraph(doc, "Daily Storm Breakdown:", size_pt=10, bold=True)
		sorted_days = sorted(daily_breakdown.keys())
		daily_items: list[str] = []
		for day in sorted_days[:10]:
			daily_items.append(f"{day}: {daily_breakdown.get(day, 0)} storm cells")
		if len(sorted_days) > 10:
			daily_items.append(f"... and {len(sorted_days) - 10} more days")
		if daily_items:
			_add_bullets(doc, daily_items, size_pt=10)

		_add_paragraph(doc, "Complete Storm Cells List:", size_pt=10, bold=True)

		def _storm_effective_time(storm: dict[str, Any]) -> datetime:
			raw = storm.get("effective_time") or storm.get("creation_time") or storm.get("expire_time") or ""
			try:
				# `format_datetime_to_local_display` can format, but for sorting we need a sortable key.
				dt = pd.to_datetime(str(raw), utc=True, errors="coerce")
				if pd.isna(dt):
					return datetime.min
				return dt.to_pydatetime()
			except Exception:
				return datetime.min

		sorted_storms = sorted(storm_data, key=_storm_effective_time)
		table_data: list[list[str]] = [["No.", "Severity", "Effective Time", "Expire Time"]]
		row_colors: list[str] = ["lightgrey"]
		for i, storm in enumerate(sorted_storms, start=1):
			report_tz = getattr(config, "timezone", None)
			effective_raw = storm.get("effective_time") or storm.get("creation_time") or ""
			expire_raw = storm.get("expire_time") or ""

			effective_formatted = format_datetime_to_local_display(effective_raw, report_tz)
			expire_formatted = format_datetime_to_local_display(expire_raw, report_tz)

			table_data.append(
				[
					str(i),
					str(storm.get("lightning_severity", "Unknown")),
					effective_formatted,
					expire_formatted,
				]
			)
			severity = str(storm.get("lightning_severity", "Unknown") or "Unknown").strip().lower()
			if severity == "high":
				row_colors.append("purple")
			elif severity == "medium":
				row_colors.append("orange")
			elif severity == "low":
				row_colors.append("green")
			else:
				row_colors.append("gray")

		# Keep the list compact: small font, fixed column widths
		_add_table(
			doc,
			table_data,
			row_colors=row_colors,
			font_size_pt=8,
			autofit=False,
			column_widths_cm=[1.0, 2.2, 3.6, 3.6],
		)

		doc.add_page_break()

		_add_title(doc, "Storm Cells", size_pt=14)
		_add_paragraph(doc, "Daily storm cells visualization.", size_pt=10)
		storm_fig = create_storm_cells_map(storm_data, turbine_df)
		_add_image_from_bytes(
			doc,
			storm_fig.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"),
			content_width,
		)
		doc.add_page_break()

	# Lightning event tables per group
	for group_info in group_data["groups"]:
		_add_title(doc, "Detailed Lightning Event Data", size_pt=14)
		table_data, row_colors = build_group_lightning_table_data(group_info["centroid_lat"], group_info["centroid_lng"], lightning_df)
		if table_data and table_data[0]:
			table_data[0] = [
				str(h)
				.replace("Time (Local)", "Time\u00A0(Local)")
				.replace("Current (amps)", "Current\u00A0(amps)")
				.replace("Height (m)", "Height\u00A0(m)")
				.replace("Lightning Type", "Lightning\u00A0Type")
				.replace("Proximity (km)", "Proximity\u00A0(km)")
				for h in table_data[0]
			]
		available_width_cm = float(content_width) * 2.54
		min_col_widths_cm = [1, 3.2, 1.6, 1.6, 1.9, 1.4, 2.0, 1.8]
		font_pt, col_widths_cm = _fit_one_line_table_layout(
			table_data=table_data,
			available_width_cm=available_width_cm,
			min_col_widths_cm=min_col_widths_cm,
			max_font_pt=15,
			min_font_pt=8,
		)
		_add_table(
			doc,
			table_data,
			row_colors=row_colors,
			column_widths_cm=col_widths_cm,
			font_size_pt=float(font_pt),
			autofit=False,
		)
		doc.add_page_break()

	# Appendix
	_add_title(doc, "Appendix", size_pt=16)
	# 1. Risk Calculation Method
	_add_title(doc, "1. Risk Calculation Method", size_pt=14)
	_add_paragraph(doc, "How Risk Scores Are Determined:", size_pt=11, bold=True)
	_add_paragraph(doc, "A turbine’s risk score represents its exposure to cloud-to-ground lightning during the analysis period.", size_pt=10)
	_add_paragraph(doc, "Each lightning strike contributes some risk based on distance to the turbine and the strike’s current magnitude, and these contributions are summed.", size_pt=10)
	_add_math_formula(
		doc,
		r"r(d, I) = P_{0} \times \left(1 + \mathrm{current\_weight}\,\frac{|I|}{10000}\right) \times e^{-\alpha\,d}",
		width_inches=min(content_width, 6.5),
	)
	_add_paragraph(doc, "P0 (base factor): baseline scaling applied to every strike’s contribution", size_pt=10)
	_add_paragraph(doc, "current_weight: controls how strongly current magnitude increases risk (larger = more weight on |I|)", size_pt=10)
	_add_paragraph(doc, "Distance (d): turbine-to-strike distance in kilometers", size_pt=10)
	_add_paragraph(doc, "Current (|I|): absolute current magnitude |I| in amperes", size_pt=10)
	_add_paragraph(doc, "α (distance decay factor): controls how risk decays with distance (smaller = slower decay)", size_pt=10)
	_add_bullets(
		doc,
		[
			"Included events: only cloud-to-ground lightning (p_type = 0)",
			"Per-strike contribution: increases with |I| and decreases exponentially with distance",
		],
		size_pt=10,
	)
	_add_paragraph(doc, "Turbine's risk score: sum of per-strike contributions for all included strikes (typically within the outermost distance ring)", size_pt=10)
	_add_math_formula(
		doc,
		r"\mathrm{turbine\_risk\_score}(t) = \sum_{j \in \mathcal{S}} r\!\left(d_{t,j}, I_{j}\right)\quad \mathcal{S}=\{j:\ p\_type_j=0,\ d_{t,j}\leq R\}",
		width_inches=min(content_width, 6.5),
		font_size=26,
	)
	_add_paragraph(doc, "For visualization and reporting, we use the log-transformed score", size_pt=10)
	_add_math_formula(
		doc,
		r"\mathrm{risk\_log}(t) = \log_{10}(\mathrm{risk\_score}(t) + 1)",
		width_inches=min(content_width, 6.5),
	)

	# 2. Risk Score Interpretation
	_add_title(doc, "2. Risk Score Interpretation", size_pt=14)
	_add_paragraph(doc, "Understanding Risk Score Values:", size_pt=11, bold=True)
	_add_bullets(
		doc,
		[
			"Minimum Risk Score: ~0.001 (far distance, low current)",
			"Maximum Risk Score: ~2.500 (close distance, high current)",
			"Typical Range: 0.010 - 1.000 for most lightning events",
		],
		size_pt=10,
	)
	_add_paragraph(doc, "Risk Score Categories (Fixed Color Intervals):", size_pt=11, bold=True)
	_add_bullets(
		doc,
		[
			"Very Low Risk (<0.1): Blue - Distant lightning with low current",
			"Low Risk (0.1-0.2): Teal - Moderate distance lightning",
			"Med-Low Risk (0.2-0.4): Green - Closer lightning",
			"Medium Risk (0.4-0.6): Yellow - Moderate risk lightning",
			"Med-High Risk (0.6-0.8): Orange - High risk lightning",
			"High Risk (0.8-1.0): Dark Orange - Very high risk lightning",
			"Very High Risk (1.0-1.2): Red - Extreme risk lightning",
			"Critical Risk (>1.2): Dark Red - Critical risk lightning",
		],
		size_pt=10,
	)

	# 3. Risk Score Calculation Chart
	_add_title(doc, "3. Risk Score Calculation Chart", size_pt=14)
	_add_paragraph(doc, "Chart Reference Guide:", size_pt=11, bold=True)
	_add_paragraph(doc, "The following chart shows how distance and current magnitude affect risk scores.", size_pt=10)
	_add_paragraph(doc, "Use this chart to interpret the risk scores in the main report.", size_pt=10)
	_add_bullets(doc, ["Red areas = High risk (close distance, high current)", "Yellow/Orange areas = Medium risk", "Blue/Green areas = Low risk (far distance, low current)"], size_pt=10)
	from src.visualization.maps import create_risk_score_heatmap
	heatmap_fig = create_risk_score_heatmap()
	_add_image_from_bytes(doc, heatmap_fig.to_image(format="png", width=1400, height=900, scale=2, engine="kaleido"), content_width)

	# 4. Centroid and Distance Ring Calculation
	_add_title(doc, "4. Centroid and Distance Ring Calculation", size_pt=14)
	_add_bullets(
		doc,
		[
			"Turbines are grouped by proximity (within 4km of each other)",
			"Group centroid = Average latitude and longitude of all turbines in the group",
			"Distance rings are drawn from the group centroid, not individual turbines",
			"Single isolated turbines get their own centroid (individual analysis)",
			"Distance calculations use Haversine formula for accurate geographic distances",
		],
		size_pt=10,
	)

	# 5. Turbine Grouping Method
	_add_title(doc, "5. Turbine Grouping Method", size_pt=14)
	_add_paragraph(doc, "How Turbines Are Grouped for Analysis:", size_pt=11, bold=True)
	_add_paragraph(doc, "The system uses an intelligent clustering algorithm to group turbines based on geographic proximity.", size_pt=10)
	_add_paragraph(doc, "Grouping Algorithm Process:", size_pt=11, bold=True)
	from src.utils import get_grouping_radius_m
	grouping_radius_m = get_grouping_radius_m()
	grouping_radius_km = grouping_radius_m / 1000.0
	_add_bullets(
		doc,
		[
			"Proximity Analysis: All turbines are analyzed for pairwise distances using Haversine formula",
			"Clustering Method: Uses DBSCAN (Density-Based Spatial Clustering) algorithm when available",
			f"Dynamic Distance Threshold: Configurable grouping radius: {grouping_radius_km:.1f}km ({grouping_radius_m}m)",
			"Fallback Algorithm: If DBSCAN unavailable, uses iterative proximity grouping",
			"Group Formation: Each group contains turbines that are mutually within the distance threshold",
		],
		size_pt=10,
	)
	_add_paragraph(doc, "Group Types and Analysis:", size_pt=11, bold=True)
	_add_bullets(
		doc,
		[
			"Multi-Turbine Groups: Multiple turbines within proximity range - analyzed as a cluster",
			"Single Turbine Groups: Isolated turbines beyond grouping threshold - individual analysis",
			"Centroid Calculation: Each group's center point calculated as mean of member coordinates",
			"Distance Ring Origin: All proximity calculations use group centroid as reference point",
		],
		size_pt=10,
	)
	_add_paragraph(doc, "Benefits of Grouping Approach:", size_pt=11, bold=True)
	_add_bullets(
		doc,
		[
			"Efficient Analysis: Reduces computational complexity for large wind farms",
			"Logical Clustering: Groups turbines that experience similar weather conditions",
			"Risk Assessment: Provides both group-level and individual turbine risk analysis",
			"Scalability: Algorithm adapts to wind farms of any size and turbine distribution",
		],
		size_pt=10,
	)

	# 6. Frequent Lightning Activity Period Detection Algorithm
	_add_title(doc, "6. Frequent Lightning Activity Period Detection Algorithm", size_pt=14)
	_add_paragraph(doc, "How Period Timespans Are Determined:", size_pt=11, bold=True)
	_add_paragraph(doc, "The algorithm uses a gap-based approach to identify concentrated lightning activity periods.", size_pt=10)
	_add_paragraph(doc, "Step-by-Step Process:", size_pt=11, bold=True)
	_add_bullets(
		doc,
		[
			"Chronological Sorting: All lightning events are sorted by timestamp (local_time)",
			"Gap Calculation: Time differences between consecutive lightning events are calculated",
			f"Period Boundary Detection: When a gap between two consecutive events exceeds {config.histogram_params['min_gap_minutes']} minutes, it marks the end of one period and the start of another",
			f"Period Validation: Only periods with ≥{config.histogram_params['min_events_per_period']} lightning events are considered significant",
			"Timespan Definition: Start time = first event; End time = last event; Duration is actual time from first to last event",
			"Peak Sub-Period Detection: 3-minute rolling average; peaks when activity exceeds mean + 1 standard deviation; highlighted in yellow on histogram",
		],
		size_pt=10,
	)

	# 7. EarthNetworks Total Lightning Network (ENTLN)
	_add_title(doc, "7. EarthNetworks Total Lightning Network (ENTLN)", size_pt=14)
	_entln_blocks = [
		"The lightning data presented in this report is sourced directly from the EarthNetworks Total Lightning Network (ENTLN), which monitors both in-cloud and cloud-to-ground strikes. ENTLN is the first in-cloud lightning and cloud-to-ground detection network deployed on a global basis. It includes more than 1,500 sensors (the world's largest) that incorporate advanced systems and technology to provide unmatched in-cloud and cloud-to-ground lightning detection – total lightning detection.",
		"A variety of government agencies, such as the National Weather Service, the Air Force Weather Agency, and organizations that include public safety and emergency response agencies, airports, and utilities rely on information from the ENTLN to make informed decisions.",
		"Its unique capabilities detect long-range in-cloud lightning at high efficiencies, which are critical for the advanced prediction of severe weather such as:",
	]
	for blk in _entln_blocks:
		_add_paragraph(doc, blk, size_pt=10)
	_add_bullets(
		doc,
		[
			"Tornadoes and cyclones",
			"Heavy rainfall and monsoons",
			"Downburst and wind shear",
			"Cloud-to-Ground lightning strikes",
		],
		size_pt=10,
	)
	_add_paragraph(doc, "These potentially deadly weather events often occur within 5 to 30 minutes of in-cloud flash initiation. ENTLN has demonstrated the ability to significantly improve severe weather warning times over radar and other technologies by incorporating predictive capabilities that are crucial for characterizing severe storm precursors, improving severe storm warning lead times, and supporting comprehensive weather management planning.", size_pt=10)
	_add_paragraph(doc, "The total lightning network has expanded to include more than 1,500 sensors in more than 40 countries around the world, including North and South America, the Caribbean, Europe, Australia, Africa and Asia. This dense sensor deployment enables high-efficiency capture of total lightning activity across these regions.", size_pt=10)

	earth_logo = _resolve_logo_path("earth_networks.jpg")
	if os.path.isfile(earth_logo):
		p = doc.add_paragraph()
		p.alignment = WD_ALIGN_PARAGRAPH.CENTER
		run = p.add_run()
		run.add_picture(earth_logo, width=Inches(2.2))

	doc.save(docx_path)

