#!/usr/bin/env python3
"""
update-tasks

Synchronizes and regenerates Documentation/Tasks.md:
1. Parses YAML frontmatter for projects, epics, task-statuses, task-types, and task-id-prefix.
2. Extracts all task chunks and sorts the Detailed Tasks section strictly by numeric task ID.
3. Updates the Detailed Tasks section:
   - Normalizes all task IDs to uniform digit width (minimum 3 digits, e.g. OT-001).
   - Synchronizes task detail blocks (including optional **Epic:**) with anchor tags (SSOT).
4. Re-renders the Overall Progress Table (tasks-list) from the sorted, updated task details:
   - Bold (**Title**) for pending tasks
   - Italic (*Title*) for planning and triage tasks
   - Plain (Title) for fixed/done/other tasks
   - Type column links ([Type](#ot-xxx))
5. Re-renders the metadata derived enum tables at the end of the document:
   - Projects (Rendered from frontmatter projects)
   - Task Statuses (Rendered from frontmatter task-statuses)
   - Task Types (Rendered from frontmatter task-types)
6. Assembles and writes the updated document.
"""

import sys
import os
import re
from pathlib import Path


def find_tasks_file(explicit_path=None):
    explicit_path = explicit_path or os.environ.get("TASKS_FILE")
    if explicit_path:
        p = Path(explicit_path)
        if p.is_file():
            return p
        raise FileNotFoundError(f"Specified file not found: {explicit_path}")

    # Search upwards or relative to script / cwd
    script_dir = Path(__file__).resolve().parent
    candidates = [
        Path.cwd() / "docs" / "tasks.md",
        Path.cwd() / "docs" / "Tasks.md",
        Path.cwd() / "Documentation" / "tasks.md",
        Path.cwd() / "Documentation" / "Tasks.md",
        Path.cwd() / "tasks.md",
        Path.cwd() / "Tasks.md",
        script_dir.parent / "docs" / "tasks.md",
        script_dir.parent / "docs" / "Tasks.md",
        script_dir.parent / "Documentation" / "tasks.md",
        script_dir.parent / "Documentation" / "Tasks.md",
        script_dir / "docs" / "tasks.md",
        script_dir / "docs" / "Tasks.md",
        script_dir / "Documentation" / "tasks.md",
        script_dir / "Documentation" / "Tasks.md",
    ]
    for c in candidates:
        if c.is_file():
            return c

    raise FileNotFoundError("Could not locate docs/tasks.md or docs/Tasks.md")


def parse_yaml_frontmatter(fm_text):
    sections = {}
    current_section = None
    current_item = None

    for line in fm_text.splitlines():
        if line and not line.startswith(" ") and ":" in line and not line.endswith(":"):
            key, value = line.split(":", 1)
            sections[key.strip()] = value.strip().strip("\"'")
            current_section = None
            current_item = None
        elif line and not line.startswith(" ") and line.endswith(":"):
            current_section = line[:-1].strip()
            sections[current_section] = []
            current_item = None
        elif current_section is not None:
            stripped = line.strip()
            if not stripped:
                continue
            if line.startswith("  - "):
                current_item = {}
                sections[current_section].append(current_item)
                kv = line[4:].strip()
                if ":" in kv:
                    k, v = kv.split(":", 1)
                    current_item[k.strip()] = v.strip().strip("\"'")
            elif line.startswith("    ") and current_item is not None:
                kv = line.strip()
                if ":" in kv:
                    k, v = kv.split(":", 1)
                    current_item[k.strip()] = v.strip().strip("\"'")

    return sections


TASK_ID_PATTERN = r'\bid=["\']([a-z][a-z0-9_-]*-(\d+))["\']'
TASK_ANCHOR_PATTERN = r'<(?:a\s+)?([^>]*' + TASK_ID_PATTERN + r'[^>]*)>'


def read_frontmatter(text):
    match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
    if not match:
        raise ValueError("YAML frontmatter not found in tasks.md")
    return parse_yaml_frontmatter(match.group(1))


