"""Pure decoder and safe tree editor for 1C scheduled-job schedules."""

from __future__ import annotations

import copy
import re
from datetime import datetime
from typing import Any


WRITABLE_SCALAR_FIELDS = {
    "begin_date",
    "end_date",
    "begin_time",
    "end_time",
    "completion_time",
    "completion_interval",
    "repeat_period_in_day",
    "repeat_pause",
    "week_day_in_month",
    "day_in_month",
    "weeks_period",
    "days_repeat_period",
}
WRITABLE_LIST_FIELDS = {"week_days", "months"}
INTEGER_RANGES = {
    "completion_interval": (0, 2_147_483_647),
    "repeat_period_in_day": (0, 2_147_483_647),
    "repeat_pause": (0, 2_147_483_647),
    "week_day_in_month": (0, 5),
    "day_in_month": (0, 31),
    "weeks_period": (0, 2_147_483_647),
    "days_repeat_period": (0, 2_147_483_647),
}


def _scalar(node: Any) -> str:
    if not isinstance(node, dict):
        return str(node or "")
    if node.get("type") in {"atom", "string"}:
        return str(node.get("value") or "")
    return ""


def schedule_datetime(raw: str) -> tuple[str | None, str | None]:
    if not re.fullmatch(r"\d{14}", raw):
        return None, None
    return (
        f"{raw[0:4]}-{raw[4:6]}-{raw[6:8]}",
        f"{raw[8:10]}:{raw[10:12]}:{raw[12:14]}",
    )


def schedule_layout(tree: Any) -> dict[str, Any]:
    items = tree.get("items") if isinstance(tree, dict) and isinstance(tree.get("items"), list) else []
    raw = [_scalar(item) for item in items]
    if len(raw) < 13:
        return {
            "status": "invalid_schedule_payload",
            "diagnostics": {"message": "The SQL schedule payload is shorter than the supported format."},
        }
    week_day_count = int(raw[8]) if re.fullmatch(r"-?\d+", raw[8]) else None
    cursor = 9
    if week_day_count is None or week_day_count < 0 or cursor + week_day_count > len(raw):
        return {
            "status": "invalid_schedule_payload",
            "diagnostics": {"message": "Invalid weekday collection in the SQL schedule payload."},
        }
    week_days_start = cursor
    cursor += week_day_count
    if cursor + 3 > len(raw):
        return {
            "status": "invalid_schedule_payload",
            "diagnostics": {"message": "The SQL schedule payload has no month collection header."},
        }
    week_day_in_month_index = cursor
    day_in_month_index = cursor + 1
    month_count_index = cursor + 2
    month_count = int(raw[month_count_index]) if re.fullmatch(r"-?\d+", raw[month_count_index]) else None
    cursor += 3
    if month_count is None or month_count < 0 or cursor + month_count > len(raw):
        return {
            "status": "invalid_schedule_payload",
            "diagnostics": {"message": "Invalid month collection in the SQL schedule payload."},
        }
    months_start = cursor
    cursor += month_count
    if cursor + 2 > len(raw):
        return {
            "status": "invalid_schedule_payload",
            "diagnostics": {"message": "The SQL schedule payload has no repeat-period tail."},
        }
    return {
        "status": "ok",
        "raw": raw,
        "indexes": {
            "begin_date": 0,
            "end_date": 1,
            "begin_time": 2,
            "end_time": 3,
            "completion_time": 4,
            "completion_interval": 5,
            "repeat_period_in_day": 6,
            "repeat_pause": 7,
            "week_day_count": 8,
            "week_days": list(range(week_days_start, week_days_start + week_day_count)),
            "week_day_in_month": week_day_in_month_index,
            "day_in_month": day_in_month_index,
            "month_count": month_count_index,
            "months": list(range(months_start, months_start + month_count)),
            "weeks_period": cursor,
            "days_repeat_period": cursor + 1,
        },
        "cursor": cursor + 2,
    }


def decode_schedule(tree: Any, *, include_storage: bool = False) -> dict[str, Any]:
    layout = schedule_layout(tree)
    if layout.get("status") != "ok":
        return layout
    raw = layout["raw"]
    indexes = layout["indexes"]

    def integer(field: str) -> int | None:
        value = raw[indexes[field]]
        return int(value) if re.fullmatch(r"-?\d+", value) else None

    begin_date, _ = schedule_datetime(raw[indexes["begin_date"]])
    end_date, _ = schedule_datetime(raw[indexes["end_date"]])
    _, begin_time = schedule_datetime(raw[indexes["begin_time"]])
    _, end_time = schedule_datetime(raw[indexes["end_time"]])
    _, completion_time = schedule_datetime(raw[indexes["completion_time"]])
    result = {
        "status": "ok",
        "begin_date": begin_date,
        "end_date": end_date,
        "begin_time": begin_time,
        "end_time": end_time,
        "completion_time": completion_time,
        "completion_interval": integer("completion_interval"),
        "repeat_period_in_day": integer("repeat_period_in_day"),
        "repeat_pause": integer("repeat_pause"),
        "week_days": [int(raw[index]) for index in indexes["week_days"] if raw[index].isdigit()],
        "week_day_in_month": integer("week_day_in_month"),
        "day_in_month": integer("day_in_month"),
        "months": [int(raw[index]) for index in indexes["months"] if raw[index].isdigit()],
        "weeks_period": integer("weeks_period"),
        "days_repeat_period": integer("days_repeat_period"),
        "evidence": "live_sql_config_schedule_decoder",
    }
    if include_storage:
        result["storage"] = {
            "format": "scheduled_job_config_suffix_0",
            "raw_values": raw,
            "trailing_values": raw[int(layout["cursor"]) :],
        }
    return result


