"""Jira MCP tool helper functions.

All tool implementations for interacting with Jira via jira-cli.
"""

import json
import logging
from typing import Any

from src.models.jira_actions import (
    AddCommentResult,
    AddToSprintResult,
    AssignToMeResult,
    CreateTicketResult,
    EditTicketResult,
    ListSprintsResult,
    MoveTicketResult,
    RemoveFromSprintResult,
    Sprint,
    UpdateDescriptionResult,
)
from src.models.jira_tickets import (
    AdfDocument,
    JiraComment,
    JiraTicket,
    JiraTicketDetail,
    normalize_status,
)
from src.tools.jira_executor import CommandResult, execute_jira_command

logger: logging.Logger = logging.getLogger(__name__)


def _build_jql_from_params(
    jql: str | None = None,
    assigned_to_me: bool | None = None,
    unassigned: bool | None = None,
    status: str | None = None,
    project: str | None = None,
    created_recently: bool | None = None,
    updated_recently: bool | None = None,
) -> str | None:
    """Build JQL query from semantic parameters.

    Args:
        jql: Raw JQL query (overrides other params if provided).
        assigned_to_me: Filter to tickets assigned to current user.
        unassigned: Filter to unassigned tickets.
        status: Filter by status.
        project: Filter by project key.
        created_recently: Filter to tickets created in last 7 days.
        updated_recently: Filter to tickets updated in last 7 days.

    Returns:
        JQL query string or None if no filters.
    """
    if jql:
        return jql

    conditions: list[str] = []

    # Assignee filters.
    if assigned_to_me:
        conditions.append("assignee = currentUser()")
    elif unassigned:
        conditions.append("assignee is EMPTY")

    # Status filter - normalize common statuses, but accept any string.
    if status:
        jira_status = normalize_status(status)
        conditions.append(f'status = "{jira_status}"')

    # Project filter.
    if project:
        conditions.append(f"project = {project}")

    # Date filters.
    if created_recently:
        conditions.append("created >= -7d")
    if updated_recently:
        conditions.append("updated >= -7d")

    return " AND ".join(conditions) if conditions else None


def list_tickets(
    jql: str | None = None,
    limit: int = 20,
    assigned_to_me: bool | None = None,
    unassigned: bool | None = None,
    status: str | None = None,
    project: str | None = None,
    created_recently: bool | None = None,
    updated_recently: bool | None = None,
    order_by: str | None = None,
    order_direction: str | None = None,
) -> list[JiraTicket]:
    """List Jira tickets with optional filters.

    Args:
        jql: Raw JQL query (advanced users only).
        limit: Maximum number of tickets to return.
        assigned_to_me: Show only tickets assigned to me.
        unassigned: Show only unassigned tickets.
        status: Filter by status.
        project: Filter by project key (e.g., 'PROJ').
        created_recently: Show tickets created in the last 7 days.
        updated_recently: Show tickets updated in the last 7 days.
        order_by: Sort tickets by field (created, updated, priority).
        order_direction: Sort direction (asc, desc).

    Returns:
        List of JiraTicket objects.
    """
    built_jql = _build_jql_from_params(
        jql=jql,
        assigned_to_me=assigned_to_me,
        unassigned=unassigned,
        status=status,
        project=project,
        created_recently=created_recently,
        updated_recently=updated_recently,
    )

    args = [
        "issue",
        "list",
        "--no-headers",
        "--plain",
        "--columns",
        "key,summary,status,priority,type,assignee",
    ]

    if built_jql:
        args.extend(["--jql", built_jql])

    # Add ordering using jira-cli flags.
    if order_by:
        args.extend(["--order-by", order_by])
        if order_direction == "asc":
            args.append("--reverse")

    if limit > 0:
        args.extend(["--paginate", f"0:{limit}"])

    result: CommandResult = execute_jira_command(args)

    if result.exit_code != 0:
        # Check if it's just "No result found" which is not an error.
        if "No result found" in result.stderr:
            return []
        raise ValueError(f"Failed to list tickets: {result.stderr}")

    # Parse the plain text output.
    lines = [line for line in result.stdout.strip().split("\n") if line.strip()]

    tickets: list[JiraTicket] = []
    for line in lines:
        # jira-cli output is tab-separated in plain mode.
        columns = [col.strip() for col in line.split("\t") if col.strip()]

        if len(columns) >= 5:
            tickets.append(
                JiraTicket(
                    key=columns[0],
                    summary=columns[1],
                    status=columns[2],
                    priority=columns[3],
                    type=columns[4],
                    assignee=columns[5] if len(columns) > 5 else None,
                )
            )

    return tickets