def get_task_id_prefix(fm_data):
    prefix = os.environ.get("TASK_ID_PREFIX", fm_data.get("task-id-prefix", "OT"))
    if not isinstance(prefix, str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]*", prefix):
        raise ValueError("Task ID prefix must start with a letter and contain only letters, digits, underscores, or hyphens")
    return prefix.upper()


def format_task_id(number, prefix, digit_width=3):
    return f"{prefix}-{number:0{max(3, digit_width, len(str(number)))}d}"


def parse_task_id(id_str):
    match = re.fullmatch(r"(?:[a-z][a-z0-9_-]*-)?(\d+)", str(id_str).strip(), re.IGNORECASE)
    if not match:
        raise ValueError(f"Invalid task ID '{id_str}'; expected a task number or PREFIX-number")
    return int(match.group(1))


def task_id_matches(id_str, anchor_id, prefix):
    number = parse_task_id(id_str)
    task_id = str(id_str).strip()
    anchor_prefix, anchor_number = anchor_id.rsplit("-", 1)
    return number == int(anchor_number) and (
        task_id.isdigit() or task_id.rsplit("-", 1)[0].lower() in (anchor_prefix.lower(), prefix.lower())
    )


def resolve_epic(epic, fm_data):
    if epic is None or epic.strip().lower() in ("", "none", "null", "clear"):
        return "", ""
    for item in fm_data.get("epics", []):
        value = item.get("value", "").strip()
        label = item.get("label", "").strip() or value.replace("-", " ").replace("_", " ").title()
        if value and epic.strip().lower() in (value.lower(), label.lower()):
            return value, label
    raise ValueError(f"Unknown epic '{epic}'; use a value or label from frontmatter epics")


def build_markdown_table(headers, rows, alignments=None):
    if not rows:
        return ""

    num_cols = len(headers)
    widths = [len(h) for h in headers]
    for row in rows:
        for i, val in enumerate(row):
            widths[i] = max(widths[i], len(str(val)))

    if alignments is None:
        alignments = [":---"] * num_cols

    header_line = "| " + " | ".join(f"{h:<{widths[i]}}" for i, h in enumerate(headers)) + " |\n"
    separator_parts = []
    for i, a in enumerate(alignments):
        w = max(widths[i], 3)
        if a.startswith(":") and a.endswith(":"):
            separator_parts.append(":" + "-" * (w - 2) + ":")
        elif a.endswith(":"):
            separator_parts.append("-" * (w - 1) + ":")
        else:
            separator_parts.append(":" + "-" * (w - 1))
    separator_line = "|" + "|".join(separator_parts) + "|\n"

    row_lines = []
    for row in rows:
        line = "| " + " | ".join(f"{str(row[i]):<{widths[i]}}" for i in range(num_cols)) + " |\n"
        row_lines.append(line)

    return header_line + separator_line + "".join(row_lines)