def rebuild_schedule_tree(tree: Any, requested: dict[str, Any]) -> dict[str, Any]:
    layout = schedule_layout(tree)
    if layout.get("status") != "ok":
        return layout
    raw = list(layout["raw"])
    indexes = layout["indexes"]
    current = decode_schedule(tree)

    for field in WRITABLE_SCALAR_FIELDS:
        if field not in requested:
            continue
        value = requested[field]
        index = int(indexes[field])
        if field in {"begin_date", "end_date"}:
            raw[index] = str(value).replace("-", "") + (
                raw[index][8:] if re.fullmatch(r"\d{14}", raw[index]) else "000000"
            )
        elif field in {"begin_time", "end_time", "completion_time"}:
            raw[index] = (
                raw[index][:8] if re.fullmatch(r"\d{14}", raw[index]) else "00010101"
            ) + str(value).replace(":", "")
        else:
            raw[index] = str(value)

    week_days = list(requested.get("week_days", current.get("week_days") or []))
    months = list(requested.get("months", current.get("months") or []))
    rebuilt_raw = [
        *raw[:8],
        str(len(week_days)),
        *(str(value) for value in week_days),
        raw[int(indexes["week_day_in_month"])],
        raw[int(indexes["day_in_month"])],
        str(len(months)),
        *(str(value) for value in months),
        raw[int(indexes["weeks_period"])],
        raw[int(indexes["days_repeat_period"])],
        *raw[int(layout["cursor"]) :],
    ]
    rebuilt_tree = copy.deepcopy(tree)
    rebuilt_tree["items"] = [{"type": "atom", "value": value} for value in rebuilt_raw]
    verification = decode_schedule(rebuilt_tree)
    if verification.get("status") != "ok":
        return {
            "status": "error",
            "error": "schedule_rebuild_verification_failed",
            "diagnostics": {"message": "The rebuilt scheduled-job tree did not pass the schedule decoder."},
        }
    for field, expected in requested.items():
        if verification.get(field) != expected:
            return {
                "status": "error",
                "error": "schedule_rebuild_verification_failed",
                "field": field,
                "diagnostics": {
                    "message": "A named schedule field changed during tree rebuild verification.",
                    "expected": expected,
                    "actual": verification.get(field),
                },
            }
    return {
        "status": "ok",
        "tree": rebuilt_tree,
        "schedule": verification,
        "old_counts": {
            "week_days": len(indexes["week_days"]),
            "months": len(indexes["months"]),
        },
        "new_counts": {
            "week_days": len(week_days),
            "months": len(months),
        },
    }


