#!/usr/bin/env python3
"""jira-cli — Jira issue tracker from the terminal.

Interact with Atlassian Jira Cloud via the REST API v3.
Requires JIRA_EMAIL and JIRA_API_TOKEN env vars (token from
https://id.atlassian.com/manage/api-tokens) and JIRA_SERVER
(defaults to https://your-domain.atlassian.net).
"""

import argparse
import json
import os
import sys
import warnings
from typing import Any, Dict, List, Optional, Tuple

warnings.simplefilter("ignore")

import requests

# === Config ===
DEFAULT_SERVER = "https://your-domain.atlassian.net"
ENV_EMAIL = os.getenv("JIRA_EMAIL", "")
ENV_TOKEN = os.getenv("JIRA_API_TOKEN", "")
ENV_SERVER = os.getenv("JIRA_SERVER", DEFAULT_SERVER)

# === Logging ===
QUIET = False


def log(msg: str) -> None:
    if not QUIET and not GLOBAL_FLAGS.get("json", False):
        print(msg)


def warn(msg: str) -> None:
    print(f"Warning: {msg}", file=sys.stderr)


def die(msg: str, exit_code: int = 1) -> None:
    print(f"Error: {msg}", file=sys.stderr)
    sys.exit(exit_code)


def emit(human: str, data: Any) -> None:
    if GLOBAL_FLAGS.get("json", False):
        print(json.dumps(data, default=str))
    else:
        print(human)


# === Global flags ===
GLOBAL_FLAGS: Dict[str, Any] = {
    "json": False, "dry_run": False, "force": False,
    "quiet": False, "verbose": False
}


def _preparse_global_flags(argv: List[str]) -> Tuple[Dict[str, Any], List[str]]:
    GLOBAL_BOOLS = {"--json", "--dry-run", "--force", "--quiet", "--verbose"}
    flags: Dict[str, Any] = {}
    filtered: List[str] = [argv[0]]
    i = 1
    while i < len(argv):
        arg = argv[i]
        if arg in GLOBAL_BOOLS:
            flags[arg.lstrip("-").replace("-", "_")] = True
            i += 1
        elif arg in ("--help", "-h"):
            return flags, argv
        elif arg == "--":
            filtered.extend(argv[i:])
            break
        else:
            filtered.append(arg)
            i += 1
    return flags, filtered


