from __future__ import annotations

import os
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

from one_run.constants import DEFAULT_RUNS_DIR
from one_run.errors import OneRunError
from one_run.models import Manifest
from one_run.status_store import read_metadata, read_status
from one_run.utils.duration import parse_duration

_TERMINAL_STATES = {"success", "failed"}


@dataclass
class _WaitConfig:
    run_ids: list[str]
    tags: list[str]
    timeout_sec: float | None
    interval_sec: float


def _state_for_run(run_dir: Path) -> str:
    status = read_status(run_dir / "status.json")
    if status.state in _TERMINAL_STATES:
        return status.state

    local_exit = run_dir / "local.exit_code"
    if local_exit.exists():
        try:
            code = int(local_exit.read_text(encoding="utf-8").strip())
        except ValueError:
            code = 1
        return "success" if code == 0 else "failed"

    local_pid = run_dir / "local.pid"
    if local_pid.exists():
        try:
            pid = int(local_pid.read_text(encoding="utf-8").strip())
            os.kill(pid, 0)
        except (ValueError, OSError):
            return "failed"

    return status.state


def _resolve_wait_config(
    manifest: Manifest,
    *,
    wait_for_run_ids: list[str] | None,
    wait_timeout: str | None,
    wait_interval: float | None,
) -> _WaitConfig:
    dep = manifest.depends_on
    run_ids = list(wait_for_run_ids or [])
    tags: list[str] = []
    timeout_str = wait_timeout
    interval_sec = wait_interval

    if dep:
        run_ids.extend(dep.run_ids or [])
        tags.extend(dep.tags or [])
        if timeout_str is None:
            timeout_str = dep.timeout
        if interval_sec is None:
            interval_sec = dep.interval_sec

    run_ids = sorted({value.strip() for value in run_ids if value and value.strip()})
    tags = sorted({value.strip() for value in tags if value and value.strip()})

    timeout_sec: float | None = None
    if timeout_str:
        try:
            timeout_sec = parse_duration(timeout_str).total_seconds()
        except ValueError as exc:
            raise OneRunError(str(exc)) from exc

    resolved_interval = interval_sec if interval_sec is not None else 2.0
    if resolved_interval <= 0:
        raise OneRunError("wait interval must be > 0")

    return _WaitConfig(
        run_ids=run_ids,
        tags=tags,
        timeout_sec=timeout_sec,
        interval_sec=resolved_interval,
    )


def _tag_status(project_root: Path, tag: str) -> str:
    runs_root = project_root / DEFAULT_RUNS_DIR
    if not runs_root.exists():
        return "missing"

    found = False
    has_running = False
    has_failed = False
    for run_dir in runs_root.iterdir():
        if not run_dir.is_dir():
            continue
        metadata_path = run_dir / "metadata.json"
        status_path = run_dir / "status.json"
        if not metadata_path.exists() or not status_path.exists():
            continue
        metadata = read_metadata(metadata_path)
        raw_tags = metadata.get("tags")
        if not isinstance(raw_tags, list):
            continue
        tags = {str(value).strip() for value in raw_tags if str(value).strip()}
        if tag not in tags:
            continue
        found = True
        state = _state_for_run(run_dir)
        if state == "success":
            return "ready"
        if state not in _TERMINAL_STATES:
            has_running = True
        elif state == "failed":
            has_failed = True

    if not found:
        return "missing"
    if has_running:
        return "running"
    if has_failed:
        return "failed"
    return "running"


def wait_for_dependencies(
    manifest: Manifest,
    *,
    project_root: Path,
    wait_for_run_ids: list[str] | None = None,
    wait_timeout: str | None = None,
    wait_interval: float | None = None,
    emit_console: bool = True,
) -> None:
    cfg = _resolve_wait_config(
        manifest,
        wait_for_run_ids=wait_for_run_ids,
        wait_timeout=wait_timeout,
        wait_interval=wait_interval,
    )
    if not cfg.run_ids and not cfg.tags:
        return

    started_at = datetime.now()
    if emit_console:
        print("waiting for dependencies...", flush=True)

    while True:
        all_ready = True

        for run_id in cfg.run_ids:
            run_dir = project_root / DEFAULT_RUNS_DIR / run_id
            if not run_dir.exists():
                all_ready = False
                continue
            state = _state_for_run(run_dir)
            if state == "failed":
                raise OneRunError(f"dependency failed: run_id={run_id}")
            if state != "success":
                all_ready = False

        for tag in cfg.tags:
            tag_status = _tag_status(project_root, tag)
            if tag_status == "ready":
                continue
            if tag_status == "failed":
                raise OneRunError(f"dependency failed: tag={tag}")
            if tag_status in {"running", "missing"}:
                all_ready = False
                continue
            all_ready = False

        if all_ready:
            if emit_console:
                print("dependencies ready", flush=True)
            return

        if cfg.timeout_sec is not None:
            elapsed = (datetime.now() - started_at).total_seconds()
            if elapsed >= cfg.timeout_sec:
                raise OneRunError("dependency wait timed out")

        time.sleep(cfg.interval_sec)