def _convert_adf_to_text(adf: AdfDocument) -> str:
    """Convert Atlassian Document Format to plain text.

    Args:
        adf: ADF document as a dictionary.

    Returns:
        Plain text representation.
    """
    parts: list[str] = []

    def process_inline_content(nodes: list[dict[str, Any]]) -> str:
        result: list[str] = []
        for node in nodes:
            if node.get("type") == "text":
                text = node.get("text", "")
                marks = node.get("marks", [])
                for mark in marks:
                    mark_type = mark.get("type")
                    if mark_type == "strong":
                        text = f"**{text}**"
                    elif mark_type == "em":
                        text = f"*{text}*"
                    elif mark_type == "code":
                        text = f"`{text}`"
                    elif mark_type == "strike":
                        text = f"~~{text}~~"
                result.append(text)
            elif node.get("type") == "hardBreak":
                result.append("\n")
        return "".join(result)

    content = adf.get("content", [])
    for block in content:
        block_type = block.get("type")

        if block_type == "paragraph":
            block_content = block.get("content", [])
            if block_content:
                text = process_inline_content(block_content)
                if text:
                    parts.append(text)

        elif block_type == "heading":
            level = block.get("attrs", {}).get("level", 1)
            block_content = block.get("content", [])
            if block_content:
                prefix = "#" * level
                text = process_inline_content(block_content)
                if text:
                    parts.append(f"{prefix} {text}")

        elif block_type == "bulletList":
            list_items: list[str] = []
            for item in block.get("content", []):
                item_parts: list[str] = []
                for p in item.get("content", []):
                    if p.get("type") == "paragraph" and p.get("content"):
                        item_parts.append(process_inline_content(p["content"]))
                if item_parts:
                    list_items.append(f"- {' '.join(item_parts)}")
            if list_items:
                parts.append("\n".join(list_items))

        elif block_type == "orderedList":
            start_num = block.get("attrs", {}).get("start", 1)
            list_items = []
            for idx, item in enumerate(block.get("content", [])):
                item_parts = []
                for p in item.get("content", []):
                    if p.get("type") == "paragraph" and p.get("content"):
                        item_parts.append(process_inline_content(p["content"]))
                if item_parts:
                    list_items.append(
                        f"{start_num + idx}. {' '.join(item_parts)}"
                    )
            if list_items:
                parts.append("\n".join(list_items))

        elif block_type == "codeBlock":
            lang = block.get("attrs", {}).get("language", "")
            code_parts: list[str] = []
            for text_node in block.get("content", []):
                code_parts.append(text_node.get("text", ""))
            code = "".join(code_parts)
            parts.append(f"```{lang}\n{code}\n```")

        elif block_type == "rule":
            parts.append("---")

        elif block_type == "blockquote":
            quoted_lines: list[str] = []
            for p in block.get("content", []):
                if p.get("type") == "paragraph" and p.get("content"):
                    quoted_lines.append(
                        f"> {process_inline_content(p['content'])}"
                    )
            if quoted_lines:
                parts.append("\n".join(quoted_lines))

    return "\n\n".join(parts)


def get_ticket(ticket_key: str, comments: int = 5) -> JiraTicketDetail:
    """Get detailed information about a Jira ticket.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).
        comments: Number of comments to include.

    Returns:
        JiraTicketDetail with full ticket information.
    """
    args = ["issue", "view", ticket_key, "--raw"]

    if comments > 0:
        args.extend(["--comments", str(comments)])

    result: CommandResult = execute_jira_command(args)

    if result.exit_code != 0:
        raise ValueError(f"Failed to get ticket {ticket_key}: {result.stderr}")

    try:
        raw_data: dict[str, Any] = json.loads(result.stdout)
    except json.JSONDecodeError as e:
        raise ValueError(
            f"Failed to parse jira response: {result.stdout}"
        ) from e
    except Exception:
        raise ValueError(f"Failed to get ticket {ticket_key}: {result.stdout}")

    fields: dict[str, Any] = raw_data.get("fields", {})

    # Extract description text from ADF format.
    description_text: str = ""
    description = fields.get("description")
    if description and isinstance(description, dict):
        description_text = _convert_adf_to_text(description)

    # Extract comments.
    comment_data = fields.get("comment", {}).get("comments", [])
    ticket_comments: list[JiraComment] = []
    for c in comment_data:
        # Convert comment body from ADF format if needed.
        comment_body = c.get("body", "")
        if isinstance(comment_body, dict):
            comment_body = _convert_adf_to_text(comment_body)
        elif not isinstance(comment_body, str):
            comment_body = str(comment_body)

        ticket_comments.append(
            JiraComment(
                author=c.get("author", {}).get("displayName", "Unknown"),
                created=c.get("created", ""),
                body=comment_body,
            )
        )

    return JiraTicketDetail(
        key=raw_data.get("key", ""),
        summary=fields.get("summary", ""),
        status=fields.get("status", {}).get("name", ""),
        priority=fields.get("priority", {}).get("name", ""),
        type=fields.get("issuetype", {}).get("name", ""),
        assignee=(
            fields.get("assignee", {}).get("displayName")
            if fields.get("assignee")
            else None
        ),
        reporter=(
            fields.get("reporter", {}).get("displayName")
            if fields.get("reporter")
            else None
        ),
        created=fields.get("created", ""),
        updated=fields.get("updated", ""),
        description=description_text,
        comments=ticket_comments,
    )


