"""
ado_workitem.py — Azure DevOps Work Item REST API calls for the Test Case Manager.

All functions use get_auth_headers() from auth.py — no credentials in this file.

Public API:
  fetch_work_item(org_url, project, work_item_id)        -> dict
  create_test_case(org_url, project, title, steps, ...)  -> dict
  link_test_cases_to_work_item(org_url, project, wi_id, tc_ids) -> dict
  get_linked_test_cases(org_url, project, work_item_id)  -> dict
  add_comment_to_work_item(org_url, project, work_item_id, text) -> dict
  add_tag_to_work_item(org_url, project, work_item_id, tag)      -> dict
"""
from __future__ import annotations

import json
import xml.etree.ElementTree as ET
from typing import Dict, List, Optional

import requests

from stlc_agents.shared.auth import get_auth_headers

_API = "7.1"


# ---------------------------------------------------------------------------
# fetch_work_item
# ---------------------------------------------------------------------------

def fetch_work_item(org_url: str, project: str, work_item_id: int) -> dict:
    """Fetch a PBI or Bug with all relevant fields and parent feature."""
    org_url = org_url.rstrip("/")
    headers = get_auth_headers()

    resp = requests.get(
        f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
        headers=headers,
        params={"$expand": "relations", "api-version": _API},
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    fields = data.get("fields", {})

    wi_type = fields.get("System.WorkItemType", "")

    # ── Epic guard — test cases must NEVER be created for Epics ─────────────
    if wi_type == "Epic":
        return {
            "error": "epic_not_supported",
            "work_item_type": wi_type,
            "work_item_id": work_item_id,
            "message": (
                f"Work item {work_item_id} is an Epic. "
                "Manual test cases cannot be created or linked to an Epic. "
                "Test cases in ADO are always scoped to PBIs, Bugs, or Features — never to Epics. "
                "Look up the child Features of this Epic in Azure DevOps, then call "
                "create_and_link_test_cases on each individual Feature or PBI/Bug work item ID."
            ),
        }

    wi = {
        "id": data["id"],
        "type": fields.get("System.WorkItemType", ""),
        "title": fields.get("System.Title", ""),
        "description": _strip_html(fields.get("System.Description", "") or ""),
        "acceptance_criteria": _strip_html(
            fields.get("Microsoft.VSTS.Common.AcceptanceCriteria", "") or ""
        ),
        "repro_steps": _strip_html(
            fields.get("Microsoft.VSTS.TCM.ReproSteps", "") or ""
        ),
        "state": fields.get("System.State", ""),
        "priority": fields.get("Microsoft.VSTS.Common.Priority", ""),
        "story_points": fields.get("Microsoft.VSTS.Scheduling.StoryPoints", ""),
        "tags": fields.get("System.Tags", ""),
        "assigned_to": (
            (fields.get("System.AssignedTo") or {}).get("displayName", "")
        ),
        "area_path": fields.get("System.AreaPath", ""),
        "iteration_path": fields.get("System.IterationPath", ""),
    }

    # Fetch parent feature if present
    parent_feature = None
    for rel in data.get("relations", []):
        if rel.get("rel") == "System.LinkTypes.Hierarchy-Reverse":
            try:
                pr = requests.get(
                    rel["url"] + f"?api-version={_API}",
                    headers=get_auth_headers(),
                    timeout=30,
                )
                if pr.ok:
                    pf = pr.json().get("fields", {})
                    parent_feature = {
                        "id": pr.json().get("id"),
                        "type": pf.get("System.WorkItemType", ""),
                        "title": pf.get("System.Title", ""),
                    }
            except Exception:
                pass
            break

    # Existing linked test cases count
    existing_tc_count = sum(
        1 for r in data.get("relations", [])
        if r.get("rel") == "Microsoft.VSTS.Common.TestedBy-Forward"
    )

    # Coverage hints derived from text
    coverage_hints = _extract_coverage_hints(wi)

    # Extract design links (Figma, Miro, etc.)
    design_links = _extract_design_links(wi)

    return {
        "work_item": wi,
        "parent_feature": parent_feature,
        "existing_test_cases_count": existing_tc_count,
        "coverage_hints": coverage_hints,
        "design_links": design_links,
    }


# ---------------------------------------------------------------------------
# create_test_case
# ---------------------------------------------------------------------------

def create_test_case(
    org_url: str,
    project: str,
    title: str,
    steps: List[Dict],
    priority: int = 2,
    area_path: Optional[str] = None,
    iteration_path: Optional[str] = None,
) -> dict:
    """Create a Test Case work item with XML steps in ADO."""
    org_url = org_url.rstrip("/")
    headers = get_auth_headers("application/json-patch+json")

    steps_xml = _build_steps_xml(steps)

    patch = [
        {"op": "add", "path": "/fields/System.Title", "value": title},
        {"op": "add", "path": "/fields/System.State", "value": "Design"},
        {"op": "add", "path": "/fields/Microsoft.VSTS.Common.Priority", "value": priority},
    ]
    if steps_xml:
        patch.append({"op": "add", "path": "/fields/Microsoft.VSTS.TCM.Steps", "value": steps_xml})
    if area_path:
        patch.append({"op": "add", "path": "/fields/System.AreaPath", "value": area_path})
    if iteration_path:
        patch.append({"op": "add", "path": "/fields/System.IterationPath", "value": iteration_path})

    resp = requests.post(
        f"{org_url}/{project}/_apis/wit/workitems/$Test%20Case",
        headers=headers,
        params={"api-version": _API},
        json=patch,
        timeout=30,
    )
    resp.raise_for_status()
    created = resp.json()
    return {
        "success": True,
        "test_case_id": created["id"],
        "title": title,
        "url": created.get("_links", {}).get("html", {}).get("href", ""),
    }


# ---------------------------------------------------------------------------
# link_test_cases_to_work_item
# ---------------------------------------------------------------------------

def link_test_cases_to_work_item(
    org_url: str,
    project: str,
    work_item_id: int,
    test_case_ids: List[int],
    link_comment: str | None = None,
) -> dict:
    """Create TestedBy-Forward links from a work item to test cases.

    link_comment is stored as attributes.comment on each relation and appears
    in the Links tab Comments column in Azure DevOps. Defaults to
    "Generated and linked by STLC-Agent - YYYY-MM-DD" so each batch
    self-identifies its run date.
    """
    if link_comment is None:
        from datetime import datetime, timezone
        link_comment = f"Generated and linked by STLC-Agent - {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"

    org_url = org_url.rstrip("/")
    headers = get_auth_headers("application/json-patch+json")

    relations = [
        {
            "op": "add",
            "path": "/relations/-",
            "value": {
                "rel": "Microsoft.VSTS.Common.TestedBy-Forward",
                "url": f"{org_url}/{project}/_apis/wit/workItems/{tc_id}",
                "attributes": {"comment": link_comment},
            },
        }
        for tc_id in test_case_ids
    ]

    resp = requests.patch(
        f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
        headers=headers,
        params={"api-version": _API},
        json=relations,
        timeout=30,
    )
    resp.raise_for_status()
    return {
        "success": True,
        "work_item_id": work_item_id,
        "linked_count": len(test_case_ids),
        "test_case_ids": test_case_ids,
    }


# ---------------------------------------------------------------------------
# get_linked_test_cases
# ---------------------------------------------------------------------------

def get_linked_test_cases(org_url: str, project: str, work_item_id: int) -> dict:
    """Return all test cases linked via TestedBy to a work item."""
    org_url = org_url.rstrip("/")
    headers = get_auth_headers()

    resp = requests.get(
        f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
        headers=headers,
        params={"$expand": "relations", "api-version": _API},
        timeout=30,
    )
    resp.raise_for_status()

    linked = []
    for rel in resp.json().get("relations", []):
        if rel.get("rel") == "Microsoft.VSTS.Common.TestedBy-Forward":
            tc_id = int(rel["url"].split("/")[-1])
            try:
                tc = requests.get(
                    rel["url"] + f"?api-version={_API}",
                    headers=get_auth_headers(),
                    timeout=30,
                )
                if tc.ok:
                    f = tc.json().get("fields", {})
                    linked.append({
                        "id": tc_id,
                        "title": f.get("System.Title", ""),
                        "state": f.get("System.State", ""),
                        "priority": f.get("Microsoft.VSTS.Common.Priority", ""),
                    })
            except Exception:
                linked.append({"id": tc_id})

    return {"work_item_id": work_item_id, "linked_test_cases": linked, "count": len(linked)}


# ---------------------------------------------------------------------------
# add_comment_to_work_item
# ---------------------------------------------------------------------------

def add_comment_to_work_item(org_url: str, project: str, work_item_id: int, text: str) -> dict:
    """Add a comment to a work item via the ADO comments API."""
    org_url = org_url.rstrip("/")
    headers = get_auth_headers()

    resp = requests.post(
        f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}/comments",
        headers=headers,
        params={"api-version": "7.1-preview.3"},
        json={"text": text},
        timeout=30,
    )
    resp.raise_for_status()
    return {"success": True, "comment_id": resp.json().get("id")}


