#!/usr/bin/env python3
"""Fill AIME production order template from Odoo MRP export.

Usage:
    python fill_production_order.py <source_xlsx>

The script loads the specified Excel source file, extracts relevant fields,
fills the ODS template, and saves it as a new ODS named after cell A2 of the
source file.
"""

from __future__ import annotations

import argparse
import os
import re
import sys
from collections import defaultdict
from copy import deepcopy
from pathlib import Path

import openpyxl
from odf import opendocument
from odf.table import Table, TableCell
from odf.text import P

# Relative paths to the template and source file(s).
TEMPLATE_GLOB = "A4004_*.ods"
SOURCE_GLOB = "Production Order (mrp.production)*.xlsx"

# Mapping from component categories in the source to target merged ranges in
# the template. The value is the cell address where the first matching component
# description should be written. Subsequent matches are written to the rows
# directly below the start cell within the allowed range.
CATEGORY_TARGETS = {
    "All / Components / Barebone": "C10",
    "All / Components / CPU": "C11",
    "All / Components / RAM": "C12",
    "All / Components / GPU": "C20",
    "All / Components / SSD": "C24",
    "All / Components / RAID": "C27",
}

# Maximum number of rows available for each category in the template. A value
# of 1 means only the start cell may be used.
CATEGORY_ROW_LIMITS = {
    "All / Components / Barebone": 1,
    "All / Components / CPU": 1,
    "All / Components / RAM": 8,
    "All / Components / GPU": 4,
    "All / Components / SSD": 2,
    "All / Components / RAID": 1,
}


def _col_letter_to_index(letter: str) -> int:
    """Convert an Excel-style column letter to a 0-based index."""
    idx = 0
    for ch in letter.upper():
        idx = idx * 26 + (ord(ch) - ord("A") + 1)
    return idx - 1


def _cell_addr_to_indices(addr: str) -> tuple[int, int]:
    """Convert an address like 'C10' to (row, col) zero-based indices."""
    match = re.fullmatch(r"([A-Za-z]+)(\d+)", addr)
    if not match:
        raise ValueError(f"Invalid cell address: {addr}")
    col = _col_letter_to_index(match.group(1))
    row = int(match.group(2)) - 1
    return row, col


def find_source_files(directory: Path) -> list[Path]:
    """Return all source Excel files sorted alphabetically."""
    files = sorted(directory.glob(SOURCE_GLOB))
    return files


def extract_source_data(source_path: Path) -> dict:
    """Read the source workbook and return extracted values.

    Returned dict keys:
        - order_name: content of cell A2
        - serial: suffix after 'FA-'
        - model: model designation after 'AIME-'
        - full_model: model + '-' + serial
        - categories: mapping category -> list of component names from column E
    """
    wb = openpyxl.load_workbook(source_path, data_only=True)
    ws = wb.active

    order_name = ws.cell(row=2, column=1).value
    if order_name is None:
        raise ValueError(f"Cell A2 is empty in {source_path}")
    order_name = str(order_name).strip()

    serial = None
    model = None
    categories: dict[str, list[str]] = defaultdict(list)

    for row in ws.iter_rows(min_row=2, values_only=True):
        # row layout from inspection: A=order, B=FA-..., C=AIME-...,
        # D=category, E=component name
        cell_b = row[1] if len(row) > 1 else None
        cell_c = row[2] if len(row) > 2 else None
        cell_d = row[3] if len(row) > 3 else None
        cell_e = row[4] if len(row) > 4 else None

        if isinstance(cell_b, str):
            m = re.search(r"FA-([A-Za-z0-9]+)", cell_b)
            if m:
                serial = m.group(1)

        if isinstance(cell_c, str):
            m = re.search(r"AIME-([A-Za-z0-9]+)", cell_c)
            if m:
                model = m.group(1)

        if isinstance(cell_d, str):
            category = cell_d.strip()
            if category in CATEGORY_TARGETS and isinstance(cell_e, str):
                categories[category].append(cell_e.strip())

    wb.close()

    if serial is None:
        raise ValueError(f"Could not find 'FA-' serial number in {source_path}")
    if model is None:
        raise ValueError(f"Could not find 'AIME-' model in {source_path}")

    full_model = f"{model}-{serial}"

    return {
        "order_name": order_name,
        "serial": serial,
        "model": model,
        "full_model": full_model,
        "categories": dict(categories),
    }


