"""
PreProcessor file writers for IWFM models.

This module provides writers for all preprocessor input files,
enabling complete round-trip read/write of IWFM models.
"""

from __future__ import annotations

import logging
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

import numpy as np
from numpy.typing import NDArray

from pyiwfm.io.config import PreProcessorFileConfig
from pyiwfm.io.writer_base import TemplateWriter
from pyiwfm.templates.engine import TemplateEngine

if TYPE_CHECKING:
    from pyiwfm.core.model import IWFMModel

logger = logging.getLogger(__name__)


class PreProcessorWriter(TemplateWriter):
    """
    Writer for IWFM PreProcessor input files.

    Writes complete preprocessor file sets including:
    - Main control file
    - Node coordinates
    - Element configuration
    - Stratigraphy data
    - Stream configuration (optional)
    - Lake configuration (optional)

    Example
    -------
    >>> from pyiwfm.core.model import IWFMModel
    >>> from pyiwfm.io.preprocessor_writer import PreProcessorWriter
    >>> from pyiwfm.io.config import PreProcessorFileConfig
    >>>
    >>> # Load a model
    >>> model = IWFMModel.from_preprocessor("model/Preprocessor.in")
    >>>
    >>> # Write to new location
    >>> config = PreProcessorFileConfig(output_dir="output/Preprocessor")
    >>> writer = PreProcessorWriter(model, config)
    >>> writer.write_all()
    """

    def __init__(
        self,
        model: IWFMModel,
        config: PreProcessorFileConfig,
        template_engine: TemplateEngine | None = None,
    ) -> None:
        """
        Initialize the preprocessor writer.

        Parameters
        ----------
        model : IWFMModel
            Model to write
        config : PreProcessorFileConfig
            Output file configuration
        template_engine : TemplateEngine, optional
            Custom template engine
        """
        super().__init__(config.output_dir, template_engine)
        self.model = model
        self.config = config

    @property
    def format(self) -> str:
        return "iwfm_preprocessor"

    def write(self, data: Any = None) -> None:
        """Write all preprocessor files."""
        self.write_all()

    def write_all(self) -> dict[str, Path]:
        """
        Write all preprocessor files.

        Returns
        -------
        dict[str, Path]
            Mapping of file type to output path
        """
        logger.info(f"Writing preprocessor files to {self.config.output_dir}")

        # Ensure output directory exists
        self.config.output_dir.mkdir(parents=True, exist_ok=True)

        results = {}

        # Write component files first (they're referenced by main file)
        results["nodes"] = self.write_nodes()
        results["elements"] = self.write_elements()
        results["stratigraphy"] = self.write_stratigraphy()

        # Write optional component files
        if self.model.has_streams:
            results["stream_config"] = self.write_stream_config()
        if self.model.has_lakes:
            results["lake_config"] = self.write_lake_config()

        # Write main file last (references other files)
        results["main"] = self.write_main()

        logger.info(f"Wrote {len(results)} preprocessor files")
        return results

    def write_main(self) -> Path:
        """
        Write the main preprocessor control file.

        Returns
        -------
        Path
            Path to written file
        """
        output_path = self.config.main_path
        self._ensure_dir(output_path)

        # Prepare context for template
        context = {
            "generation_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "output_file": self._get_output_binary_path(),
            "description_line1": getattr(self.model, "name", "IWFM Model"),
            "description_line2": "Generated by pyiwfm",
            "description_line3": "",
            "node_file": self.config.node_file,
            "element_file": self.config.element_file,
            "stratigraphy_file": self.config.stratigraphy_file,
            "stream_config_file": self.config.stream_config_file if self.model.has_streams else "",
            "lake_config_file": self.config.lake_config_file if self.model.has_lakes else "",
            # Output specifications
            "kout": 1,
            "kdeb": 2,
            # Unit conversions
            "length_factor": 1.0,
            "length_unit": "FEET",
            "area_factor": 0.000022957,
            "area_unit": "ACRES",
        }

        # Render template - use inline template (more reliable)
        content = self._render_main_inline(context)

        output_path.write_text(content)
        logger.info(f"Wrote main file: {output_path}")
        return output_path

    def _get_output_binary_path(self) -> str:
        """Get the path for preprocessor binary output."""
        return self.config.binary_output_file

    def _render_main_inline(self, context: dict) -> str:
        """Render main file using inline template matching IWFM format."""
        template = """C*******************************************************************************
C
C                  INTEGRATED WATER FLOW MODEL (IWFM)
C
C*******************************************************************************
C
C                            MAIN INPUT FILE
C                        for IWFM Pre-Processing
C
C             Generated by pyiwfm
C             {{ generation_time }}
C
C*****************************************************************************
C                     Titles Printed in the Output
C
C   *A Maximum of 3 title lines can be printed.
C   *Do not use '*' , 'c' or 'C' in the first column.
C
C                          **********************
                                  {{ description_line1 }}
                              {{ description_line2 }}
                                   {{ description_line3 }}
C                          **********************
C*****************************************************************************
C                            File Description
C
C  *Listed below are all input and output file names used when running the
C   pre-processor for IWFM simulation.
C
C  *Each file name has a maximum length of 500 characters
C
C  *If a file does not exist for a project, leave the filename blank
C-----------------------------------------------------------------------------
C   FILE NAME                                      DESCRIPTION
C-----------------------------------------------------------------------------
    {{ output_file | pad_right(45) }} / 1: BINARY OUTPUT FOR SIMULATION (OUTPUT, REQUIRED)
    {{ element_file | pad_right(45) }} / 2: ELEMENT CONFIGURATION FILE (INPUT, REQUIRED)
    {{ node_file | pad_right(45) }} / 3: NODE X-Y COORDINATE FILE (INPUT, REQUIRED)
    {{ stratigraphy_file | pad_right(45) }} / 4: STRATIGRAPHIC DATA FILE (INPUT, REQUIRED)
    {{ stream_config_file | pad_right(45) }} / 5: STREAM GEOMETRIC DATA FILE (INPUT, OPTIONAL)
    {{ lake_config_file | pad_right(45) }} / 6: LAKE DATA FILE (INPUT, OPTIONAL)
C******************************************************************************
C                    Pre-Processor Output Specifications
C
C    KOUT; Enter 1 - Print geometric and stratigraphic information
C          Enter 0 - Otherwise
C
C    KDEB; Enter 2 - Print messages on the screen during program execution
C          Enter 1 - Print non-zero Finite Element Stiffness Matrix Components
C          Enter 0 - Otherwise
C-----------------------------------------------------------------------------
C  VALUE                        DESCRIPTION
C-----------------------------------------------------------------------------
     {{ kout | default(1) }}                          /KOUT
     {{ kdeb | default(2) }}                          /KDEB
C*****************************************************************************
C                  Unit Specifications of Pre-Processor Output
C
C    FACTLTOU; Factor to convert simulation unit of length to specified output unit of length
C    UNITLTOU; The output unit of length (maximum of 10 characters)
C    FACTAROU; Factor to convert simulation unit of area to specified output unit of area
C    UNITAROU; The output unit of area (maximum of 10 characters)
C
C-----------------------------------------------------------------------------
C   VALUE                       DESCRIPTION
C-----------------------------------------------------------------------------
    {{ length_factor | default(1.0) }}                         /FACTLTOU
    {{ length_unit | default('FEET') }}                        /UNITLTOU
    {{ area_factor | default(0.000022957) }}                 /FACTAROU
    {{ area_unit | default('ACRES') }}                       /UNITAROU
C*****************************************************************************
"""
        return self._engine.render_string(template, **context)

    def write_nodes(self) -> Path:
        """
        Write the node coordinates file.

        Returns
        -------
        Path
            Path to written file
        """
        output_path = self.config.node_path
        self._ensure_dir(output_path)

        grid = self.model.grid
        if grid is None:
            raise ValueError("Model has no grid; cannot write nodes file")
        n_nodes = self.model.n_nodes

        # Get sorted node IDs and coordinates
        sorted_node_ids = sorted(grid.nodes.keys())
        node_ids = np.array(sorted_node_ids, dtype=np.int32)
        x_coords = np.array([grid.nodes[nid].x for nid in sorted_node_ids], dtype=np.float64)
        y_coords = np.array([grid.nodes[nid].y for nid in sorted_node_ids], dtype=np.float64)

        # Write file
        with open(output_path, "w") as f:
            # Header
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write("C                      IWFM NODAL COORDINATES FILE\n")
            f.write("C\n")
            f.write("C                         Generated by pyiwfm\n")
            f.write(f"C                         {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write("C\n")
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write(f"{n_nodes}                                               / NNODES\n")
            f.write(f"{'1.0':>14}                                  / FACTXY\n")
            f.write("C\n")
            f.write("C   ID              X              Y\n")
            f.write(
                "C-----------------------------------------------------------------------------\n"
            )

            # Data - use numpy for efficiency
            data = np.column_stack([node_ids, x_coords, y_coords])
            np.savetxt(f, data, fmt="%6d %15.6f %15.6f")

        logger.info(f"Wrote nodes file: {output_path} ({n_nodes} nodes)")
        return output_path

    def write_elements(self) -> Path:
        """
        Write the element configuration file.

        Returns
        -------
        Path
            Path to written file
        """
        output_path = self.config.element_path
        self._ensure_dir(output_path)

        grid = self.model.grid
        if grid is None:
            raise ValueError("Model has no grid; cannot write elements file")
        n_elements = self.model.n_elements

        # Get subregions and their elements
        subregions = grid.subregions or {}

        # Get unique subregion IDs actually used by elements
        sorted_elem_ids = sorted(grid.elements.keys())
        unique_sr_ids = sorted(
            {getattr(grid.elements[eid], "subregion", 1) or 1 for eid in sorted_elem_ids}
        )
        n_subregions = len(unique_sr_ids)

        # Build subregion name list based on unique IDs
        if subregions:
            sr_names = [
                subregions.get(sr_id, type("obj", (object,), {"name": f"Region_{sr_id}"})()).name
                for sr_id in unique_sr_ids
            ]
        else:
            sr_names = [f"Region_{sr_id}" for sr_id in unique_sr_ids]

        with open(output_path, "w") as f:
            # Header
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write("C                      IWFM ELEMENT CONFIGURATION FILE\n")
            f.write("C\n")
            f.write("C                         Generated by pyiwfm\n")
            f.write(f"C                         {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write("C\n")
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write(f"{n_elements}                                            / NELEM\n")
            f.write(f"{n_subregions}                                            / NSUBREGION\n")
            f.write("C\n")
            f.write("C   Subregion Names\n")
            f.write(
                "C-----------------------------------------------------------------------------\n"
            )

            # Subregion names - IWFM format: NAME / DESCRIPTION
            # The IDs are implicit based on order and must match element subregion values
            for sr_name in sr_names:
                f.write(f"    {sr_name:<30} / RNAME\n")

            f.write("C\n")
            f.write("C   ID     V1     V2     V3     V4   SR\n")
            f.write(
                "C-----------------------------------------------------------------------------\n"
            )

            # Element data
            for elem_id in sorted_elem_ids:
                elem = grid.elements[elem_id]
                vertices = list(elem.vertices)

                # Pad to 4 vertices (0 for triangles)
                while len(vertices) < 4:
                    vertices.append(0)

                # Get subregion (default to 1 if not set)
                sr = getattr(elem, "subregion", 1) or 1

                f.write(
                    f"{elem_id:6d} {vertices[0]:6d} {vertices[1]:6d} "
                    f"{vertices[2]:6d} {vertices[3]:6d} {sr:4d}\n"
                )

        logger.info(f"Wrote elements file: {output_path} ({n_elements} elements)")
        return output_path

    def write_stratigraphy(self) -> Path:
        """
        Write the stratigraphy data file.

        IWFM stratigraphy format uses THICKNESSES, not elevations:
        - ID, GSElev, W(1), W(2), W(3), W(4), ...
        - W(2*i-1) = aquitard thickness above layer i (from previous bottom or GS to layer top)
        - W(2*i) = aquifer thickness of layer i (from layer top to layer bottom)

        For NLayers layers, each row has 2 + 2*NLayers columns.

        Returns
        -------
        Path
            Path to written file
        """
        output_path = self.config.stratigraphy_path
        self._ensure_dir(output_path)

        strat = self.model.stratigraphy
        if strat is None:
            raise ValueError("Model has no stratigraphy; cannot write stratigraphy file")
        grid = self.model.grid
        if grid is None:
            raise ValueError("Model has no grid; cannot write stratigraphy file")
        n_nodes = self.model.n_nodes
        n_layers = strat.n_layers

        # Get sorted node IDs
        sorted_node_ids = sorted(grid.nodes.keys())

        with open(output_path, "w") as f:
            # Header
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write("C                      IWFM STRATIGRAPHY DATA FILE\n")
            f.write("C\n")
            f.write("C                         Generated by pyiwfm\n")
            f.write(f"C                         {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write("C\n")
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write(f"{n_layers}                                               / NLAYERS\n")
            f.write(f"{'1.0':>14}                                  / FACTEL\n")
            f.write("C\n")

            # Column header - IWFM uses thicknesses: W(1), W(2), W(3), W(4), ...
            # W(2*i-1) = aquitard thickness, W(2*i) = aquifer thickness
            col_header = "C   ID        GSELEV"
            for i in range(n_layers):
                col_header += f"     AQT_L{i + 1}"  # Aquitard thickness
                col_header += f"     AQF_L{i + 1}"  # Aquifer thickness
            f.write(col_header + "\n")
            f.write(
                "C-----------------------------------------------------------------------------\n"
            )

            # Build data array with thicknesses
            # Columns: ID, GS_ELEV, AQT_L1, AQF_L1, AQT_L2, AQF_L2, ...
            data = np.zeros((n_nodes, 2 + 2 * n_layers), dtype=np.float64)

            for i, node_id in enumerate(sorted_node_ids):
                data[i, 0] = node_id
                # Ground surface elevation
                data[i, 1] = strat.gs_elev[i]

                # For each layer, compute aquitard and aquifer thicknesses
                for layer in range(n_layers):
                    # Aquitard thickness: from previous layer bottom (or GS) to current layer top
                    if layer == 0:
                        # First layer: from ground surface to layer top
                        aquitard_thick = strat.gs_elev[i] - strat.top_elev[i, layer]
                    else:
                        # Subsequent layers: from previous layer bottom to current layer top
                        aquitard_thick = strat.bottom_elev[i, layer - 1] - strat.top_elev[i, layer]

                    # Aquifer thickness: from layer top to layer bottom
                    aquifer_thick = strat.top_elev[i, layer] - strat.bottom_elev[i, layer]

                    # Store in columns: W(2*layer+1) = aquitard, W(2*layer+2) = aquifer
                    data[i, 2 + 2 * layer] = aquitard_thick
                    data[i, 2 + 2 * layer + 1] = aquifer_thick

            # Format string: ID (int), GSElev (float), then 2*n_layers thickness values
            fmt = "%6d %12.4f" + " %12.4f" * (2 * n_layers)
            np.savetxt(f, data, fmt=fmt)

        logger.info(f"Wrote stratigraphy file: {output_path} ({n_layers} layers)")
        return output_path

    def write_stream_config(self) -> Path:
        """
        Write the stream configuration file.

        Returns
        -------
        Path
            Path to written file
        """
        output_path = self.config.stream_config_path
        self._ensure_dir(output_path)

        streams = self.model.streams
        version = self.config.stream_version

        with open(output_path, "w") as f:
            # Header
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write("C                      IWFM STREAM CONFIGURATION FILE\n")
            f.write("C\n")
            f.write(f"C                         Version {version}\n")
            f.write("C                         Generated by pyiwfm\n")
            f.write(f"C                         {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write("C\n")
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")

            if streams is None:
                f.write("0                                               / NREACH\n")
                f.write("0                                               / NSTRMNODE\n")
            else:
                # Get reaches
                reaches = list(streams.iter_reaches()) if hasattr(streams, "iter_reaches") else []
                n_reaches = len(reaches)

                f.write(f"{n_reaches}                                               / NREACH\n")
                f.write("C\n")

                # Write reach definitions
                for reach in reaches:
                    f.write(f"C   Reach {reach.id}\n")
                    f.write(f"{reach.id}                                             / ID\n")
                    f.write(f"{reach.upstream_node}                                  / UPNODE\n")
                    f.write(f"{reach.downstream_node}                                / DNNODE\n")
                    f.write(f"{getattr(reach, 'outflow_reach', 0)}                   / OUTREACH\n")

                f.write("C\n")

                # Get stream nodes
                nodes = list(streams.iter_nodes()) if hasattr(streams, "iter_nodes") else []
                n_stream_nodes = len(nodes)

                f.write(
                    f"{n_stream_nodes}                                            / NSTRMNODE\n"
                )
                f.write("C\n")
                f.write("C   ID      GWN   REACH        STAGE\n")
                f.write(
                    "C-----------------------------------------------------------------------------\n"
                )

                for node in nodes:
                    gw_node = getattr(node, "gw_node", node.id)
                    reach_id = getattr(node, "reach_id", 1)
                    stage = getattr(node, "stage", 0.0)
                    f.write(f"{node.id:6d} {gw_node:6d} {reach_id:6d} {stage:12.4f}\n")

        logger.info(f"Wrote stream config file: {output_path}")
        return output_path

    def write_lake_config(self) -> Path:
        """
        Write the lake configuration file.

        Returns
        -------
        Path
            Path to written file
        """
        output_path = self.config.lake_config_path
        self._ensure_dir(output_path)

        lakes = self.model.lakes
        version = self.config.lake_version

        with open(output_path, "w") as f:
            # Header
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")
            f.write("C                      IWFM LAKE CONFIGURATION FILE\n")
            f.write("C\n")
            f.write(f"C                         Version {version}\n")
            f.write("C                         Generated by pyiwfm\n")
            f.write(f"C                         {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write("C\n")
            f.write(
                "C*******************************************************************************\n"
            )
            f.write("C\n")

            if lakes is None:
                f.write("0                                               / NLAKES\n")
            else:
                lake_list = list(lakes.iter_lakes()) if hasattr(lakes, "iter_lakes") else []
                n_lakes = len(lake_list)

                f.write(f"{n_lakes}                                               / NLAKES\n")
                f.write("C\n")

                for lake in lake_list:
                    f.write(f"C   Lake {lake.id}: {getattr(lake, 'name', '')}\n")
                    f.write(f"{lake.id}                                             / LAKEID\n")
                    # Write lake elements
                    elements = getattr(lake, "elements", [])
                    f.write(f"{len(elements)}                                         / NELEM\n")
                    for elem_id in elements:
                        f.write(f"{elem_id}\n")

        logger.info(f"Wrote lake config file: {output_path}")
        return output_path


# =============================================================================
# Convenience Functions
# =============================================================================


def write_preprocessor_files(
    model: IWFMModel,
    output_dir: Path | str,
    stream_version: str = "5.0",
    lake_version: str = "5.0",
) -> dict[str, Path]:
    """
    Write all preprocessor files for a model.

    Parameters
    ----------
    model : IWFMModel
        Model to write
    output_dir : Path or str
        Output directory
    stream_version : str
        Stream component version
    lake_version : str
        Lake component version

    Returns
    -------
    dict[str, Path]
        Mapping of file type to output path

    Example
    -------
    >>> from pyiwfm.core.model import IWFMModel
    >>> from pyiwfm.io.preprocessor_writer import write_preprocessor_files
    >>>
    >>> model = IWFMModel.from_preprocessor("model/Preprocessor.in")
    >>> results = write_preprocessor_files(model, "output/Preprocessor")
    >>> print(results["main"])
    output/Preprocessor/Preprocessor.in
    """
    config = PreProcessorFileConfig(
        output_dir=Path(output_dir),
        stream_version=stream_version,
        lake_version=lake_version,
    )
    writer = PreProcessorWriter(model, config)
    return writer.write_all()


def write_nodes_file(
    output_path: Path | str,
    node_ids: NDArray[np.int32],
    x_coords: NDArray[np.float64],
    y_coords: NDArray[np.float64],
    coord_factor: float = 1.0,
) -> Path:
    """
    Write a standalone nodes file.

    Parameters
    ----------
    output_path : Path or str
        Output file path
    node_ids : NDArray
        Node IDs
    x_coords : NDArray
        X coordinates
    y_coords : NDArray
        Y coordinates
    coord_factor : float
        Coordinate conversion factor

    Returns
    -------
    Path
        Path to written file
    """
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    n_nodes = len(node_ids)

    with open(output_path, "w") as f:
        f.write("C  IWFM Node Coordinates File\n")
        f.write(f"C  Generated by pyiwfm on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write("C\n")
        f.write(f"{n_nodes}                                               / NNODES\n")
        f.write(f"{coord_factor:14.6f}                                  / FACTXY\n")
        f.write("C\n")
        f.write("C   ID              X              Y\n")

        data = np.column_stack([node_ids, x_coords, y_coords])
        np.savetxt(f, data, fmt="%6d %15.6f %15.6f")

    return output_path


def write_elements_file(
    output_path: Path | str,
    element_ids: NDArray[np.int32],
    vertices: NDArray[np.int32],
    subregions: NDArray[np.int32],
    subregion_names: dict[int, str] | None = None,
) -> Path:
    """
    Write a standalone elements file.

    Parameters
    ----------
    output_path : Path or str
        Output file path
    element_ids : NDArray
        Element IDs
    vertices : NDArray
        Vertex node IDs (n_elements, 4)
    subregions : NDArray
        Subregion ID for each element
    subregion_names : dict, optional
        Mapping of subregion ID to name

    Returns
    -------
    Path
        Path to written file
    """
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    n_elements = len(element_ids)
    unique_sr = np.unique(subregions)
    n_subregions = len(unique_sr)

    if subregion_names is None:
        subregion_names = {sr: f"Region_{sr}" for sr in unique_sr}

    with open(output_path, "w") as f:
        f.write("C  IWFM Element Configuration File\n")
        f.write(f"C  Generated by pyiwfm on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write("C\n")
        f.write(f"{n_elements}                                            / NELEM\n")
        f.write(f"{n_subregions}                                            / NSUBREGION\n")
        f.write("C\n")

        for sr_id in sorted(unique_sr):
            f.write(f"{sr_id} / {subregion_names.get(sr_id, f'Region_{sr_id}')}\n")

        f.write("C\n")
        f.write("C   ID     V1     V2     V3     V4   SR\n")

        data = np.column_stack([element_ids, vertices, subregions])
        np.savetxt(f, data, fmt="%6d %6d %6d %6d %6d %4d")

    return output_path


def write_stratigraphy_file(
    output_path: Path | str,
    node_ids: NDArray[np.int32],
    ground_surface: NDArray[np.float64],
    layer_tops: NDArray[np.float64],
    layer_bottoms: NDArray[np.float64],
    elev_factor: float = 1.0,
) -> Path:
    """
    Write a standalone stratigraphy file in IWFM format.

    IWFM stratigraphy uses THICKNESSES, not elevations:
    - ID, GSElev, W(1), W(2), W(3), W(4), ...
    - W(2*i-1) = aquitard thickness above layer i
    - W(2*i) = aquifer thickness of layer i

    Parameters
    ----------
    output_path : Path or str
        Output file path
    node_ids : NDArray
        Node IDs
    ground_surface : NDArray
        Ground surface elevations (n_nodes,)
    layer_tops : NDArray
        Layer top elevations (n_nodes, n_layers)
    layer_bottoms : NDArray
        Layer bottom elevations (n_nodes, n_layers)
    elev_factor : float
        Elevation conversion factor

    Returns
    -------
    Path
        Path to written file
    """
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    n_nodes = len(node_ids)
    n_layers = layer_bottoms.shape[1]

    with open(output_path, "w") as f:
        f.write("C  IWFM Stratigraphy Data File\n")
        f.write(f"C  Generated by pyiwfm on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write("C\n")
        f.write(f"{n_layers}                                               / NLAYERS\n")
        f.write(f"{elev_factor:14.6f}                                  / FACTEL\n")
        f.write("C\n")

        col_header = "C   ID        GSELEV"
        for i in range(n_layers):
            col_header += f"     AQT_L{i + 1}"  # Aquitard thickness
            col_header += f"     AQF_L{i + 1}"  # Aquifer thickness
        f.write(col_header + "\n")

        # Build data with thicknesses
        data = np.zeros((n_nodes, 2 + 2 * n_layers), dtype=np.float64)
        data[:, 0] = node_ids
        data[:, 1] = ground_surface

        for layer in range(n_layers):
            if layer == 0:
                # Aquitard: from ground surface to layer top
                aquitard_thick = ground_surface - layer_tops[:, layer]
            else:
                # Aquitard: from previous layer bottom to current layer top
                aquitard_thick = layer_bottoms[:, layer - 1] - layer_tops[:, layer]

            # Aquifer thickness: from layer top to layer bottom
            aquifer_thick = layer_tops[:, layer] - layer_bottoms[:, layer]

            data[:, 2 + 2 * layer] = aquitard_thick
            data[:, 2 + 2 * layer + 1] = aquifer_thick

        fmt = "%6d %12.4f" + " %12.4f" * (2 * n_layers)
        np.savetxt(f, data, fmt=fmt)

    return output_path