# === Jira API Client ===
class JiraClient:
    """REST API client for Jira Cloud v3."""

    def __init__(self, email: str = "", token: str = "",
                 server: str = "", dry_run: bool = False):
        self.email = email or ENV_EMAIL
        self.token = token or ENV_TOKEN
        self.server = (server or ENV_SERVER).rstrip("/")
        self.dry_run = dry_run

    def _headers(self) -> Dict[str, str]:
        return {
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

    def _auth(self) -> Optional[Tuple[str, str]]:
        if self.email and self.token:
            return (self.email, self.token)
        return None

    def _request(self, method: str, path: str,
                 params: Optional[Dict] = None,
                 json_data: Any = None) -> Any:
        url = f"{self.server}/rest/api/3{path}"

        if self.dry_run:
            return {"dry_run": True, "method": method.upper(),
                    "url": url, "params": params, "json": json_data}

        auth = self._auth()
        if not auth:
            die("JIRA_EMAIL and JIRA_API_TOKEN not set. "
                "Get a token at https://id.atlassian.com/manage/api-tokens")

        try:
            resp = requests.request(
                method=method, url=url, auth=auth,
                params=params, json=json_data,
                headers=self._headers(), timeout=30
            )
        except requests.ConnectionError as e:
            die(f"Cannot connect to {self.server}: {e}\n"
                f"  Check JIRA_SERVER or use --server")

        if resp.status_code == 401:
            die("Auth failed (401). Check your JIRA_EMAIL and JIRA_API_TOKEN.")
        if resp.status_code == 403:
            die("Forbidden (403). Your account may not have access to this resource.")
        if resp.status_code == 404:
            return None
        if resp.status_code >= 400:
            try:
                detail = resp.json()
            except Exception:
                detail = resp.text[:200]
            die(f"API error ({resp.status_code}): {detail}")

        try:
            return resp.json()
        except ValueError:
            return {"raw": resp.text[:500]}

    def _get(self, path: str, params: Optional[Dict] = None) -> Any:
        return self._request("GET", path, params=params)

    def _post(self, path: str, json_data: Any = None) -> Any:
        return self._request("POST", path, json_data=json_data)

    def _put(self, path: str, json_data: Any = None) -> Any:
        return self._request("PUT", path, json_data=json_data)

    # === Endpoints ===

    def get_myself(self) -> Any:
        return self._get("/myself")

    def search_issues(self, jql: str, fields: str = "summary,status,issuetype,assignee,priority",
                      max_results: int = 20) -> Any:
        return self._get("/search", params={
            "jql": jql,
            "fields": fields,
            "maxResults": max_results
        })

    def get_issue(self, issue_key: str) -> Any:
        return self._get(f"/issue/{issue_key}")

    def list_projects(self) -> Any:
        return self._get("/project")

    def create_issue(self, project_key: str, summary: str,
                     issuetype: str = "Task", description: str = "",
                     priority: Optional[str] = None) -> Any:
        fields = {
            "project": {"key": project_key},
            "summary": summary,
            "issuetype": {"name": issuetype},
        }
        if description:
            fields["description"] = {
                "type": "doc",
                "version": 1,
                "content": [{
                    "type": "paragraph",
                    "content": [{"type": "text", "text": description}]
                }]
            }
        if priority:
            fields["priority"] = {"name": priority}
        return self._post("/issue", json_data={"fields": fields})

    def add_comment(self, issue_key: str, body: str) -> Any:
        return self._post(f"/issue/{issue_key}/comment", json_data={
            "body": {
                "type": "doc",
                "version": 1,
                "content": [{
                    "type": "paragraph",
                    "content": [{"type": "text", "text": body}]
                }]
            }
        })

    def get_transitions(self, issue_key: str) -> Any:
        return self._get(f"/issue/{issue_key}/transitions")

    def transition_issue(self, issue_key: str, transition_id: str) -> Any:
        return self._post(f"/issue/{issue_key}/transitions",
                          json_data={"transition": {"id": transition_id}})


# === Command Handlers ===

def cmd_me(client: JiraClient, args: List[str]) -> None:
    if client.dry_run:
        emit("[dry-run] Would fetch current user profile",
             {"dry_run": True, "command": "me"})
        return
    data = client.get_myself()
    if not data:
        emit("Could not fetch user info.", {"error": "not found"})
        return
    emit(
        f"👤 {data.get('displayName', '?')} ({data.get('emailAddress', '?')})\n"
        f"   Account: {data.get('accountId', '?')}\n"
        f"   Timezone: {data.get('timeZone', '?')}",
        {"displayName": data.get("displayName"),
         "emailAddress": data.get("emailAddress"),
         "accountId": data.get("accountId"),
         "timeZone": data.get("timeZone"),
         "locale": data.get("locale")}
    )


def cmd_list(client: JiraClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="jira-cli list")
    parser.add_argument("--jql", default="", help="JQL query (default: recent issues)")
    parser.add_argument("--max", type=int, default=20, help="Max results (default: 20)")
    parser.add_argument("--project", help="Shortcut: filter by project key")
    parsed, _ = parser.parse_known_args(args)

    jql = parsed.jql
    if not jql and parsed.project:
        jql = f"project={parsed.project} ORDER BY created DESC"
    elif not jql:
        jql = "ORDER BY created DESC"

    if client.dry_run:
        emit(f"[dry-run] Would search issues: {jql}",
             {"dry_run": True, "command": "list", "jql": jql,
              "max_results": parsed.max})
        return

    data = client.search_issues(jql, max_results=parsed.max)
    issues = data.get("issues", []) if data else []

    if not issues:
        emit("No issues found.", {"issues": []})
        return

    lines = []
    out = []
    for issue in issues:
        key = issue.get("key", "?")
        fields = issue.get("fields", {})
        summary = fields.get("summary", "?")
        status = fields.get("status", {}).get("name", "?")
        assignee = fields.get("assignee", {})
        assignee_name = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
        lines.append(f"  {key} [{status}] {summary}  ({assignee_name})")
        out.append({
            "key": key, "summary": summary, "status": status,
            "assignee": assignee_name,
            "issuetype": fields.get("issuetype", {}).get("name", "?"),
            "priority": fields.get("priority", {}).get("name") if fields.get("priority") else None,
        })

    total = data.get("total", len(issues))
    emit(f"{total} issue(s):\n" + "\n".join(lines),
         {"total": total, "issues": out})


def cmd_view(client: JiraClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="jira-cli view")
    parser.add_argument("issue_key", help="Issue key (e.g. PROJ-123)")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would fetch issue {parsed.issue_key}",
             {"dry_run": True, "command": "view",
              "issue_key": parsed.issue_key})
        return

    data = client.get_issue(parsed.issue_key)
    if not data:
        emit(f"Issue {parsed.issue_key} not found.",
             {"error": "not found", "issue_key": parsed.issue_key})
        return

    fields = data.get("fields", {})
    key = data.get("key", parsed.issue_key)
    summary = fields.get("summary", "?")
    status = fields.get("status", {}).get("name", "?")
    issuetype = fields.get("issuetype", {}).get("name", "?")
    priority = fields.get("priority", {})
    priority_str = priority.get("name", "None") if priority else "None"
    assignee = fields.get("assignee", {})
    assignee_str = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
    reporter = fields.get("reporter", {})
    reporter_str = reporter.get("displayName", "?") if reporter else "?"
    created = fields.get("created", "?")
    updated = fields.get("updated", "?")
    description = fields.get("description", "")
    desc_text = _extract_text(description) if description else "(no description)"

    human = (
        f"📋 {key}: {summary}\n"
        f"   Type: {issuetype}  Status: {status}  Priority: {priority_str}\n"
        f"   Assignee: {assignee_str}  Reporter: {reporter_str}\n"
        f"   Created: {created}\n"
        f"   Updated: {updated}\n"
        f"─── Description ───\n{desc_text}"
    )
    emit(human, {
        "key": key, "summary": summary, "status": status,
        "issuetype": issuetype, "priority": priority_str,
        "assignee": assignee_str, "reporter": reporter_str,
        "created": created, "updated": updated,
        "description": desc_text
    })