def _set_cell_value(table, row: int, col: int, value: str) -> None:
    """Set the text value of a table cell, preserving style references."""
    cell = table.getCellByPosition(col, row)
    # Clone the existing cell to keep style information, then clear paragraphs.
    new_cell = deepcopy(cell)
    for child in list(new_cell.childNodes):
        new_cell.removeChild(child)
    p = P(text=str(value))
    new_cell.addElement(p)
    table.setCellByPosition(col, row, new_cell)


def _find_template(directory: Path) -> Path:
    """Return the first ODS file matching the template name pattern."""
    candidates = sorted(directory.glob(TEMPLATE_GLOB))
    if not candidates:
        raise FileNotFoundError(
            f"No ODS template matching '{TEMPLATE_GLOB}' found in {directory}"
        )
    return candidates[0]


def fill_template(source_path: Path, output_path: Path | None = None) -> Path:
    """Fill the template using data from ``source_path``.

    The output file is named after cell A2 of the source file unless an
    explicit ``output_path`` is provided.
    """
    directory = source_path.parent
    data = extract_source_data(source_path)

    template_path = _find_template(directory)
    doc = opendocument.load(template_path)

    # Find the first table in the template.
    tables = doc.spreadsheet.getElementsByType(Table)
    table = next(iter(tables), None)
    if table is None:
        raise ValueError("No table found in the template ODS file")

    # Write model-serial combination into C10? No – C10 is reserved for
    # Barebone. The user requested to combine model and serial into a variable
    # but did not specify a target cell. We keep the variable available and log
    # it; nothing is pasted into the template for it automatically.
    print(f"Extracted order: {data['order_name']}")
    print(f"Model: {data['model']}, Serial: {data['serial']}")
    print(f"Combined: {data['full_model']}")

    # Fill component categories.
    for category, items in data["categories"].items():
        start_addr = CATEGORY_TARGETS[category]
        max_rows = CATEGORY_ROW_LIMITS[category]
        start_row, start_col = _cell_addr_to_indices(start_addr)

        for i, item in enumerate(items[:max_rows]):
            target_row = start_row + i
            # Write into the left cell of the merged range; ODS merges span
            # multiple columns, so writing C is sufficient for C:D.
            _set_cell_value(table, target_row, start_col, item)
            print(f"  {category} -> {start_addr[0]}{start_row + i + 1}: {item}")

        if len(items) > max_rows:
            print(
                f"  Warning: {category} has {len(items)} items, "
                f"only {max_rows} fit in the template",
                file=sys.stderr,
            )

    # Determine output path.
    if output_path is None:
        output_path = directory / f"{data['order_name']}.ods"
    else:
        output_path = Path(output_path)

    doc.save(output_path)
    print(f"Saved: {output_path}")
    return output_path


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Fill production order template from an Odoo export."
    )
    parser.add_argument(
        "source",
        nargs="?",
        help="Path to the source Excel file. If omitted, the most recent source "
        "file matching the production order glob is used.",
    )
    parser.add_argument(
        "-o", "--output", help="Optional explicit output ODS file path"
    )
    args = parser.parse_args(argv)

    directory = Path(__file__).resolve().parent

    if args.source:
        source_path = Path(args.source).expanduser()
        if not source_path.is_absolute():
            source_path = directory / source_path
    else:
        sources = find_source_files(directory)
        if not sources:
            print(f"No source file matching '{SOURCE_GLOB}' found.", file=sys.stderr)
            return 1
        source_path = sources[-1]
        print(f"Using source file: {source_path.name}")

    if not source_path.exists():
        print(f"Source file not found: {source_path}", file=sys.stderr)
        return 1

    fill_template(source_path, args.output)
    return 0


if __name__ == "__main__":
    sys.exit(main())
