"""
jira_workitem.py — Jira Cloud REST API v3 calls for the QA Jira Manager.

All functions use get_auth_headers() from shared_jira/auth.py — no credentials here.

Public API:
  fetch_work_item(cloud_id, issue_key)                    -> dict
  create_test_case(cloud_id, project_key, title, steps, priority, labels, epic_link) -> dict
  link_test_cases_to_issue(cloud_id, issue_key, tc_keys)  -> dict
  get_linked_test_cases(cloud_id, issue_key)              -> dict
  attach_gherkin_to_issue(cloud_id, issue_key, gherkin_content, file_name) -> dict

Jira REST v3 base: https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3
"""
from __future__ import annotations

import re
from typing import Dict, List, Optional

import requests

from stlc_agents.shared_jira.auth import get_auth_headers, get_cloud_id

_API_VERSION = "3"


def _base(cloud_id: str) -> str:
    return f"https://api.atlassian.com/ex/jira/{cloud_id}/rest/api/{_API_VERSION}"


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

def fetch_work_item(cloud_id: str, issue_key: str) -> dict:
    """Fetch a Jira issue with all relevant fields.

    Returns a normalised work_item dict plus:
      - parent (Epic or parent Story)
      - existing_test_cases_count  (issues linked via 'is tested by')
      - coverage_hints             (derived from description + acceptance criteria)
    """
    headers = get_auth_headers()
    url = f"{_base(cloud_id)}/issue/{issue_key}"
    resp = requests.get(
        url,
        headers=headers,
        params={
            "expand": "renderedFields,names,transitions",
            "fields": (
                "summary,description,issuetype,status,priority,assignee,"
                "labels,customfield_10014,customfield_10016,customfield_10006,"
                "customfield_10010,customfield_10008,customfield_10028,"
                "parent,subtasks,issuelinks,project,fixVersions,components,"
                "customfield_10000,customfield_10001,customfield_10003,"
                "customfield_10004,customfield_10005,comment"
            ),
        },
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    fields = data.get("fields", {})

    issue_type = (fields.get("issuetype") or {}).get("name", "")

    # Epic guard — Epics are not supported for test case operations
    if issue_type == "Epic":
        return {
            "error": "epic_use_hierarchy",
            "issue_type": issue_type,
            "issue_key": issue_key,
            "message": (
                f"{issue_key} is an Epic. Epic-scoped operations are not supported. "
                "Please provide a child Story, Task, or Bug key instead."
            ),
        }

    description_text = _render_doc(fields.get("description") or {})
    acceptance_criteria = _render_doc(
        fields.get("customfield_10028") or {}  # common Acceptance Criteria custom field
    )

    wi = {
        "key": data["key"],
        "id": data["id"],
        "type": issue_type,
        "summary": fields.get("summary", ""),
        "description": description_text,
        "acceptance_criteria": acceptance_criteria,
        "status": (fields.get("status") or {}).get("name", ""),
        "priority": (fields.get("priority") or {}).get("name", ""),
        "labels": fields.get("labels", []),
        "project_key": (fields.get("project") or {}).get("key", ""),
        "epic_key": fields.get("customfield_10014", ""),  # Epic Link
    }

    # Parent issue (Story → Epic hierarchy)
    parent_issue = None
    parent_raw = fields.get("parent")
    if parent_raw:
        parent_issue = {
            "key": parent_raw.get("key", ""),
            "id": parent_raw.get("id", ""),
            "type": (parent_raw.get("fields", {}).get("issuetype") or {}).get("name", ""),
            "summary": parent_raw.get("fields", {}).get("summary", ""),
        }

    # Existing "is tested by" links
    existing_tc_count = sum(
        1 for link in (fields.get("issuelinks") or [])
        if (link.get("type") or {}).get("name", "") in ("Test", "is tested by", "tested by")
        and "inwardIssue" in link
    )

    coverage_hints = _extract_coverage_hints(wi)

    return {
        "work_item": wi,
        "parent_issue": parent_issue,
        "existing_test_cases_count": existing_tc_count,
        "coverage_hints": coverage_hints,
    }


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

def create_test_case(
    cloud_id: str,
    project_key: str,
    summary: str,
    steps: List[Dict],
    priority: str = "Medium",
    labels: Optional[List[str]] = None,
    epic_link: Optional[str] = None,
) -> dict:
    """Create a Jira issue of type 'Test' (or 'Task' if Test type unavailable).

    Steps are stored as a formatted description table since Jira core does not
    have a native step field — this is compatible with both Jira Software and
    Xray-free environments.

    Priority values: Highest, High, Medium, Low, Lowest
    """
    headers = get_auth_headers()

    # Build step table in Atlassian Document Format (ADF)
    description_adf = _build_steps_adf(steps)

    payload: dict = {
        "fields": {
            "project": {"key": project_key},
            "summary": summary,
            "issuetype": {"name": "Test"},
            "priority": {"name": priority},
            "description": description_adf,
        }
    }
    if labels:
        payload["fields"]["labels"] = labels
    if epic_link:
        payload["fields"]["customfield_10014"] = epic_link  # Epic Link

    resp = requests.post(
        f"{_base(cloud_id)}/issue",
        headers=headers,
        json=payload,
        timeout=30,
    )

    # If 'Test' issue type doesn't exist, fall back to 'Task'
    if resp.status_code == 400 and "issuetype" in resp.text.lower():
        payload["fields"]["issuetype"] = {"name": "Task"}
        payload["fields"]["labels"] = list(set((labels or []) + ["qa-test-case"]))
        resp = requests.post(
            f"{_base(cloud_id)}/issue",
            headers=get_auth_headers(),
            json=payload,
            timeout=30,
        )

    resp.raise_for_status()
    created = resp.json()
    return {
        "success": True,
        "issue_key": created["key"],
        "issue_id": created["id"],
        "summary": summary,
        "url": f"https://your-domain.atlassian.net/browse/{created['key']}",
    }


# ---------------------------------------------------------------------------
# link_test_cases_to_issue
# ---------------------------------------------------------------------------

def link_test_cases_to_issue(
    cloud_id: str,
    issue_key: str,
    test_case_keys: List[str],
) -> dict:
    """Create 'is tested by' / 'Test' links from an issue to test case issues."""
    headers = get_auth_headers()
    linked = []
    failed = []

    for tc_key in test_case_keys:
        payload = {
            "type": {"name": "Test"},  # standard Jira link type; falls back gracefully
            "inwardIssue": {"key": issue_key},
            "outwardIssue": {"key": tc_key},
        }
        resp = requests.post(
            f"{_base(cloud_id)}/issueLink",
            headers=get_auth_headers(),
            json=payload,
            timeout=30,
        )
        if resp.status_code == 404:
            # Try generic "Relates" link type
            payload["type"] = {"name": "Relates"}
            resp = requests.post(
                f"{_base(cloud_id)}/issueLink",
                headers=get_auth_headers(),
                json=payload,
                timeout=30,
            )
        if resp.ok or resp.status_code == 201:
            linked.append(tc_key)
        else:
            failed.append({"key": tc_key, "status": resp.status_code, "error": resp.text[:200]})

    return {
        "success": len(failed) == 0,
        "issue_key": issue_key,
        "linked_count": len(linked),
        "linked_keys": linked,
        "failed": failed,
    }


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

def get_linked_test_cases(cloud_id: str, issue_key: str) -> dict:
    """Return all issues linked to this issue via a Test/tested-by link type."""
    headers = get_auth_headers()
    resp = requests.get(
        f"{_base(cloud_id)}/issue/{issue_key}",
        headers=headers,
        params={"fields": "issuelinks,summary"},
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()

    test_link_names = {"test", "is tested by", "tested by", "relates to"}
    test_cases = []
    for link in (data.get("fields", {}).get("issuelinks") or []):
        link_type = (link.get("type") or {}).get("name", "").lower()
        if link_type in test_link_names:
            related = link.get("inwardIssue") or link.get("outwardIssue")
            if related:
                rf = related.get("fields", {})
                test_cases.append({
                    "key": related.get("key", ""),
                    "id": related.get("id", ""),
                    "summary": rf.get("summary", ""),
                    "status": (rf.get("status") or {}).get("name", ""),
                    "priority": (rf.get("priority") or {}).get("name", ""),
                    "type": (rf.get("issuetype") or {}).get("name", ""),
                })

    return {
        "issue_key": issue_key,
        "test_cases": test_cases,
        "count": len(test_cases),
    }


# ---------------------------------------------------------------------------
# attach_gherkin_to_issue
# ---------------------------------------------------------------------------

def attach_gherkin_to_issue(cloud_id: str, issue_key: str, gherkin_content: str, file_name: str = "feature.feature") -> dict:
    """Attach a .feature file to a Jira issue.
    
    Args:
        cloud_id: Jira cloud ID
        issue_key: Issue key (e.g. PROJ-123)
        gherkin_content: Full Gherkin feature file content
        file_name: Name of the file to attach (default: feature.feature)
    
    Returns:
        dict with attachment metadata
    """
    headers = get_auth_headers()
    
    # Upload attachment
    url = f"{_base(cloud_id)}/issue/{issue_key}/attachments"
    files = {"file": (file_name, gherkin_content, "text/plain")}
    
    resp = requests.post(url, headers=headers, files=files, timeout=30)
    resp.raise_for_status()
    data = resp.json()
    
    attachment = (data.get("results") or [{}])[0] if data.get("results") else {}
    return {
        "issue_key": issue_key,
        "file_name": file_name,
        "attachment_id": attachment.get("id", ""),
        "success": bool(attachment.get("id")),
    }


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

def _render_doc(doc: dict) -> str:
    """Recursively extract plain text from an Atlassian Document Format node."""
    if not doc or not isinstance(doc, dict):
        return ""
    node_type = doc.get("type", "")
    text = ""
    if node_type == "text":
        text = doc.get("text", "")
    for child in doc.get("content", []):
        child_text = _render_doc(child)
        if node_type in ("paragraph", "listItem", "tableRow"):
            child_text += "\n"
        text += child_text
    return re.sub(r"\n{3,}", "\n\n", text).strip()


def _build_steps_adf(steps: List[Dict]) -> dict:
    """Build an ADF document representing test steps as a table."""
    if not steps:
        return {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "No steps defined."}],
                }
            ],
        }

    header_row = {
        "type": "tableRow",
        "content": [
            _th("Step #"),
            _th("Action"),
            _th("Expected Result"),
        ],
    }
    data_rows = [
        {
            "type": "tableRow",
            "content": [
                _td(str(i + 1)),
                _td(s.get("action", "")),
                _td(s.get("expected_result", "")),
            ],
        }
        for i, s in enumerate(steps)
    ]

    return {
        "type": "doc",
        "version": 1,
        "content": [
            {
                "type": "table",
                "attrs": {"isNumberColumnEnabled": False, "layout": "default"},
                "content": [header_row] + data_rows,
            }
        ],
    }


def _th(text: str) -> dict:
    return {
        "type": "tableHeader",
        "attrs": {},
        "content": [
            {
                "type": "paragraph",
                "content": [{"type": "text", "text": text, "marks": [{"type": "strong"}]}],
            }
        ],
    }


def _td(text: str) -> dict:
    return {
        "type": "tableCell",
        "attrs": {},
        "content": [
            {"type": "paragraph", "content": [{"type": "text", "text": text}]}
        ],
    }


def _extract_coverage_hints(wi: dict) -> list:
    text = " ".join([
        wi.get("description", ""),
        wi.get("acceptance_criteria", ""),
        wi.get("summary", ""),
    ]).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"),
        (["required", "invalid", "error", "validation", "must be"], "validation_errors"),
    ]
    return [hint for keywords, hint in checks if any(kw in text for kw in keywords)]