def _extract_text(adf: Any) -> str:
    """Extract plain text from Atlassian Document Format."""
    if isinstance(adf, str):
        return adf
    if isinstance(adf, dict):
        content = adf.get("content", [])
        texts = []
        for node in content:
            if isinstance(node, dict):
                node_content = node.get("content", [])
                for item in node_content:
                    if isinstance(item, dict) and item.get("type") == "text":
                        texts.append(item.get("text", ""))
        return "\n".join(texts) if texts else json.dumps(adf, indent=2)[:500]
    return str(adf)[:500]


def cmd_projects(client: JiraClient, args: List[str]) -> None:
    if client.dry_run:
        emit("[dry-run] Would list projects",
             {"dry_run": True, "command": "projects"})
        return
    data = client.list_projects()
    if not data:
        emit("No projects found.", {"projects": []})
        return

    projects = data if isinstance(data, list) else []
    lines = []
    out = []
    for p in sorted(projects, key=lambda x: x.get("name", "")):
        key = p.get("key", "?")
        name = p.get("name", "?")
        lead = p.get("lead", {})
        lead_name = lead.get("displayName", "?") if lead else "?"
        ptype = p.get("projectTypeKey", "?")
        lines.append(f"  {key:12} {name:40} [{ptype}]  Lead: {lead_name}")
        out.append({
            "key": key, "name": name, "type": ptype,
            "lead": lead_name
        })
    emit(f"{len(projects)} project(s):\n" + "\n".join(lines),
         {"total": len(projects), "projects": out})


def cmd_create(client: JiraClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="jira-cli create")
    parser.add_argument("--project", required=True, help="Project key (e.g. PROJ)")
    parser.add_argument("--summary", required=True, help="Issue summary/title")
    parser.add_argument("--type", default="Task", help="Issue type (Task, Bug, Story, etc.)")
    parser.add_argument("--description", default="", help="Issue description")
    parser.add_argument("--priority", help="Priority (Highest, High, Medium, Low, Lowest)")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit("[dry-run] Would create issue",
             {"dry_run": True, "command": "create",
              "project": parsed.project, "summary": parsed.summary,
              "type": parsed.type})
        return

    result = client.create_issue(
        project_key=parsed.project,
        summary=parsed.summary,
        issuetype=parsed.type,
        description=parsed.description,
        priority=parsed.priority
    )
    key = result.get("key", "?") if result else "?"
    url = result.get("self", "") if result else ""
    emit(f"✅ Created {key}: {parsed.summary}\n   {url}",
         {"status": "created", "key": key, "url": url,
          "summary": parsed.summary})


def cmd_comment(client: JiraClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="jira-cli comment")
    parser.add_argument("issue_key", help="Issue key (e.g. PROJ-123)")
    parser.add_argument("--body", "-m", required=True, help="Comment body text")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would add comment to {parsed.issue_key}",
             {"dry_run": True, "command": "comment",
              "issue_key": parsed.issue_key, "body": parsed.body})
        return

    result = client.add_comment(parsed.issue_key, parsed.body)
    cid = result.get("id", "?") if result else "?"
    emit(f"💬 Comment added to {parsed.issue_key} (id={cid})",
         {"status": "comment_added", "issue_key": parsed.issue_key,
          "comment_id": cid})