def update_tasks_document(tasks_path):
    tasks_path = Path(tasks_path)
    with open(tasks_path, "r", encoding="utf-8") as f:
        text = f.read()

    # 1. Parse frontmatter
    fm_match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
    if not fm_match:
        raise ValueError("YAML frontmatter not found in tasks.md")

    fm_text = fm_match.group(1)
    fm_data = parse_yaml_frontmatter(fm_text)
    prefix = get_task_id_prefix(fm_data)

    project_list = fm_data.get("projects", [])
    epic_list = fm_data.get("epics", [])
    subproject_list = fm_data.get("subprojects", [])
    status_list = fm_data.get("task-statuses", [])
    type_list = fm_data.get("task-types", [])

    # Maps for lookup (both value and label matching)
    project_map = {}
    project_value_map = {}
    for p in project_list:
        val = p.get("value", "").strip()
        lbl = p.get("label", "").strip()
        if val:
            project_map[val.lower()] = lbl or val.capitalize()
            project_value_map[val.lower()] = val
        if lbl:
            project_map[lbl.lower()] = lbl
            if val:
                project_value_map[lbl.lower()] = val

    subproject_map = {}
    subproject_value_map = {}
    for sp in subproject_list:
        val = sp.get("value", "").strip()
        lbl = sp.get("label", "").strip()
        if val:
            subproject_map[val.lower()] = lbl or val.replace("-", " ").title()
            subproject_value_map[val.lower()] = val
        if lbl:
            subproject_map[lbl.lower()] = lbl
            if val:
                subproject_value_map[lbl.lower()] = val

    status_map = {s.get("value", "").lower(): s.get("label", "") for s in status_list}
    status_value_map = {s.get("value", "").lower(): s.get("value", "") for s in status_list}
    for s in status_list:
        lbl = s.get("label", "").strip()
        val = s.get("value", "").strip()
        if lbl:
            status_map[lbl.lower()] = lbl
            if val:
                status_value_map[lbl.lower()] = val
    # Accept 'done' or 'fixed' fallbacks if not defined in frontmatter
    status_map.setdefault("fixed", status_map.get("done", "Done"))
    status_map.setdefault("done", status_map.get("fixed", "Done"))
    status_value_map.setdefault("fixed", "done")
    status_value_map.setdefault("done", "done")

    type_map = {t.get("value", "").lower(): t.get("label", "") for t in type_list}
    type_value_map = {t.get("value", "").lower(): t.get("value", "") for t in type_list}
    for t in type_list:
        lbl = t.get("label", "").strip()
        val = t.get("value", "").strip()
        if lbl:
            type_map[lbl.lower()] = lbl
            if val:
                type_value_map[lbl.lower()] = val

    # 2. Locate boundaries
    m_detail = re.search(r"^## Detailed Tasks\s*$", text, re.MULTILINE)
    if not m_detail:
        raise ValueError("## Detailed Tasks heading not found")
    detail_start = m_detail.start()
    detail_header_len = len(m_detail.group(0))

    m_notes = re.search(r"(?:\n\s*---\s*)?\n\s*## Notes\s*$", text, re.MULTILINE)
    if not m_notes:
        raise ValueError("## Notes section marker not found")
    notes_start = m_notes.start()

    # Determine part_a (everything before the summary table)
    tasks_list_m = re.search(r'<a\s+[^>]*\bid=["\']tasks-list["\'][^>]*></a>', text, re.IGNORECASE)
    progress_m = re.search(r'^## Overall Progress\s*$', text, re.MULTILINE)

    if tasks_list_m and tasks_list_m.start() < detail_start:
        part_a = text[:tasks_list_m.end()].rstrip() + "\n\n"
        table_search_start = tasks_list_m.end()
    elif progress_m and progress_m.start() < detail_start:
        part_a = text[:progress_m.end()].rstrip() + "\n\n"
        table_search_start = progress_m.end()
    else:
        table_start_marker = "| ID"
        table_start = text.find(table_start_marker)
        if table_start == -1 or table_start >= detail_start:
            raise ValueError("Overall Progress Table (| ID) not found")
        # Strip any leading git conflict markers right before | ID from part_a
        raw_part_a = text[:table_start]
        raw_part_a = re.sub(r'<{7}\s+[^\n]*\n*$', '', raw_part_a)
        part_a = raw_part_a.rstrip() + "\n\n"
        table_search_start = table_start

    # Determine part_c (the transition into ## Detailed Tasks)
    # Check if <a id="task-details"></a> was present between table and ## Detailed Tasks
    between_table_and_detail = text[table_search_start:detail_start]
    has_task_details_anchor = bool(re.search(r'<a\s+[^>]*\bid=["\']task-details["\'][^>]*>', between_table_and_detail, re.IGNORECASE))

    if has_task_details_anchor:
        part_c = "\n\n---\n\n<a id=\"task-details\"></a>\n\n## Detailed Tasks\n"
    else:
        part_c = "\n\n---\n\n## Detailed Tasks\n"

    detail_body = text[detail_start + detail_header_len : notes_start]

    # 3. Extract and Sort Task Chunks by Numeric ID
    task_chunks = re.split(r"(?=<a\s+[^>]*\bid=|<id=)", detail_body)
    task_chunks = [c.strip() for c in task_chunks if c.strip()]

    if not task_chunks:
        raise ValueError("No task chunks found in Detailed Tasks")

    extracted_chunks = []
    for chunk in task_chunks:
        m = re.search(TASK_ID_PATTERN, chunk, re.IGNORECASE)
        if not m:
            raise ValueError(f"Could not find task ID in chunk:\n{chunk[:150]}")
        num = int(m.group(2))
        extracted_chunks.append((num, chunk))

    seen_nums = {}
    for num, chunk in extracted_chunks:
        seen_nums.setdefault(num, []).append(chunk)

    duplicates = [num for num, chunks in seen_nums.items() if len(chunks) > 1]
    if duplicates:
        prefix = get_task_id_prefix(fm_data)
        dup_ids = [format_task_id(num, prefix) for num in sorted(duplicates)]
        raise ValueError(f"Duplicate numeric task IDs found in Detailed Tasks: {', '.join(dup_ids)}")

    # Sort task detail section chunks by numeric ID
    extracted_chunks.sort(key=lambda x: x[0])

    max_id_num = extracted_chunks[-1][0] if extracted_chunks else 1
    digit_width = max(3, len(str(max_id_num)))

    # 4. Update the Task Detail Section (First)
    updated_detailed_chunks = []
    updated_task_records = []

    header_block_pattern = r'<(?:a\s+[^>]*\bid=|\bid=)["\'][a-zA-Z][a-zA-Z0-9_-]*-\d+["\'][^>]*></a>\s*\n###\s+[^\n]+\n\*\*ID:\*\*[^\n]+\n\*\*Project:\*\*[^\n]+(?:\n\*\*(?:Subproject|Epic):\*\*[^\n]*)*\n\*\*Status:\*\*[^\n]+\n\*\*Type:\*\*[^\n]+'
    anchor_replacements = {}

    for num, chunk in extracted_chunks:
        anchor_match = re.search(TASK_ANCHOR_PATTERN, chunk, re.IGNORECASE)
        if not anchor_match:
            raise ValueError(f"Anchor tag missing in task chunk {num}")
        anchor_tag = anchor_match.group(1)

        proj_m = re.search(r'data-project=["\']([^"\']+)["\']', anchor_tag)
        subproj_m = re.search(r'data-subproject=["\']([^"\']*)["\']', anchor_tag)
        status_m = re.search(r'data-status=["\']([^"\']+)["\']', anchor_tag)
        type_m = re.search(r'data-task-type=["\']([^"\']+)["\']', anchor_tag)
        epic_m = re.search(r'data-epic=["\']([^"\']*)["\']', anchor_tag)
        epic_key, expected_epic = resolve_epic(epic_m.group(1) if epic_m else None, fm_data)

        # Also check for **Subproject:** in chunk if anchor tag didn't have data-subproject
        if not subproj_m:
            subproj_field_m = re.search(r'^\*\*Subproject:\*\*\s*([^\n]+)$', chunk, re.MULTILINE)
            subproj_val = subproj_field_m.group(1).strip() if subproj_field_m else None
        else:
            subproj_val = subproj_m.group(1).strip() if subproj_m.group(1).strip() else None

        proj_raw = proj_m.group(1).strip() if proj_m else "shared"
        status_raw = status_m.group(1).strip() if status_m else "triage"
        type_raw = type_m.group(1).strip() if type_m else "feature"

        proj_key = project_value_map.get(proj_raw.lower(), proj_raw.lower())
        status_key = status_value_map.get(status_raw.lower(), status_raw.lower())
        type_key = type_value_map.get(type_raw.lower(), type_raw.lower())

        expected_proj = project_map.get(proj_key, project_map.get(proj_raw.lower(), proj_raw.capitalize()))
        expected_status = status_map.get(status_key, status_map.get(status_raw.lower(), status_raw.capitalize()))
        expected_type = type_map.get(type_key, type_map.get(type_raw.lower(), type_raw.capitalize()))

        subproj_key = None
        expected_subproj = None
        if subproj_val and subproj_val.lower() not in ("", "none", "null"):
            subproj_key = subproject_value_map.get(subproj_val.lower(), subproj_val.lower())
            expected_subproj = subproject_map.get(subproj_key, subproject_map.get(subproj_val.lower(), subproj_val.replace("-", " ").replace("_", " ").title()))

        title_m = re.search(r'^###\s+(.+)$', chunk, re.MULTILINE)
        title = title_m.group(1).strip() if title_m else f"Task {num}"

        normalized_id = format_task_id(num, prefix, digit_width)
        normalized_aid = normalized_id.lower()
        anchor_replacements[anchor_match.group(2).lower()] = normalized_aid

        if subproj_key:
            anchor_tag_str = f'<a id="{normalized_aid}" class="task" data-project="{proj_key}" data-subproject="{subproj_key}" data-status="{status_key}" data-task-type="{type_key}"></a>'
            subproj_line = f"**Subproject:** {expected_subproj}\n"
        else:
            anchor_tag_str = f'<a id="{normalized_aid}" class="task" data-project="{proj_key}" data-status="{status_key}" data-task-type="{type_key}"></a>'
            subproj_line = ""

        if epic_key:
            anchor_tag_str = anchor_tag_str.replace('></a>', f' data-epic="{epic_key}"></a>')
        epic_line = f"**Epic:** {expected_epic}\n" if epic_key else ""

        new_header_block = (
            f'{anchor_tag_str}\n'
            f"### {title}\n"
            f"**ID:** {normalized_id}\n"
            f"**Project:** {expected_proj}\n"
            f"{subproj_line}"
            f"{epic_line}"
            f"**Status:** {expected_status}\n"
            f"**Type:** {expected_type}"
        )

        if re.search(header_block_pattern, chunk):
            updated_chunk = re.sub(header_block_pattern, new_header_block, chunk, count=1)
        else:
            # Fallback if header spacing or fields differed slightly
            rest = chunk.split("**Type:**", 1)[1].lstrip().split("\n", 1)[1] if "**Type:**" in chunk else ""
            updated_chunk = f"{new_header_block}\n\n{rest}".rstrip()

        updated_detailed_chunks.append(updated_chunk)

        updated_task_records.append({
            "num": num,
            "id": normalized_id,
            "aid": normalized_aid,
            "title": title,
            "project": expected_proj,
            "subproject": expected_subproj or "",
            "epic": expected_epic,
            "status": expected_status,
            "type": expected_type,
        })

    # Assemble sorted, updated Detailed Tasks section
    sorted_detailed_tasks = "\n\n".join(updated_detailed_chunks)

    # 5. Rebuild Summary Table (Rendered from the sorted, updated task details)
    has_subprojects = any(bool(t.get("subproject")) for t in updated_task_records)
    if has_subprojects:
        summary_headers = ["ID", "Title", "Project", "Subproject", "Status", "Type"]
        alignments = [":---", ":---", ":---", ":---", ":---", ":---"]
    else:
        summary_headers = ["ID", "Title", "Project", "Status", "Type"]
        alignments = [":---", ":---", ":---", ":---", ":---"]
    has_epics = bool(epic_list)
    if has_epics:
        summary_headers.insert(-2, "Epic")
        alignments.insert(-2, ":---")

    summary_rows = []

    for task in updated_task_records:
        clean_title = task["title"]
        while (clean_title.startswith("**") and clean_title.endswith("**")) or (
            clean_title.startswith("*") and clean_title.endswith("*")
        ):
            if clean_title.startswith("**") and clean_title.endswith("**"):
                clean_title = clean_title[2:-2].strip()
            elif clean_title.startswith("*") and clean_title.endswith("*"):
                clean_title = clean_title[1:-1].strip()

        st_lower = task["status"].lower()
        if st_lower == "pending":
            formatted_title = f"**{clean_title}**"
        elif st_lower in ("triage", "planning"):
            formatted_title = f"*{clean_title}*"
        else:
            formatted_title = clean_title

        formatted_type = f"[{task['type']}](#{task['aid']})"
        row = [task["id"], formatted_title, task["project"]]
        if has_subprojects:
            row.append(task["subproject"])
        if has_epics:
            row.append(task["epic"])
        row.extend([task["status"], formatted_type])
        summary_rows.append(row)

    new_summary_table = build_markdown_table(
        summary_headers, summary_rows, alignments=alignments
    ).rstrip("\n")

    # 8. Re-render metadata derived enum tables at end of document
    end_text = text[notes_start:]

    # Projects table: Value | Label | Prefix | Path
    project_headers = ["Value", "Label", "Prefix", "Path"]
    project_rows = [
        [p.get("value", ""), p.get("label", ""), p.get("prefix", ""), p.get("path", "")]
        for p in project_list
    ]
    projects_table = build_markdown_table(project_headers, project_rows, [":---", ":---", ":---", ":---"]).strip()

    # Statuses table: Value | Label | Description
    status_headers = ["Value", "Label", "Description"]
    status_rows = [
        [s.get("value", ""), s.get("label", ""), s.get("description", "")]
        for s in status_list
    ]
    statuses_table = build_markdown_table(status_headers, status_rows, [":---", ":---", ":---"]).strip()

    # Types table: Value | Label | Prefix | Description
    type_headers = ["Value", "Label", "Prefix", "Description"]
    type_rows = [
        [t.get("value", ""), t.get("label", ""), t.get("prefix", ""), t.get("description", "")]
        for t in type_list
    ]
    types_table = build_markdown_table(type_headers, type_rows, [":---", ":---", ":---", ":---"]).strip()

    # Subprojects table if present in frontmatter
    subproject_section = ""
    if subproject_list:
        subproject_headers = ["Value", "Label", "Description"]
        subproject_rows = [
            [sp.get("value", ""), sp.get("label", ""), sp.get("description", "")]
            for sp in subproject_list
        ]
        subprojects_table = build_markdown_table(subproject_headers, subproject_rows, [":---", ":---", ":---"]).strip()
        subproject_section = f"\n\n#### Subprojects (Rendered from frontmatter subprojects)\n{subprojects_table}"

    epic_section = ""
    if epic_list:
        epic_rows = [[e.get("value", ""), e.get("label", ""), e.get("description", "")] for e in epic_list]
        epics_table = build_markdown_table(["Value", "Label", "Description"], epic_rows).strip()
        epic_section = f"\n\n#### Epics (Rendered from frontmatter epics)\n{epics_table}"

    # Replace enum tables in end_text
    enum_tables_pattern = r"(#### Projects \(Rendered from frontmatter projects\)\n).*?(<!-- Table Sort Injection -->)"
    new_enum_tables = (
        "#### Projects (Rendered from frontmatter projects)\n"
        f"{projects_table}\n\n"
        "#### Task Statuses (Rendered from frontmatter task-statuses)\n"
        f"{statuses_table}\n\n"
        "#### Task Types (Rendered from frontmatter task-types)\n"
        f"{types_table}"
        f"{subproject_section}{epic_section}\n\n"
        r"\2"
    )

    updated_end_text = re.sub(enum_tables_pattern, new_enum_tables, end_text, flags=re.DOTALL)

    # 9. Assemble full document
    new_full_text = (
        part_a
        + new_summary_table
        + part_c
        + "\n"
        + sorted_detailed_tasks
        + "\n\n"
        + updated_end_text.lstrip("\n")
    )
    new_full_text = re.sub(
        r'(\]\(|href=["\']|^\[[^\]\n]+\]:[ \t]*)#([a-z][a-z0-9_-]*-\d+)(?=[\s)"\']|$)',
        lambda m: m.group(1) + "#" + anchor_replacements.get(m.group(2).lower(), m.group(2)),
        new_full_text,
        flags=re.IGNORECASE | re.MULTILINE,
    )

    with open(tasks_path, "w", encoding="utf-8") as f:
        f.write(new_full_text)

    return len(updated_task_records)


def main():
    import argparse
    parser = argparse.ArgumentParser(description="Synchronize and update Documentation/tasks.md")
    parser.add_argument(
        "-f", "--file",
        dest="file",
        default=None,
        help="Path to tasks.md (defaults to searching workspace)"
    )
    args = parser.parse_args()

    try:
        tasks_file = find_tasks_file(args.file)
        count = update_tasks_document(tasks_file)
        print(f"Successfully synchronized and updated {count} tasks in {tasks_file}")
    except Exception as e:
        print(f"Error updating tasks: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
