import argparse
import datetime as dt
import os
import sys
from typing import Any, Dict, Optional, TypedDict

import yaml

from schedint.core.storage import (
    OverridesFormatError,
    OverrideWindow,
    RequestNotFoundError,
    _now_utc,
    _parse_utc,
    append_request_atomic,
    build_request,
    delete_request_by_prefix_atomic,
    filter_requests,
    iter_override_windows,
    load_state,
)
from schedint.core.validate import assert_no_overlap_for_source
from schedint.daemon.client import (
    DaemonClientError,
    cancel_request,
    get_cadence_multiplier,
    list_requests,
    set_cadence_multiplier,
    submit_request,
)
from schedint.logging_config import configure_logging, get_logger

logger = get_logger("cli")


class SubmitPayload(TypedDict):
    source: str
    reason: Optional[str]
    overrides: Dict[str, Dict[str, Any]]


def _fmt_utc_z(t: Optional[dt.datetime]) -> Optional[str]:
    if t is None:
        return None
    if t.tzinfo is None:
        t = t.replace(tzinfo=dt.timezone.utc)
    return t.astimezone(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _payload_from_manual_args(args: argparse.Namespace) -> SubmitPayload:
    start = _parse_utc(args.start_time) or _now_utc()
    end = _parse_utc(args.end_time)

    overrides: Dict[str, Dict[str, Any]] = {}

    if args.set_stars is not None:
        overrides["stars"] = {
            "value": args.set_stars,
            "start_time": _fmt_utc_z(start),
            "end_time": _fmt_utc_z(end),
        }

    if args.set_tint is not None:
        overrides["tint"] = {
            "value": args.set_tint,
            "start_time": _fmt_utc_z(start),
            "end_time": _fmt_utc_z(end),
        }

    if args.set_cadence is not None:
        overrides["cadence"] = {
            "value": args.set_cadence,
            "start_time": _fmt_utc_z(start),
            "end_time": _fmt_utc_z(end),
        }

    return {
        "source": args.source,
        "reason": args.reason,
        "overrides": overrides,
    }


def _payload_from_yaml_file(path: str) -> SubmitPayload:
    with open(path, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)

    if not isinstance(data, dict):
        raise ValueError("Request YAML must be a mapping")

    source = data.get("source")
    reason = data.get("reason")
    overrides = data.get("overrides")

    if not isinstance(source, str) or not source:
        raise ValueError("source must be a non-empty string")
    if reason is not None and not isinstance(reason, str):
        raise ValueError("reason must be a string or null")
    if not isinstance(overrides, dict) or not overrides:
        raise ValueError("overrides must be a non-empty mapping")

    # overrides can include datetime objects if YAML parsed timestamps; that's fine.
    return {"source": source, "reason": reason, "overrides": overrides}


def submit_payload(args: argparse.Namespace, payload: SubmitPayload) -> str:
    """
    Submit via daemon if enabled, otherwise submit locally.
    Returns request_id.
    """
    if args.use_daemon:

        def _normalise_block(block: dict) -> dict:
            return {
                "value": block.get("value"),
                "start_time": _fmt(block.get("start_time")),
                "end_time": _fmt(block.get("end_time")),
            }

        def _fmt(t):
            if t is None:
                return None
            if isinstance(t, dt.datetime):
                if t.tzinfo is None:
                    t = t.replace(tzinfo=dt.timezone.utc)
                return t.astimezone(dt.timezone.utc).strftime(
                    "%Y-%m-%dT%H:%M:%SZ"
                )
            return t  # already a string

        overrides = payload["overrides"]
        normalised_overrides = {
            name: _normalise_block(block) for name, block in overrides.items()
        }

        resp = submit_request(
            args.socket_path,
            source=payload["source"],
            reason=payload.get("reason"),
            overrides=normalised_overrides,
        )

        if not resp.ok:
            raise ValueError(resp.error or "Unknown daemon error")
        assert resp.request_id is not None
        return resp.request_id

    overrides_windows = {}
    for name, block in payload["overrides"].items():
        if not isinstance(block, dict):
            raise ValueError(f"overrides.{name} must be a mapping")
        overrides_windows[name] = OverrideWindow(
            value=block.get("value"),
            start=_parse_utc(block.get("start_time")),
            end=_parse_utc(block.get("end_time")),
        )

    req = build_request(
        source=payload["source"],
        overrides=overrides_windows,
        reason=payload["reason"],
        # build_request already fills in user/host/time/id sensibly
    )

    append_request_atomic(
        args.state_path,
        req,
        overlap_check=assert_no_overlap_for_source,
    )
    return req.request_id


def handle_multiplier(args: argparse.Namespace) -> int:
    try:
        if args.mult_cmd == "get":
            resp = get_cadence_multiplier(args.socket_path)
            if not resp.ok:
                print(f"Error: {resp.error}", file=sys.stderr)
                return 2
            print((resp.raw or {}).get("cadence_multiplier", 1.0))
            return 0

        if args.mult_cmd == "set":
            resp = set_cadence_multiplier(args.socket_path, value=args.value)
            if not resp.ok:
                print(f"Error: {resp.error}", file=sys.stderr)
                return 2
            print((resp.raw or {}).get("cadence_multiplier", "<unknown>"))
            return 0

        print("Error: Unknown multiplier command", file=sys.stderr)
        return 2

    except DaemonClientError as e:
        print(f"Error: {e}", file=sys.stderr)
        return 2


def handle_template(args: argparse.Namespace) -> int:
    template = """ \
# schedint override request template
# Fill in required fields and submit with:
#   schedint submit file /path/to/request.yaml

source: J1913+0446
reason: "Brief explanation for override"

overrides:
  stars:
    value: 3
    start_time: 2026-02-01T00:00:00Z
    end_time: 2026-02-10T00:00:00Z

  #tint:
  #  value: 720
  #  start_time: 2026-02-01T00:00:00Z
  #  end_time: 2026-02-10T00:00:00Z

  #cadence:
  #  value: 14.0
  #  start_time: 2026-02-01T00:00:00Z
  #  end_time: 2026-02-10T00:00:00Z
"""
    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(template)
        logger.info(f"Written template to {args.output}")
    else:
        print(template)

    return 0


def handle_list(args: argparse.Namespace) -> int:

    if args.use_daemon:
        between = None
        if args.between:
            between = (args.between[0], args.between[1])

        try:
            resp = list_requests(
                args.socket_path,
                request_id=args.request_id,
                source=args.source,
                submitted_by=args.user,
                active=True if args.active else None,
                expired=True if args.expired else None,
                between=between,
            )
        except DaemonClientError as e:
            print(f"Error: {e}", file=sys.stderr)
            return 2

        if not resp.ok:
            print(f"Error: {resp.error}", file=sys.stderr)

        reqs = (resp.raw or {}).get("requests", [])
        for r in reqs:
            print(
                f"{r.get('request_id')}  {r.get('source')}  by={r.get('submitted_by')}  at={r.get('submitted_at')}"
            )

            overrides = r.get("overrides") or {}
            for name, ow in overrides.items():
                if not isinstance(ow, dict):
                    continue
                if ow.get("value") is None:
                    continue
                print(
                    f"  - {name}: value={ow.get('value')}  {ow.get('start_time')} -> {ow.get('end_time')}"
                )
        return 0

    # Local mode
    state = load_state(args.state_path)

    between = None
    if args.between:
        start = _parse_utc(args.between[0])
        end = _parse_utc(args.between[1])

        if start is None or end is None:
            raise ValueError("between requires two non-null timestamps")

        between = (start, end)

    requests = filter_requests(
        state,
        request_id=args.request_id,
        source=args.source,
        submitted_by=args.user,
        active=True if args.active else None,
        expired=True if args.expired else None,
        between=between,
    )

    for this_request in requests:
        print(
            f"{this_request.request_id}  {this_request.source}  by={this_request.submitted_by}  at={this_request.submitted_at}"
        )

        for name, override_window in iter_override_windows(this_request):
            if override_window.value is None:
                continue
            print(
                f"  - {name}: value={override_window.value}  {override_window.start} -> {override_window.end}"
            )

    return 0


def handle_delete(args: argparse.Namespace) -> int:
    if args.use_daemon:
        try:
            resp = cancel_request(
                args.socket_path,
                request_id_prefix=args.id,
            )
        except DaemonClientError as e:
            print(f"ERROR: {e}", file=sys.stderr)
            return 2

        if not resp.ok:
            print(f"ERROR: {resp.error}", file=sys.stderr)
            return 2

        deleted_id = (resp.raw or {}).get("deleted_request_id", "<unknown>")
        source = (resp.raw or {}).get("source", "<unknown>")
        print(f"OK: deleted {deleted_id}  source={source}")
        return 0

    try:
        deleted = delete_request_by_prefix_atomic(args.state_path, args.id)
    except RequestNotFoundError as e:
        print(f"ERROR: {e}", file=sys.stderr)
        return 2

    print(
        f"OK: deleted {deleted.request_id}  source={deleted.source} "
        f"submitted_by={deleted.submitted_by}  submitted_at={deleted.submitted_at}"
    )

    return 0


def build_parser() -> argparse.ArgumentParser:

    parser = argparse.ArgumentParser(
        prog="schedint",
        description="Jodrell Bank Pulsar Observing Schedule Editor",
    )

    parser.add_argument(
        "--log-level",
        default="INFO",
        help="Set verbosity",
    )

    parser.add_argument(
        "--state-path",
        default="data/overrides_example.yaml",
        help="Path to overrides YAML (dev default)",
    )

    parser.add_argument(
        "--socket-path",
        default="/run/schedint/schedint.sock",
        help="Unix socket path for schedintd (used with daemon mode)",
    )

    mode = parser.add_mutually_exclusive_group()

    mode.add_argument(
        "--daemon",
        dest="use_daemon",
        action="store_true",
        help="Use schedintd over the Unix socket (default)",
    )

    mode.add_argument(
        "--local",
        dest="use_daemon",
        action="store_false",
        help="Operate directly on the state file (dev/test only)",
    )

    # Backwards-compat alias: keep for a release or two, then remove.
    mode.add_argument(
        "--use-daemon",
        dest="use_daemon",
        action="store_true",
        help=argparse.SUPPRESS,
    )

    parser.set_defaults(use_daemon=True)

    sub = parser.add_subparsers(dest="cmd", required=True)

    # template
    p = sub.add_parser(
        "template",
        help="Print a YAML template for a new request",
    )
    p.add_argument("--output", help="Write to file instead of stdout")

    p = sub.add_parser(
        "multiplier", help="Show or set the global cadence multiplier"
    )
    mult_sub = p.add_subparsers(dest="mult_cmd")
    p.set_defaults(mult_cmd="get")

    p_set = mult_sub.add_parser("set", help="Set cadence multiplier")
    p_set.add_argument("value", type=float, help="New multiplier (>0)")

    # list
    p = sub.add_parser("list", help="List override requests")
    p.add_argument("--user")
    p.add_argument("--request_id")
    p.add_argument("--source")
    status = p.add_mutually_exclusive_group()
    status.add_argument("--active", action="store_true")
    status.add_argument("--expired", action="store_true")
    p.add_argument("--between", nargs=2, metavar=("START", "END"))
    p.add_argument("--output")

    # submit (parent)
    submit = sub.add_parser("submit", help="Submit a new override request")
    submit_sub = submit.add_subparsers(dest="mode", required=True)

    # submit file
    p = submit_sub.add_parser("file", help="Submit request from YAML file")
    p.add_argument("path", help="Path to YAML request file")

    # submit manual
    p = submit_sub.add_parser(
        "manual", help="Submit request from command line"
    )
    p.add_argument(
        "--source", required=True, help="Source name. E.g., J1913+0446"
    )
    p.add_argument(
        "--start-time", default=None, help="Override activation time"
    )
    p.add_argument("--end-time", default=None, help="Override expiry time")
    p.add_argument(
        "--set-stars", type=int, default=None, help="Cadence tier (stars)"
    )
    p.add_argument(
        "--set-tint", type=int, default=None, help="Integration time (seconds)"
    )
    p.add_argument(
        "--set-cadence", type=float, default=None, help="Cadence (days)"
    )
    p.add_argument("--reason", default=None, help="Reason for override")
    p.set_defaults(_required_override=True)

    # cancel
    p = sub.add_parser("cancel", help="Cancel an existing active override")
    p.add_argument(
        "id", help="ID of override to cancel (at least the first 6 characters)"
    )

    return parser


def _validate_args(
    parser: argparse.ArgumentParser, args: argparse.Namespace
) -> None:
    """
    TODO: Add docstring
    """

    daemon_only = os.environ.get(
        "SCHEDINT_DAEMON_ONLY", ""
    ).strip().lower() in {
        "1",
        "true",
        "yes",
        "on",
    }

    if daemon_only and not args.use_daemon:
        parser.error("Local mode is disable in this environment")

    if args.cmd == "multiplier" and args.mult_cmd == "set":
        if args.value <= 0:
            parser.error("Multiplier must be positive")

    if args.cmd == "submit" and args.mode == "manual":
        if not (
            args.set_stars is not None
            or args.set_tint is not None
            or args.set_cadence is not None
        ):
            parser.error(
                "submit manual requires at least one of --set-stars/--set-tint/--set-cadence"
            )

        if args.set_stars is not None and not (0 <= args.set_stars <= 4):
            parser.error("--set-stars must be between 0 and 4")

        if args.end_time is None:
            parser.error(
                "submit manual requires --end-time (ISO8601 like 2026-01-30T10:00:00Z)"
            )

        if args.set_stars is not None and args.set_cadence is not None:
            raise ValueError(
                "Cannot set stars AND cadence - choose one or the other"
            )

        if not args.reason:
            raise ValueError("--reason must be supplied to submit override")


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    configure_logging(level=args.log_level)

    _validate_args(parser, args)

    # Dispatch
    if args.cmd == "submit" and args.mode == "file":
        logger.info("Submitting from file: %s", args.path)

        try:
            payload = _payload_from_yaml_file(args.path)
            rid = submit_payload(args, payload)
            logger.info(f"OK: Submitted {rid}")
            return 0
        except (
            DaemonClientError,
            ValueError,
            OverridesFormatError,
            ValueError,
            yaml.YAMLError,
        ) as e:
            print(f"Error: {e}", file=sys.stderr)
            return 2

    if args.cmd == "submit" and args.mode == "manual":
        logger.info("Submitting manual override for source: %s", args.source)

        try:
            payload = _payload_from_manual_args(args)
            rid = submit_payload(args, payload)
            logger.info(f"OK: Submitted {rid}")
            return 0
        except (
            DaemonClientError,
            ValueError,
            OverridesFormatError,
            ValueError,
            yaml.YAMLError,
        ) as e:
            print(f"Error: {e}", file=sys.stderr)
            return 2

    if args.cmd == "list":
        return handle_list(args)

    if args.cmd == "template":
        return handle_template(args)

    if args.cmd == "cancel":
        return handle_delete(args)

    if args.cmd == "multiplier":
        return handle_multiplier(args)

    return 0
