"""CSV import/export for maintenance objects and tasks."""

from __future__ import annotations

import csv
import io
import logging
import re
from typing import Any
from uuid import uuid4

from homeassistant.core import HomeAssistant

from ..const import (
    CONF_OBJECT,
    CONF_TASKS,
    DEFAULT_TASK_PRIORITY,
    DEFAULT_WARNING_DAYS,
    MAX_CHECKLIST_ITEM_LENGTH,
    MAX_CHECKLIST_ITEMS,
)
from .aggregate import merged_tasks
from .dates import INTERVAL_UNITS
from .global_options import get_default_warning_days
from .schedule import read_legacy_fields

_LOGGER = logging.getLogger(__name__)

# CSV column order
_COLUMNS = [
    "object_name",
    "object_manufacturer",
    "object_model",
    "object_serial_number",
    "object_area_id",
    "object_installation_date",
    "object_warranty_expiry",
    # The 2026-08 round-trip audit: the IMPORT read these two from day one,
    # but the export never wrote the columns — a CSV migration silently
    # dropped object notes + documentation URL.
    "object_documentation_url",
    "object_notes",
    "task_name",
    "task_type",
    "enabled",
    "schedule_type",
    "interval_days",
    "interval_unit",
    "due_date",
    "interval_anchor",
    "schedule_time",
    "reading_unit",
    "warning_days",
    "last_performed",
    "notes",
    "documentation_url",
    "custom_icon",
    "nfc_tag_id",
    "responsible_user_id",
    # #134 audit: priority routes notifications/filters and labels drive views
    # — a CSV migration losing them silently was the object_notes class again.
    "priority",
    "labels",
    "trigger_type",
    "status",
    "times_performed",
    "total_cost",
    # Checklist exported as a single cell with steps separated by literal "\n".
    # The csv module handles the embedded newlines via RFC 4180 field quoting.
    "checklist",
]


def _csv_safe(val: str) -> str:
    """Prefix cells that start with formula-triggering characters to mitigate CSV injection."""
    if val and val[0] in ("=", "+", "-", "@"):
        return "\t" + val
    return val


def export_objects_csv(hass: HomeAssistant, entry_ids: set[str] | None = None) -> str:
    """Export maintenance objects and tasks as CSV (all, or a selection).

    Each row represents one task, with the parent object info repeated.
    """
    from ..export import object_entries

    entries = object_entries(hass, entry_ids)

    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=_COLUMNS, extrasaction="ignore")
    writer.writeheader()

    for entry in entries:
        obj_data = entry.data.get(CONF_OBJECT, {})
        # Merge static + Store dynamic data
        tasks_data = merged_tasks(entry)

        rd = getattr(entry, "runtime_data", None)
        coord_data = rd.coordinator.data if rd and rd.coordinator else None
        ct_tasks = (coord_data or {}).get(CONF_TASKS, {})

        for tid, tdata in tasks_data.items():
            ct = ct_tasks.get(tid, {})
            sched = read_legacy_fields(tdata)
            writer.writerow(
                {
                    "object_name": _csv_safe(obj_data.get("name", "")),
                    "object_manufacturer": _csv_safe(obj_data.get("manufacturer", "")),
                    "object_model": _csv_safe(obj_data.get("model", "")),
                    "object_serial_number": _csv_safe(obj_data.get("serial_number", "")),
                    "object_area_id": _csv_safe(obj_data.get("area_id", "")),
                    "object_installation_date": _csv_safe(obj_data.get("installation_date", "")),
                    "object_warranty_expiry": _csv_safe(obj_data.get("warranty_expiry", "")),
                    "object_documentation_url": _csv_safe(obj_data.get("documentation_url") or ""),
                    "object_notes": _csv_safe(obj_data.get("notes") or ""),
                    "task_name": _csv_safe(tdata.get("name", "")),
                    "task_type": tdata.get("type", "custom"),
                    "enabled": tdata.get("enabled", True),
                    "schedule_type": sched["schedule_type"],
                    "interval_days": sched["interval_days"] if sched["interval_days"] is not None else "",
                    "interval_unit": sched["interval_unit"],
                    "due_date": sched["due_date"] or "",
                    "interval_anchor": sched["interval_anchor"],
                    "schedule_time": tdata.get("schedule_time", ""),
                    "reading_unit": _csv_safe(tdata.get("reading_unit", "")),
                    "warning_days": tdata.get("warning_days", DEFAULT_WARNING_DAYS),
                    "last_performed": tdata.get("last_performed", ""),
                    "notes": _csv_safe(tdata.get("notes", "")),
                    "documentation_url": _csv_safe(tdata.get("documentation_url", "")),
                    "custom_icon": _csv_safe(tdata.get("custom_icon", "")),
                    "nfc_tag_id": _csv_safe(tdata.get("nfc_tag_id", "")),
                    "responsible_user_id": _csv_safe(tdata.get("responsible_user_id", "")),
                    "priority": tdata.get("priority") or DEFAULT_TASK_PRIORITY,
                    # Labels ride one cell, "\n"-separated like the checklist.
                    "labels": "\n".join(_csv_safe(lb) for lb in (tdata.get("labels") or []) if lb),
                    "trigger_type": (tdata.get("trigger_config") or {}).get("type", ""),
                    "status": ct.get("_status", "ok"),
                    "times_performed": ct.get("_times_performed", 0),
                    "total_cost": ct.get("_total_cost", 0.0),
                    # Each item is _csv_safe()-prefixed individually so a step
                    # starting with "=" can't trigger a formula in Excel after
                    # the cell is unpacked.
                    "checklist": "\n".join(_csv_safe(item) for item in (tdata.get("checklist") or []) if item),
                }
            )

    return output.getvalue()