def schedule_write_edits(tree: Any, requested: Any) -> dict[str, Any]:
    if not isinstance(requested, dict) or not requested:
        return {
            "status": "invalid_argument",
            "error": "schedule_required",
            "diagnostics": {"message": "schedule must be a non-empty JSON object."},
        }
    unknown = sorted(set(requested) - WRITABLE_SCALAR_FIELDS - WRITABLE_LIST_FIELDS)
    if unknown:
        return {
            "status": "invalid_argument",
            "error": "unsupported_schedule_fields",
            "diagnostics": {
                "message": "The request contains unsupported scheduled-job fields.",
                "fields": unknown,
                "allowed_fields": sorted(WRITABLE_SCALAR_FIELDS | WRITABLE_LIST_FIELDS),
            },
        }
    layout = schedule_layout(tree)
    if layout.get("status") != "ok":
        return layout
    current = decode_schedule(tree)
    raw = layout["raw"]
    indexes = layout["indexes"]
    normalized: dict[str, Any] = {}
    edits: list[dict[str, Any]] = []
    resized_collections: list[str] = []

    for field, value in requested.items():
        if field in {"begin_date", "end_date"}:
            if value is None:
                compact = "00010101"
            elif isinstance(value, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
                try:
                    datetime.fromisoformat(value)
                except ValueError:
                    return {
                        "status": "invalid_argument",
                        "error": "invalid_schedule_value",
                        "field": field,
                        "diagnostics": {"message": f"{field} must be a real ISO date YYYY-MM-DD."},
                    }
                compact = value.replace("-", "")
            else:
                return {
                    "status": "invalid_argument",
                    "error": "invalid_schedule_value",
                    "field": field,
                    "diagnostics": {"message": f"{field} must be an ISO date YYYY-MM-DD or null."},
                }
            index = int(indexes[field])
            encoded = compact + (raw[index][8:] if re.fullmatch(r"\d{14}", raw[index]) else "000000")
            normalized[field] = "0001-01-01" if value is None else value
            if encoded != raw[index]:
                edits.append(
                    {
                        "path": str(index),
                        "value": encoded,
                        "node_type": "atom",
                        "expected_old": raw[index],
                        "field": field,
                    }
                )
            continue
        if field in {"begin_time", "end_time", "completion_time"}:
            if value is None:
                compact = "000000"
            elif isinstance(value, str) and re.fullmatch(r"\d{2}:\d{2}:\d{2}", value):
                try:
                    datetime.strptime(value, "%H:%M:%S")
                except ValueError:
                    return {
                        "status": "invalid_argument",
                        "error": "invalid_schedule_value",
                        "field": field,
                        "diagnostics": {"message": f"{field} must be a real time HH:MM:SS."},
                    }
                compact = value.replace(":", "")
            else:
                return {
                    "status": "invalid_argument",
                    "error": "invalid_schedule_value",
                    "field": field,
                    "diagnostics": {"message": f"{field} must be a time HH:MM:SS or null."},
                }
            index = int(indexes[field])
            encoded = (
                raw[index][:8] if re.fullmatch(r"\d{14}", raw[index]) else "00010101"
            ) + compact
            normalized[field] = "00:00:00" if value is None else value
            if encoded != raw[index]:
                edits.append(
                    {
                        "path": str(index),
                        "value": encoded,
                        "node_type": "atom",
                        "expected_old": raw[index],
                        "field": field,
                    }
                )
            continue
        if field in WRITABLE_LIST_FIELDS:
            maximum = 7 if field == "week_days" else 12
            if (
                not isinstance(value, list)
                or any(
                    isinstance(item, bool)
                    or not isinstance(item, int)
                    or item < 1
                    or item > maximum
                    for item in value
                )
                or len(set(value)) != len(value)
            ):
                return {
                    "status": "invalid_argument",
                    "error": "invalid_schedule_value",
                    "field": field,
                    "diagnostics": {
                        "message": f"{field} must be a JSON array of unique integers from 1 to {maximum}."
                    },
                }
            field_indexes = list(indexes[field])
            if len(value) != len(field_indexes):
                resized_collections.append(field)
                normalized[field] = list(value)
                continue
            normalized[field] = list(value)
            for index, item in zip(field_indexes, value):
                encoded = str(item)
                if encoded != raw[index]:
                    edits.append(
                        {
                            "path": str(index),
                            "value": encoded,
                            "node_type": "atom",
                            "expected_old": raw[index],
                            "field": field,
                        }
                    )
            continue
        minimum, maximum = INTEGER_RANGES[field]
        if isinstance(value, bool) or not isinstance(value, int) or value < minimum or value > maximum:
            return {
                "status": "invalid_argument",
                "error": "invalid_schedule_value",
                "field": field,
                "diagnostics": {
                    "message": f"{field} must be a JSON integer from {minimum} to {maximum}."
                },
            }
        index = int(indexes[field])
        encoded = str(value)
        normalized[field] = value
        if encoded != raw[index]:
            edits.append(
                {
                    "path": str(index),
                    "value": encoded,
                    "node_type": "atom",
                    "expected_old": raw[index],
                    "field": field,
                }
            )

    if resized_collections:
        rebuilt = rebuild_schedule_tree(tree, normalized)
        if rebuilt.get("status") != "ok":
            return rebuilt
        edits = [
            {
                "replace_root": rebuilt["tree"],
                "fields": sorted(normalized),
                "resized_collections": sorted(resized_collections),
                "old_counts": rebuilt["old_counts"],
                "new_counts": rebuilt["new_counts"],
            }
        ]
    return {
        "status": "ok",
        "current": current,
        "requested": normalized,
        "edits": edits,
        "counts": {
            "requested_fields": len(normalized),
            "edits": len(edits),
            "resized_collections": len(resized_collections),
        },
    }


# Compatibility aliases retained by adapter_1c_server and existing clients.
config_schedule_datetime = schedule_datetime
scheduled_job_schedule_layout = schedule_layout
scheduled_job_sql_schedule = decode_schedule
scheduled_job_schedule_rebuild_tree = rebuild_schedule_tree
scheduled_job_schedule_write_edits = schedule_write_edits
SCHEDULED_JOB_WRITABLE_SCALAR_FIELDS = WRITABLE_SCALAR_FIELDS
SCHEDULED_JOB_WRITABLE_LIST_FIELDS = WRITABLE_LIST_FIELDS
SCHEDULED_JOB_INTEGER_RANGES = INTEGER_RANGES


__all__ = [
    "INTEGER_RANGES",
    "WRITABLE_LIST_FIELDS",
    "WRITABLE_SCALAR_FIELDS",
    "config_schedule_datetime",
    "decode_schedule",
    "rebuild_schedule_tree",
    "schedule_datetime",
    "schedule_layout",
    "schedule_write_edits",
    "scheduled_job_schedule_layout",
    "scheduled_job_schedule_rebuild_tree",
    "scheduled_job_schedule_write_edits",
    "scheduled_job_sql_schedule",
]