def create_ticket(
    project: str,
    issue_type: str,
    summary: str,
    description: str | None = None,
    priority: str | None = None,
    assignee: str | None = None,
    labels: list[str] | None = None,
    components: list[str] | None = None,
) -> CreateTicketResult:
    """Create a new Jira ticket.

    Args:
        project: Jira project key (e.g., PROJ).
        issue_type: Issue type (e.g., Bug, Story, Task).
        summary: Issue summary/title.
        description: Issue description (markdown supported).
        priority: Priority level (e.g., High, Medium, Low).
        assignee: Assignee username or email.
        labels: List of labels to add.
        components: List of components to add.

    Returns:
        CreateTicketResult with success status and ticket info.
    """
    args: list[str] = [
        "issue",
        "create",
        "--project",
        project,
        "--type",
        issue_type,
        "--summary",
        summary,
        "--no-input",
        "--raw",
    ]

    if priority:
        args.extend(["--priority", priority])

    if assignee:
        args.extend(["--assignee", assignee])

    if labels:
        for label in labels:
            args.extend(["--label", label])

    if components:
        for component in components:
            args.extend(["--component", component])

    # If description is provided, use stdin with template flag.
    stdin_input: str | None = None
    if description:
        args.extend(["--template", "-"])
        stdin_input = description

    try:
        result: CommandResult = execute_jira_command(
            args, stdin_input=stdin_input
        )

        if result.exit_code != 0:
            return CreateTicketResult(
                success=False,
                error=result.stderr or "Failed to create ticket",
            )

        output: dict[str, Any] = json.loads(result.stdout)
        ticket_key = output.get("key", "")
        ticket_url = output.get("self", "")

        return CreateTicketResult(
            success=True,
            ticket_key=ticket_key,
            ticket_url=ticket_url,
        )
    except Exception as e:
        return CreateTicketResult(
            success=False,
            error=f"Failed to create ticket: {e}",
        )


def move_ticket(ticket_key: str, status: str) -> MoveTicketResult:
    """Move a Jira ticket to a different status.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).
        status: Target status to move the ticket to.

    Returns:
        MoveTicketResult with success status and details.
    """
    # Normalize status to Jira status name (handles common statuses).
    target_status = normalize_status(status)

    # First, get current status.
    view_result: CommandResult = execute_jira_command(
        [
            "issue",
            "list",
            "--jql",
            f"key = {ticket_key}",
            "--plain",
            "--no-headers",
            "--columns",
            "status",
        ]
    )

    if view_result.exit_code != 0:
        raise ValueError(
            f"Failed to get current status for {ticket_key}: {view_result.stderr}"
        )

    # Parse the output.
    output_parts: list[str] = view_result.stdout.strip().split("\t")
    current_status = (
        output_parts[1] if len(output_parts) > 1 else output_parts[0]
    )
    current_status = current_status.strip() if current_status else "Unknown"

    # Move the ticket.
    move_result: CommandResult = execute_jira_command(
        [
            "issue",
            "move",
            ticket_key,
            target_status,
        ]
    )

    if move_result.exit_code != 0:
        raise ValueError(
            f"Failed to move ticket {ticket_key}: {move_result.stderr}"
        )

    return MoveTicketResult(
        success=True,
        ticket_key=ticket_key,
        previous_status=current_status,
        new_status=target_status,
        message=f"Successfully moved {ticket_key} from {current_status} to {target_status}",
    )


