from __future__ import annotations

import datetime as dt
import json
import re
from pathlib import Path
from typing import Any

from .clock import now as clock_now
from .clock import today as clock_today
from .release import (
    get_current_release,
    get_release_timeline_info,
    load_release_features,
    parse_plan_front_matter,
    parse_release_plan_slot_alignment,
)


def load_sprint_config(*, ph_project_root: Path) -> dict[str, Any]:
    rules_path = ph_project_root / "process" / "checks" / "validation_rules.json"
    try:
        if rules_path.exists():
            return json.loads(rules_path.read_text(encoding="utf-8"))
    except Exception:
        pass

    return {
        "sprint_management": {
            "mode": "bounded",
            "health_check_thresholds": {
                "blocked_percentage_red": 30,
                "progress_percentage_red": 50,
                "progress_check_day": 3,
            },
            "sprint_duration_days": 5,
        }
    }


def use_iso_week_ids(config: dict[str, Any]) -> bool:
    return bool(config.get("sprint_management", {}).get("enforce_iso_week_ids", False))


def sprint_id_scheme(config: dict[str, Any]) -> str:
    """
    Return sprint id scheme:
    - "date" (default): SPRINT-YYYY-MM-DD[-HH]
    - "iso-week": SPRINT-YYYY-W##
    - "sequence": SPRINT-SEQ-0001
    """
    sprint_management = config.get("sprint_management", {}) or {}
    raw = str(sprint_management.get("id_scheme") or "").strip().lower()
    if raw in {"seq", "sequence", "counter"}:
        return "sequence"
    if raw in {"iso-week", "iso_week", "week"}:
        return "iso-week"
    if use_iso_week_ids(config):
        return "iso-week"
    return "date"


def sprint_dir_from_id(*, ph_data_root: Path, sprint_id: str) -> Path:
    parts = sprint_id.split("-")
    year = parts[1] if len(parts) > 1 else dt.date.today().strftime("%Y")
    return ph_data_root / "sprints" / year / sprint_id


def get_sprint_id(*, ph_project_root: Path, ph_data_root: Path, env: dict[str, str]) -> str:
    config = load_sprint_config(ph_project_root=ph_project_root)
    now = clock_now(env=env)
    scheme = sprint_id_scheme(config)
    if scheme == "sequence":
        seq_root = ph_data_root / "sprints" / "SEQ"
        seq_root.mkdir(parents=True, exist_ok=True)
        archive_root = ph_data_root / "sprints" / "archive" / "SEQ"
        archive_root.mkdir(parents=True, exist_ok=True)

        pattern = re.compile(r"^SPRINT-SEQ-(\d{4,})$")
        max_seen = 0
        for base in (seq_root, archive_root):
            for entry in base.iterdir():
                if not entry.is_dir():
                    continue
                match = pattern.match(entry.name)
                if not match:
                    continue
                try:
                    max_seen = max(max_seen, int(match.group(1)))
                except Exception:
                    continue

        next_value = max_seen + 1 if max_seen else 1
        candidate = f"SPRINT-SEQ-{next_value:04d}"
        while (seq_root / candidate).exists() or (archive_root / candidate).exists():
            next_value += 1
            candidate = f"SPRINT-SEQ-{next_value:04d}"
        return candidate

    if scheme == "iso-week":
        iso_date = now.date()
        week_num = iso_date.isocalendar()[1]
        year = iso_date.year
        return f"SPRINT-{year}-W{week_num:02d}"

    base = f"SPRINT-{now:%Y-%m-%d}"
    year_dir = ph_data_root / "sprints" / f"{now.year:04d}"
    archive_year_dir = ph_data_root / "sprints" / "archive" / f"{now.year:04d}"
    year_dir.mkdir(parents=True, exist_ok=True)

    candidate = base
    if (year_dir / candidate).exists() or (archive_year_dir / candidate).exists():
        candidate = f"{base}-{now:%H}"
        suffix = 0
        while (year_dir / candidate).exists() or (archive_year_dir / candidate).exists():
            suffix += 1
            candidate = f"{base}-{now:%H}{suffix:02d}"
    return candidate