# (#67) Per-object CSV columns — one row per maintenance object (asset record).
_OBJECT_RECORD_COLUMNS = [
    "object_name",
    "object_manufacturer",
    "object_model",
    "object_serial_number",
    "object_area_id",
    "object_installation_date",
    "object_warranty_expiry",
    "object_documentation_url",
    "object_notes",
    "task_count",
]


def export_object_records_csv(hass: HomeAssistant, entry_ids: set[str] | None = None) -> str:
    """Export one row per maintenance object (the objects-table download, #67).

    Unlike ``export_objects_csv`` (one row per task, object fields repeated),
    this emits exactly one row per object — including objects that have no
    tasks, which the per-task export skips entirely — and carries the full
    asset field set used by the objects table. ``entry_ids`` narrows to a
    selection (None = all).
    """
    from ..export import object_entries

    entries = object_entries(hass, entry_ids)

    output = io.StringIO()
    writer = csv.DictWriter(output, fieldnames=_OBJECT_RECORD_COLUMNS, extrasaction="ignore")
    writer.writeheader()

    for entry in entries:
        obj_data = entry.data.get(CONF_OBJECT, {})
        tasks_data = merged_tasks(entry)
        writer.writerow(
            {
                "object_name": _csv_safe(obj_data.get("name", "")),
                "object_manufacturer": _csv_safe(obj_data.get("manufacturer") or ""),
                "object_model": _csv_safe(obj_data.get("model") or ""),
                "object_serial_number": _csv_safe(obj_data.get("serial_number") or ""),
                "object_area_id": _csv_safe(obj_data.get("area_id") or ""),
                "object_installation_date": _csv_safe(obj_data.get("installation_date") or ""),
                "object_warranty_expiry": _csv_safe(obj_data.get("warranty_expiry") or ""),
                "object_documentation_url": _csv_safe(obj_data.get("documentation_url") or ""),
                "object_notes": _csv_safe(obj_data.get("notes") or ""),
                "task_count": len(tasks_data),
            }
        )

    return output.getvalue()