def add_comment(ticket_key: str, comment: str) -> AddCommentResult:
    """Add a comment to a Jira ticket.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).
        comment: Comment text to add.

    Returns:
        AddCommentResult with success status.
    """
    args: list[str] = ["issue", "comment", "add", ticket_key, "--no-input"]
    result: CommandResult = execute_jira_command(args, stdin_input=comment)

    if result.exit_code != 0:
        raise ValueError(
            f"Failed to add comment to {ticket_key}: {result.stderr}"
        )

    return AddCommentResult(
        success=True,
        ticket_key=ticket_key,
        message=f"Successfully added comment to {ticket_key}",
    )


def assign_to_me(ticket_key: str) -> AssignToMeResult:
    """Assign a Jira ticket to the current user.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).

    Returns:
        AssignToMeResult with success status and assignee info.
    """
    # Get current user.
    me_result: CommandResult = execute_jira_command(["me"])
    if me_result.exit_code != 0:
        raise ValueError(f"Failed to get current user: {me_result.stderr}")

    current_user: str = me_result.stdout.strip()
    if not current_user:
        raise ValueError("Unable to determine current user")

    # Assign the ticket.
    assign_result: CommandResult = execute_jira_command(
        [
            "issue",
            "assign",
            ticket_key,
            current_user,
        ]
    )

    if assign_result.exit_code != 0:
        raise ValueError(
            f"Failed to assign ticket {ticket_key}: {assign_result.stderr}"
        )

    return AssignToMeResult(
        success=True,
        ticket_key=ticket_key,
        assignee=current_user,
        message=f"Successfully assigned {ticket_key} to {current_user}",
    )


def open_ticket_in_browser(ticket_key: str) -> str:
    """Open a Jira ticket in the default web browser.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).

    Returns:
        Success message.
    """
    result: CommandResult = execute_jira_command(["open", ticket_key])

    if result.exit_code != 0:
        raise ValueError(
            f"Failed to open ticket {ticket_key} in browser: {result.stderr}"
        )

    return f"Successfully opened ticket {ticket_key} in browser"


def update_ticket_description(
    ticket_key: str, description: str
) -> UpdateDescriptionResult:
    """Update the description of a Jira ticket.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).
        description: New description content for the ticket.

    Returns:
        UpdateDescriptionResult with success status.
    """
    args: list[str] = ["issue", "edit", ticket_key, "--no-input"]
    result: CommandResult = execute_jira_command(args, stdin_input=description)

    if result.exit_code != 0:
        raise ValueError(
            f"Failed to update ticket {ticket_key}: {result.stderr}"
        )

    return UpdateDescriptionResult(
        success=True,
        ticket_key=ticket_key,
        message=f"Successfully updated description for {ticket_key}",
    )


def list_sprints(
    board_id: int,
    state: str | None = None,
    limit: int = 20,
) -> ListSprintsResult:
    """List sprints from a Jira board.

    Args:
        board_id: Jira board ID to list sprints from.
        state: Filter sprints by state (active, future, closed).
        limit: Maximum number of sprints to return.

    Returns:
        ListSprintsResult with list of sprints.
    """
    # Note: jira-cli doesn't support --board flag. It uses the configured project.
    # The board_id parameter is kept for API compatibility but is currently unused.
    _ = board_id  # Suppress unused variable warning.
    args: list[str] = [
        "sprint",
        "list",
        "--table",
        "--plain",
        "--no-headers",
        "--columns",
        "id,name,start,end,state",
    ]

    if state:
        args.extend(["--state", state])

    if limit > 0:
        args.extend(["--paginate", f"0:{limit}"])

    result: CommandResult = execute_jira_command(args)

    if result.exit_code != 0:
        # Check if it's just "no sprints found" which is not an error.
        if (
            "No result found" in result.stderr
            or "no sprints" in result.stderr.lower()
        ):
            return ListSprintsResult(sprints=[])
        raise ValueError(f"Failed to list sprints: {result.stderr}")

    # Parse the plain text output.
    lines = [line for line in result.stdout.strip().split("\n") if line.strip()]

    sprints: list[Sprint] = []
    for line in lines:
        columns = [col.strip() for col in line.split("\t") if col.strip()]

        # Columns: id, name, start, end, state.
        if len(columns) >= 3:
            sprints.append(
                Sprint(
                    id=int(columns[0]),
                    name=columns[1],
                    start_date=columns[2] if len(columns) > 2 else None,
                    end_date=columns[3] if len(columns) > 3 else None,
                    state=columns[4] if len(columns) > 4 else "unknown",
                )
            )

    return ListSprintsResult(sprints=sprints)