def cmd_transition(client: JiraClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="jira-cli transition")
    parser.add_argument("issue_key", help="Issue key (e.g. PROJ-123)")
    parser.add_argument("--to", required=True, help="Transition by name or ID")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would transition {parsed.issue_key} to '{parsed.to}'",
             {"dry_run": True, "command": "transition",
              "issue_key": parsed.issue_key, "to": parsed.to})
        return

    # First, get available transitions
    trans_data = client.get_transitions(parsed.issue_key)
    if not trans_data:
        die(f"No transitions available for {parsed.issue_key}")
    transitions = trans_data.get("transitions", [])

    # Find the target transition by name or ID
    match = None
    for t in transitions:
        tid = t.get("id", "")
        tname = t.get("name", "").lower()
        if tid == parsed.to or tname == parsed.to.lower():
            match = tid
            break

    if not match:
        available = ", ".join([f"{t.get('id')}={t.get('name')}"
                               for t in transitions])
        die(f"Transition '{parsed.to}' not found. Available: {available}")

    result = client.transition_issue(parsed.issue_key, match)
    emit(f"🔄 {parsed.issue_key} transitioned to '{parsed.to}'",
         {"status": "transitioned", "issue_key": parsed.issue_key,
          "transition": parsed.to})


# === Main ===

def main() -> None:
    global GLOBAL_FLAGS, QUIET
    GLOBAL_FLAGS, filtered_argv = _preparse_global_flags(sys.argv)
    if GLOBAL_FLAGS.get("quiet", False):
        QUIET = True
    if GLOBAL_FLAGS.get("json", False):
        warnings.simplefilter("ignore")

    parser = argparse.ArgumentParser(
        prog="jira-cli",
        description="Jira issue tracker from the terminal.",
        epilog="Global flags work anywhere: jira-cli --json list --jql 'project=PROJ'"
    )
    sub = parser.add_subparsers(dest="command", help="Available commands")

    sub.add_parser("me", help="Get current user profile")
    sub.add_parser("projects", help="List projects")

    p_list = sub.add_parser("list", help="Search issues")
    p_list.add_argument("--jql", default="", help="JQL query")
    p_list.add_argument("--max", type=int, default=20, help="Max results")
    p_list.add_argument("--project", help="Filter by project key")

    p_view = sub.add_parser("view", help="View issue details")
    p_view.add_argument("issue_key", help="Issue key (e.g. PROJ-123)")

    p_create = sub.add_parser("create", help="Create an issue")
    p_create.add_argument("--project", required=True, help="Project key")
    p_create.add_argument("--summary", required=True, help="Issue summary")
    p_create.add_argument("--type", default="Task", help="Issue type")
    p_create.add_argument("--description", default="", help="Description")
    p_create.add_argument("--priority", help="Priority (Highest, High, Medium, Low, Lowest)")

    p_comment = sub.add_parser("comment", help="Add a comment")
    p_comment.add_argument("issue_key", help="Issue key")
    p_comment.add_argument("--body", "-m", required=True, help="Comment text")

    p_trans = sub.add_parser("transition", help="Transition an issue")
    p_trans.add_argument("issue_key", help="Issue key")
    p_trans.add_argument("--to", required=True, help="Transition name or ID")

    args = parser.parse_args(filtered_argv[1:])
    if not args.command:
        parser.print_help()
        sys.exit(1)

    # Lazy auth: --help and --dry-run work without credentials
    needs_auth = args.command not in ("help",) and not GLOBAL_FLAGS.get("dry_run", False)

    if needs_auth and not (ENV_EMAIL and ENV_TOKEN):
        die("Set JIRA_EMAIL and JIRA_API_TOKEN in your environment.\n"
            "  Get a token: https://id.atlassian.com/manage/api-tokens\n"
            "  Or use --dry-run to preview without credentials.")

    client = JiraClient(dry_run=GLOBAL_FLAGS.get("dry_run", False))

    # Dispatch
    cmd_map = {
        "me": cmd_me,
        "list": cmd_list,
        "view": cmd_view,
        "projects": cmd_projects,
        "create": cmd_create,
        "comment": cmd_comment,
        "transition": cmd_transition,
    }
    handler = cmd_map.get(args.command)
    if not handler:
        parser.print_help()
        sys.exit(1)

    # Pass remaining args to handler
    remaining = filtered_argv[filtered_argv.index(args.command) + 1:]
    handler(client, remaining)


if __name__ == "__main__":
    main()