# ---------------------------------------------------------------------------
# add_tag_to_work_item
# ---------------------------------------------------------------------------

def add_tag_to_work_item(org_url: str, project: str, work_item_id: int, tag: str) -> dict:
    """Append a tag to a work item's System.Tags field (no-op if already present)."""
    org_url = org_url.rstrip("/")

    fetch_resp = requests.get(
        f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
        headers=get_auth_headers(),
        params={"api-version": _API},
        timeout=30,
    )
    fetch_resp.raise_for_status()
    existing_str = fetch_resp.json().get("fields", {}).get("System.Tags", "") or ""
    existing = [t.strip() for t in existing_str.split(";") if t.strip()]

    if tag in existing:
        return {"success": True, "tag": tag, "already_present": True}

    existing.append(tag)
    new_tags_str = "; ".join(existing)

    patch = [{"op": "add", "path": "/fields/System.Tags", "value": new_tags_str}]
    patch_resp = requests.patch(
        f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
        headers=get_auth_headers("application/json-patch+json"),
        params={"api-version": _API},
        json=patch,
        timeout=30,
    )
    patch_resp.raise_for_status()
    return {"success": True, "tag": tag, "tags": new_tags_str}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _build_steps_xml(steps: List[Dict]) -> str:
    if not steps:
        return ""

    def esc(s: str) -> str:
        return (s or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")

    inner = "".join(
        f'<step id="{i+1}" type="ValidateStep">'
        f'<parameterizedString isformatted="false">{esc(s.get("action",""))}</parameterizedString>'
        f'<parameterizedString isformatted="false">{esc(s.get("expected_result",""))}</parameterizedString>'
        f"<description/></step>"
        for i, s in enumerate(steps)
    )
    return f'<steps id="0" last="{len(steps)}">{inner}</steps>'


def _strip_html(html: str) -> str:
    if not html:
        return ""
    import re
    text = re.sub(r"<br\s*/?>", "\n", html, flags=re.IGNORECASE)
    text = re.sub(r"</p>|</li>|</tr>", "\n", text, flags=re.IGNORECASE)
    text = re.sub(r"<li[^>]*>", "• ", text, flags=re.IGNORECASE)
    text = re.sub(r"<[^>]+>", "", text)
    for old, new in [("&nbsp;", " "), ("&amp;", "&"), ("&lt;", "<"), ("&gt;", ">"), ("&quot;", '"')]:
        text = text.replace(old, new)
    return re.sub(r"\n{3,}", "\n\n", text).strip()


def _extract_coverage_hints(wi: dict) -> list:
    text = " ".join([wi.get("description", ""), wi.get("acceptance_criteria", "")]).lower()
    checks = [
        (["toast", "notification", "success message", "confirmation"], "toast_notifications"),
        (["mb", "kb", "size limit", "file size", "maximum size"], "file_size_boundary"),
        (["display", "shows", "reflects", "after upload", "after save", "updated"], "post_action_state"),
        (["mobile", "webview", "ios", "android"], "platform_specific"),
        (["initials", "first name", "last name", "derived", "generated"], "computed_values"),
        (["refresh", "reload", "re-login", "persist", "retained"], "data_persistence"),
        (["tab", "keyboard", "accessible", "aria", "wcag", "a11y"], "accessibility"),
        (["crop", "resize", "zoom", "drag", "rotate"], "image_manipulation"),
        (["format", "png", "jpg", "jpeg", "webp", "svg"], "file_formats"),
        (["cancel", "close", "discard", "dismiss"], "cancel_flows"),
    ]
    return [hint for keywords, hint in checks if any(kw in text for kw in keywords)]


def _extract_design_links(wi: dict) -> dict:
    """Extract design/mockup links from work item description and acceptance criteria.

    Returns a dict with link types:
      - figma: list of Figma URLs
      - miro: list of Miro URLs
      - other: list of other design tool URLs
    """
    import re

    text = " ".join([
        wi.get("description", ""),
        wi.get("acceptance_criteria", "")
    ])

    # Regex patterns for different design tools
    figma_pattern = r'https?://(?:www\.)?figma\.com/(?:file|design|proto)/[^\s<>"\')]+[^\s<>"\').,;!?]+'
    miro_pattern = r'https?://(?:www\.)?miro\.com/app/board/[^\s<>"\')]+[^\s<>"\').,;!?]+'
    sketch_pattern = r'https?://(?:www\.)?sketch\.com/s/[^\s<>"\')]+[^\s<>"\').,;!?]+'
    invision_pattern = r'https?://(?:www\.)?invisionapp\.com/[^\s<>"\')]+[^\s<>"\').,;!?]+'

    figma_links = list(set(re.findall(figma_pattern, text, re.IGNORECASE)))
    miro_links = list(set(re.findall(miro_pattern, text, re.IGNORECASE)))
    sketch_links = list(set(re.findall(sketch_pattern, text, re.IGNORECASE)))
    invision_links = list(set(re.findall(invision_pattern, text, re.IGNORECASE)))

    result = {}
    if figma_links:
        result["figma"] = figma_links
    if miro_links:
        result["miro"] = miro_links
    if sketch_links or invision_links:
        result["other"] = sketch_links + invision_links

    return result