def import_objects_csv(
    csv_content: str,
    hass: HomeAssistant | None = None,
) -> list[dict[str, Any]]:
    """Parse CSV content into a list of object dicts ready for creation.

    When *hass* is supplied, missing per-row ``warning_days`` columns fall back
    to the integration-wide default from the global config entry. Without
    *hass* (e.g. in unit tests that exercise the parser in isolation), the
    bare constant ``7`` is used.

    Returns a list of objects, each with 'object' and 'tasks' dicts
    matching the format expected by the config flow.
    """
    default_warning_days = get_default_warning_days(hass) if hass is not None else 7
    reader = csv.DictReader(io.StringIO(csv_content))

    # Group rows by object name
    objects_map: dict[str, dict[str, Any]] = {}

    for row in reader:
        obj_name = (row.get("object_name") or "").strip()
        if not obj_name:
            continue

        if obj_name not in objects_map:
            objects_map[obj_name] = {
                "object": {
                    "id": uuid4().hex,
                    "name": obj_name,
                    "manufacturer": (row.get("object_manufacturer") or "").strip() or None,
                    "model": (row.get("object_model") or "").strip() or None,
                    "serial_number": (row.get("object_serial_number") or "").strip() or None,
                    "area_id": (row.get("object_area_id") or "").strip() or None,
                    "installation_date": (row.get("object_installation_date") or "").strip() or None,
                    "warranty_expiry": (row.get("object_warranty_expiry") or "").strip() or None,
                    # In the object-CSV export columns but never read back on
                    # import until now (round-trip gap, audit 2026-07-11).
                    "documentation_url": (row.get("object_documentation_url") or "").strip() or None,
                    "notes": (row.get("object_notes") or "").strip() or None,
                    "task_ids": [],
                },
                "tasks": {},
            }

        task_name = (row.get("task_name") or "").strip()
        if not task_name:
            continue

        task_id = uuid4().hex
        task_data: dict[str, Any] = {
            "id": task_id,
            "object_id": objects_map[obj_name]["object"]["id"],
            "name": task_name,
            "type": (row.get("task_type") or "custom").strip(),
            "enabled": True,
            "schedule_type": (row.get("schedule_type") or "time_based").strip(),
            "warning_days": _safe_int(row.get("warning_days"), default_warning_days),
            "history": [],
        }

        interval = row.get("interval_days", "").strip()
        if interval:
            task_data["interval_days"] = _safe_int(interval, None)

        interval_unit = (row.get("interval_unit") or "").strip().lower()
        if interval_unit in INTERVAL_UNITS:
            task_data["interval_unit"] = interval_unit

        due_date = (row.get("due_date") or "").strip()
        if due_date and re.fullmatch(r"\d{4}-\d{2}-\d{2}", due_date):
            task_data["due_date"] = due_date

        anchor = (row.get("interval_anchor") or "").strip()
        if anchor in ("planned", "completion"):
            task_data["interval_anchor"] = anchor

        # schedule_time round-trip with strict HH:MM validation; malformed
        # values are dropped silently (consistent with other CSV import fields).
        sched_time = (row.get("schedule_time") or "").strip()
        if sched_time and re.fullmatch(r"^([01]\d|2[0-3]):[0-5]\d$", sched_time):
            task_data["schedule_time"] = sched_time

        reading_unit = (row.get("reading_unit") or "").strip()
        if reading_unit:
            task_data["reading_unit"] = reading_unit[:32]

        last_performed = (row.get("last_performed") or "").strip()
        if last_performed:
            task_data["last_performed"] = last_performed

        notes = (row.get("notes") or "").strip()
        if notes:
            task_data["notes"] = notes

        # Optional fields (backwards-compatible — missing columns default to empty)
        if (row.get("enabled") or "").strip().lower() == "false":
            task_data["enabled"] = False
        doc_url = (row.get("documentation_url") or "").strip()
        if doc_url:
            from urllib.parse import urlparse

            scheme = urlparse(doc_url).scheme.lower()
            if scheme in ("", "http", "https"):
                task_data["documentation_url"] = doc_url
        custom_icon = (row.get("custom_icon") or "").strip()
        if custom_icon:
            task_data["custom_icon"] = custom_icon
        nfc_tag = (row.get("nfc_tag_id") or "").strip()
        if nfc_tag:
            task_data["nfc_tag_id"] = nfc_tag
        resp_user = (row.get("responsible_user_id") or "").strip()
        if resp_user:
            task_data["responsible_user_id"] = resp_user

        # #134 audit: priority (closed set) + labels ("\n" cell like checklist).
        priority = (row.get("priority") or "").strip().lower()
        if priority in ("low", "normal", "high"):
            task_data["priority"] = priority
        labels_raw = row.get("labels") or ""
        if labels_raw:
            from ..const import MAX_LABEL_LENGTH, MAX_LABELS

            labels = [line.strip()[:MAX_LABEL_LENGTH] for line in labels_raw.splitlines() if line.strip()][:MAX_LABELS]
            if labels:
                task_data["labels"] = labels

        # Checklist round-trips via a single cell with "\n" between items.
        # Apply the same hard caps as the WebSocket schema so a malicious or
        # accidental CSV can't bloat the entry.
        checklist_raw = row.get("checklist") or ""
        if checklist_raw:
            items = [line.strip()[:MAX_CHECKLIST_ITEM_LENGTH] for line in checklist_raw.splitlines() if line.strip()][
                :MAX_CHECKLIST_ITEMS
            ]
            if items:
                task_data["checklist"] = items

        objects_map[obj_name]["tasks"][task_id] = task_data
        objects_map[obj_name]["object"]["task_ids"].append(task_id)

    return list(objects_map.values())


def _safe_int(value: str | None, default: int | None) -> int | None:
    """Safely convert a string to int."""
    if value is None:
        return default
    try:
        return int(float(value))
    except (ValueError, TypeError):
        return default