def get_sprint_dates(sprint_id: str) -> tuple[dt.date, dt.date]:
    parts = sprint_id.split("-")
    if len(parts) >= 3 and parts[2].startswith("W"):
        year = int(parts[1])
        week = int(parts[2][1:])
        jan1 = dt.date(year, 1, 1)
        week1_monday = jan1 - dt.timedelta(days=jan1.weekday())
        sprint_start = week1_monday + dt.timedelta(weeks=week - 1)
        sprint_end = sprint_start + dt.timedelta(days=4)
        return sprint_start, sprint_end

    if len(parts) < 4:
        raise ValueError(f"Unrecognized sprint id: {sprint_id}")
    date_str = "-".join(parts[1:4])
    sprint_start = dt.datetime.strptime(date_str, "%Y-%m-%d").date()
    sprint_end = sprint_start + dt.timedelta(days=4)
    return sprint_start, sprint_end


def update_current_symlink(*, ph_data_root: Path, sprint_id: str) -> None:
    sprints_dir = ph_data_root / "sprints"
    current_link = sprints_dir / "current"
    sprint_path = sprint_dir_from_id(ph_data_root=ph_data_root, sprint_id=sprint_id)

    if current_link.exists() or current_link.is_symlink():
        try:
            current_link.unlink()
        except FileNotFoundError:
            pass

    if sprint_path.exists():
        current_link.symlink_to(sprint_path.relative_to(sprints_dir))


def _get_release_timeline_info(*, ph_project_root: Path, version: str) -> dict[str, object]:
    try:
        return get_release_timeline_info(ph_root=ph_project_root, version=version)
    except Exception:
        plan_path = ph_project_root / "releases" / version / "plan.md"
        return parse_plan_front_matter(plan_path=plan_path)


def _get_current_release_context(*, ph_project_root: Path) -> dict[str, object]:
    version = get_current_release(ph_root=ph_project_root)
    if not version:
        return {}
    features = load_release_features(ph_root=ph_project_root, version=version)
    timeline = _get_release_timeline_info(ph_project_root=ph_project_root, version=version)
    return {"version": version, "features": features, "timeline": timeline}