def add_to_sprint(ticket_key: str, sprint_id: int) -> AddToSprintResult:
    """Add a Jira ticket to a sprint.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).
        sprint_id: Sprint ID to add the ticket to.

    Returns:
        AddToSprintResult with success status.
    """
    args: list[str] = ["sprint", "add", str(sprint_id), ticket_key]

    result: CommandResult = execute_jira_command(args)

    if result.exit_code != 0:
        raise ValueError(
            f"Failed to add {ticket_key} to sprint {sprint_id}: {result.stderr}"
        )

    return AddToSprintResult(
        success=True,
        ticket_key=ticket_key,
        sprint_id=sprint_id,
        message=f"Successfully added {ticket_key} to sprint {sprint_id}",
    )


def remove_from_sprint(ticket_key: str) -> RemoveFromSprintResult:
    """Remove a Jira ticket from its current sprint.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).

    Returns:
        RemoveFromSprintResult with success status.
    """
    # To remove from sprint, we edit the issue and set sprint to empty.
    args: list[str] = [
        "issue",
        "edit",
        ticket_key,
        "--custom",
        "sprint=",
        "--no-input",
    ]

    result: CommandResult = execute_jira_command(args)

    if result.exit_code != 0:
        raise ValueError(
            f"Failed to remove {ticket_key} from sprint: {result.stderr}"
        )

    return RemoveFromSprintResult(
        success=True,
        ticket_key=ticket_key,
        message=f"Successfully removed {ticket_key} from its sprint",
    )


def edit_ticket(
    ticket_key: str,
    summary: str | None = None,
    priority: str | None = None,
    assignee: str | None = None,
    labels: list[str] | None = None,
    add_labels: list[str] | None = None,
    remove_labels: list[str] | None = None,
    components: list[str] | None = None,
    fix_versions: list[str] | None = None,
    parent: str | None = None,
    custom_fields: dict[str, str] | None = None,
) -> EditTicketResult:
    """Edit fields on a Jira ticket.

    Args:
        ticket_key: Jira ticket key (e.g., PROJ-123).
        summary: New summary/title for the ticket.
        priority: New priority level (e.g., High, Medium, Low).
        assignee: New assignee username or email (use empty string to unassign).
        labels: Labels to set on the ticket (replaces existing labels).
        add_labels: Labels to add to the ticket.
        remove_labels: Labels to remove from the ticket.
        components: Components to set on the ticket.
        fix_versions: Fix versions to set on the ticket.
        parent: Parent issue key (for subtasks/child issues).
        custom_fields: Custom fields to set (key-value pairs).

    Returns:
        EditTicketResult with success status and list of updated fields.
    """
    args: list[str] = ["issue", "edit", ticket_key, "--no-input"]
    updated_fields: list[str] = []

    if summary:
        args.extend(["--summary", summary])
        updated_fields.append("summary")

    if priority:
        args.extend(["--priority", priority])
        updated_fields.append("priority")

    if assignee is not None:
        if assignee == "":
            # Unassign by setting to "x" (jira-cli convention).
            args.extend(["--assignee", "x"])
        else:
            args.extend(["--assignee", assignee])
        updated_fields.append("assignee")

    # Handle labels.
    if labels:
        for label in labels:
            args.extend(["--label", label])
        updated_fields.append("labels")

    if add_labels:
        for label in add_labels:
            args.extend(["--label", f"+{label}"])
        updated_fields.append("labels (added)")

    if remove_labels:
        for label in remove_labels:
            args.extend(["--label", f"-{label}"])
        updated_fields.append("labels (removed)")

    # Handle components.
    if components:
        for component in components:
            args.extend(["--component", component])
        updated_fields.append("components")

    # Handle fix versions.
    if fix_versions:
        for version in fix_versions:
            args.extend(["--fix-version", version])
        updated_fields.append("fix_versions")

    # Handle parent issue.
    if parent:
        args.extend(["--parent", parent])
        updated_fields.append("parent")

    # Handle custom fields.
    if custom_fields:
        for key, value in custom_fields.items():
            args.extend(["--custom", f"{key}={value}"])
            updated_fields.append(f"custom:{key}")

    # Check if any fields were specified.
    if not updated_fields:
        return EditTicketResult(
            success=False,
            ticket_key=ticket_key,
            message="No fields specified to update",
            updated_fields=[],
        )

    result: CommandResult = execute_jira_command(args)

    if result.exit_code != 0:
        raise ValueError(f"Failed to edit ticket {ticket_key}: {result.stderr}")

    return EditTicketResult(
        success=True,
        ticket_key=ticket_key,
        message=f"Successfully updated {ticket_key}: {', '.join(updated_fields)}",
        updated_fields=updated_fields,
    )