def create_sprint_plan_template(
    *, ph_project_root: Path, ph_data_root: Path, scope: str, sprint_id: str, env: dict[str, str]
) -> str:
    config = load_sprint_config(ph_project_root=ph_project_root)
    sprint_management = config.get("sprint_management")
    sprint_management_dict = sprint_management if isinstance(sprint_management, dict) else {}
    raw_mode = sprint_management_dict.get("mode")

    # Defensive fallback: when validation_rules.json exists but omits sprint_management (or mode),
    # default sprint planning to bounded (not timeboxed).
    if raw_mode is None or (isinstance(raw_mode, str) and not raw_mode.strip()):
        mode = "bounded"
    else:
        mode = str(raw_mode).strip().lower()
        if mode not in {"bounded", "timeboxed"}:
            mode = "bounded"

    release_context: dict[str, object] = {}
    if scope != "system":
        release_context = _get_current_release_context(ph_project_root=ph_project_root)

    template = f"""---
title: Sprint Plan - {sprint_id}
type: sprint-plan
date: {clock_today(env=env).strftime("%Y-%m-%d")}
sprint: {sprint_id}
mode: {mode}
tags: [sprint, planning]
"""

    release_slot: int | None = None

    # System scope MUST NOT include release context.
    if scope == "system":
        template += "release: null\n"
    else:
        release_version = str(release_context.get("version") or "").strip()
        template += f"release: {release_version}\n" if release_version else "release: null\n"
        if release_version:
            timeline = release_context.get("timeline") if isinstance(release_context.get("timeline"), dict) else {}
            timeline_mode = str((timeline or {}).get("timeline_mode") or "").strip().lower()
            if timeline_mode == "sprint_slots":
                slots = (timeline or {}).get("sprint_slots") or []
                assignments = (timeline or {}).get("slot_assignments") or {}
                next_slot: int | None = None
                for slot in slots:
                    try:
                        slot_int = int(slot)
                    except Exception:
                        continue
                    if slot_int not in assignments:
                        next_slot = slot_int
                        break
                if next_slot is not None:
                    release_slot = next_slot
                    template += f"release_sprint_slot: {next_slot}\n"

    if mode == "timeboxed":
        start_date, end_date = get_sprint_dates(sprint_id)
        template += f"start: {start_date.strftime('%Y-%m-%d')}\n"
        template += f"end: {end_date.strftime('%Y-%m-%d')}\n"

    template += "---\n\n"
    template += f"# Sprint Plan: {sprint_id}\n\n"

    if scope != "system" and str(release_context.get("version") or "").strip():
        version = str(release_context.get("version") or "").strip()
        template += "## Release Context\n"
        timeline = release_context.get("timeline") if isinstance(release_context.get("timeline"), dict) else {}
        sprint_index = (timeline or {}).get("current_sprint_index")
        planned_sprints = (timeline or {}).get("planned_sprints")
        end_sprint = (timeline or {}).get("end_sprint")

        template += f"**Release**: {version}\n"
        if sprint_index and planned_sprints:
            template += f"**Release sprint position**: Sprint {sprint_index} of {planned_sprints}\n"
        if end_sprint:
            template += f"**Release target**: {end_sprint}\n"
        features = release_context.get("features") if isinstance(release_context.get("features"), dict) else {}
        slot_features: list[tuple[str, dict[str, object]]] = []
        other_features: list[tuple[str, dict[str, object]]] = []
        for feature_name, feature_data in (features or {}).items():
            feature_dict = feature_data if isinstance(feature_data, dict) else {}
            if release_slot is not None:
                raw_slot = feature_dict.get("slot")
                try:
                    slot_val = int(raw_slot)  # type: ignore[arg-type]
                except Exception:
                    slot_val = None
                if slot_val == int(release_slot):
                    slot_features.append((feature_name, feature_dict))
                else:
                    other_features.append((feature_name, feature_dict))
            else:
                other_features.append((feature_name, feature_dict))

        def _feature_line(fname: str, fmeta: dict[str, object]) -> str:
            critical_note = " (Critical Path)" if fmeta.get("critical_path") else ""
            return f"- {fname}: {fmeta.get('type', 'regular')}{critical_note}"

        if release_slot is not None:
            template += f"**Slot {release_slot} features**:\n"
            for fname, fmeta in sorted(slot_features, key=lambda t: t[0]):
                template += _feature_line(fname, fmeta) + "\n"
            template += "\n"
            if other_features:
                template += "<details>\n<summary>Other release features</summary>\n\n"
                for fname, fmeta in sorted(other_features, key=lambda t: t[0]):
                    template += _feature_line(fname, fmeta) + "\n"
                template += "\n</details>\n\n"
        else:
            template += "**Features in this release**:\n"
            for fname, fmeta in sorted(other_features, key=lambda t: t[0]):
                template += _feature_line(fname, fmeta) + "\n"
            template += "\n"

    slot_alignment_lines: list[str] | None = None
    release_version_value = str(release_context.get("version") or "").strip()
    if scope != "system" and release_version_value and release_slot is not None:
        alignment = parse_release_plan_slot_alignment(
            plan_path=ph_project_root / "releases" / release_version_value / "plan.md",
            slot=release_slot,
        )
        if not alignment:
            raise ValueError(
                "\n".join(
                    [
                        "Release slot alignment is missing or unreadable.",
                        f"  release: {release_version_value}",
                        f"  slot: {release_slot}",
                        "  expected: strict slot section like `## Slot <n>: <label>` with required subsections",
                        f"  remediation: ph release migrate-slot-format --release {release_version_value} --write-back",
                    ]
                )
            )

        slot_goal = str(alignment.get("slot_goal") or "").strip() or "TBD"
        enablement = str(alignment.get("enablement") or "").strip() or "TBD"
        intended_gates_raw = alignment.get("intended_gates")
        intended_gates = intended_gates_raw if isinstance(intended_gates_raw, list) else []
        intended_gates = [str(item).rstrip() for item in intended_gates if str(item).strip()]

        if slot_goal.strip().upper() == "TBD" or enablement.strip().upper() == "TBD":
            raise ValueError(
                "\n".join(
                    [
                        "Release plan slot alignment is incomplete (TBD).",
                        f"  release: {release_version_value}",
                        f"  slot: {release_slot}",
                        "  expected: `### Slot Goal` and `### Enablement` filled with non-TBD values",
                        f"  file: .project-handbook/releases/{release_version_value}/plan.md",
                    ]
                )
            )

        non_tbd_gates = [g for g in intended_gates if "tbd" not in g.lower()]
        if not non_tbd_gates:
            raise ValueError(
                "\n".join(
                    [
                        "Release plan slot intended gates are incomplete (TBD).",
                        f"  release: {release_version_value}",
                        f"  slot: {release_slot}",
                        "  expected: at least one `- Gate: ...` bullet under `### Intended Gates`",
                        f"  file: .project-handbook/releases/{release_version_value}/plan.md",
                    ]
                )
            )

        features = load_release_features(ph_root=ph_project_root, version=release_version_value)
        slot_features: list[tuple[str, dict[str, object]]] = []
        for fname, fmeta in (features or {}).items():
            meta = fmeta if isinstance(fmeta, dict) else {}
            raw_slot = meta.get("slot")
            try:
                slot_val = int(raw_slot)  # type: ignore[arg-type]
            except Exception:
                continue
            if slot_val == int(release_slot):
                slot_features.append((fname, meta))
        slot_features.sort(key=lambda t: t[0])
        if not slot_features:
            raise ValueError(
                "\n".join(
                    [
                        "No features are assigned to the current release slot.",
                        f"  release: {release_version_value}",
                        f"  slot: {release_slot}",
                        f"  remediation: ph release add-feature --release {release_version_value} --feature <name> "
                        f"--slot {release_slot} --commitment committed --intent deliver",
                        f"  file: .project-handbook/releases/{release_version_value}/features.yaml",
                    ]
                )
            )

        slot_alignment_lines = [
            f"## Release Alignment (Slot {release_slot})",
            f"Slot goal: {slot_goal}",
            f"Enablement: {enablement}",
            "Intended gates:",
            *non_tbd_gates,
            "",
            "Slot Features:",
            *[
                (
                    f"- {fname} "
                    f"[{str(meta.get('commitment') or 'committed').strip().lower()}/"
                    f"{str(meta.get('intent') or 'deliver').strip().lower()}]"
                )
                for fname, meta in slot_features
            ],
            "",
        ]

    if mode == "bounded":
        if scope == "system":
            task_create_cmd = "ph --scope system task create"
            bounded_lines = [
                "## Sprint Model",
                (
                    "This sprint uses **bounded planning** (ADR-0013): sprint scope is defined by "
                    "*work boundaries* and *parallel lanes*, not a fixed calendar window or points cap."
                ),
                "",
                "## Sprint Goal",
                "1. [ ] Primary outcome (clear deliverable + validation gate)",
                "2. [ ] Secondary outcome",
                "3. [ ] Integration outcome (if multiple lanes)",
                "",
                "## Boundaries (Lanes)",
                "Define lanes to maximize parallel execution and minimize cross-lane coupling.",
                "",
                "| Lane | Scope | Success Output |",
                "|------|-------|----------------|",
                "| `service/<name>` | | |",
                "| `infra/<name>` | | |",
                "| `docs/<area>` | | |",
                "| `integration/<scope>` | | |",
                "",
                "## Integration Tasks",
                "- List explicit cross-lane integration tasks and their dependencies.",
                "",
                "## Task Creation Guide",
                "```bash",
                (
                    f'{task_create_cmd} --title "Task Name" --feature feature-name --decision ADR-XXX '
                    '--points 3 --lane "ops/automation"'
                ),
                "```",
                "",
                "## Telemetry (Points)",
                "- Story points are tracked for throughput/velocity trends, not for limiting sprint scope.",
                "",
                "## Dependencies & Risks",
                "- External dependencies",
                "- Cross-lane dependencies (integration risk)",
                "- Unknowns / validation gaps",
                "",
                "## Success Criteria",
                "- [ ] Lanes are explicit and independently executable",
                "- [ ] Integration tasks are explicit and dependency-linked",
                "- [ ] All committed tasks completed (points recorded for telemetry)",
            ]
            template += "\n".join(bounded_lines) + "\n"
            return template

        release_alignment_lines = (
            slot_alignment_lines
            if slot_alignment_lines
            else [
                "## Release Alignment (Explicit)",
                "If you have an active release, it is a *measurement context* — not an automatic scope cap.",
                "",
                "| Bucket | Intention | Notes |",
                "|--------|-----------|-------|",
                "| Release-critical | Work that must move critical-path features | |",
                "| Release-support | Work that enables the release but lives outside assigned features | |",
                "| Non-release | Necessary work that does not serve the active release | |",
                "",
            ]
        )

        project_bounded_lines = [
            "## Sprint Model",
            (
                "This sprint uses **bounded planning** (ADR-0013): sprint scope is defined by "
                "*work boundaries* and *parallel lanes*, not a fixed calendar window or points cap."
            ),
            "",
            "## Sprint Goal",
            "1. [ ] Primary outcome (clear deliverable + validation gate)",
            "2. [ ] Secondary outcome",
            "3. [ ] Integration outcome (if multiple lanes)",
            "",
            *release_alignment_lines,
            "## Boundaries (Lanes)",
            "Define lanes to maximize parallel execution and minimize cross-lane coupling.",
            "",
            "| Lane | Scope | Success Output |",
            "|------|-------|----------------|",
            "| `service/<name>` | | |",
            "| `infra/<name>` | | |",
            "| `docs/<area>` | | |",
            "| `integration/<scope>` | | |",
            "",
            "## Integration Tasks",
            "- List explicit cross-lane integration tasks and their dependencies.",
            "",
            "## Task Creation Guide",
            "```bash",
            (
                'ph task create --title "Task Name" --feature feature-name --decision ADR-XXX --points 3 '
                '--lane "ops/automation" --release current'
            ),
            (
                'ph task create --title "Gate: <name>" --feature feature-name --decision ADR-XXX --points 3 '
                '--lane "integration/<scope>" --release current --gate'
            ),
            "```",
            "",
            "## Telemetry (Points)",
            "- Story points are tracked for throughput/velocity trends, not for limiting sprint scope.",
            "",
            "## Dependencies & Risks",
            "- External dependencies",
            "- Cross-lane dependencies (integration risk)",
            "- Unknowns / validation gaps",
            "",
            "## Success Criteria",
            "- [ ] Lanes are explicit and independently executable",
            "- [ ] Integration tasks are explicit and dependency-linked",
            "- [ ] All committed tasks completed (points recorded for telemetry)",
        ]
        template += "\n".join(project_bounded_lines) + "\n"
        return template

    start_date, end_date = get_sprint_dates(sprint_id)
    start_label = start_date.strftime("%A, %B %d, %Y")
    end_label = end_date.strftime("%A, %B %d, %Y")
    timeboxed_lines: list[str] = [
        "## Sprint Duration",
        f"- Start: {start_label}",
        f"- End: {end_label}",
        "",
        "## Sprint Goals",
    ]
    if scope != "system" and str(release_context.get("version") or "").strip():
        timeboxed_lines.append("*(Align with release features above)*")

    release_alignment_timeboxed_lines = (
        slot_alignment_lines
        if slot_alignment_lines
        else [
            "## Release Alignment (Optional)",
            "| Bucket | Intention | Notes |",
            "|--------|-----------|-------|",
            "| Release-critical | | |",
            "| Release-support | | |",
            "| Non-release | | |",
            "",
        ]
    )
    timeboxed_lines.extend(
        [
            "1. [ ] Primary goal",
            "2. [ ] Secondary goal",
            "3. [ ] Stretch goal",
            "",
            *release_alignment_timeboxed_lines,
            "## Task Creation Guide",
            "```bash",
            (
                'ph task create --title "Task Name" --feature feature-name --decision ADR-XXX --points 5 '
                "--release current"
            ),
            (
                'ph task create --title "Gate: <name>" --feature feature-name --decision ADR-XXX --points 3 '
                "--release current --gate"
            ),
            "```",
            "",
            "## Capacity Planning (Optional)",
            "- Use capacity planning only if it adds signal for your team.",
            "",
            "## Dependencies & Risks",
            "- External dependencies",
            "- Cross-team dependencies",
            "- Technical risks",
            "",
            "## Success Criteria",
            "- [ ] All committed tasks completed (story points delivered)",
            "- [ ] Sprint health maintained",
        ]
    )
    template += "\n".join(timeboxed_lines) + "\n"

    return template
