from __future__ import annotations

import asyncio
import logging
import textwrap
import typing
from pathlib import Path
from typing import Annotated

from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field

from tugboat.engine import DiagnosisModel, analyze_yaml_stream

if typing.TYPE_CHECKING:
    from collections.abc import Iterator

    from pydantic.fields import FieldInfo

logger = logging.getLogger(__name__)
server = FastMCP("tugboat")


def _Docstring(description: str) -> FieldInfo:
    """Shortcut for creating a description-only Field annotation."""
    description = textwrap.dedent(description).strip()
    return Field(description=description)


class SuccessResult(BaseModel):
    """A report generated by the analyzer."""

    count: Annotated[
        int,
        _Docstring("The number of issues found in the manifest."),
    ]

    issues: Annotated[
        list[Issue],
        _Docstring("A list of issues found in the manifest."),
    ]


class ErrorResult(BaseModel):
    """An error result returned by the analyzer."""

    message: Annotated[
        str,
        _Docstring("A human-readable message describing the error."),
    ]


class Issue(DiagnosisModel):
    """An issue reported by the analyzer."""

    sourceNearby: Annotated[
        str,
        _Docstring("Text near the issue occurrence in the source file."),
    ]


@server.tool(
    annotations=ToolAnnotations(
        title="Analyze Manifest",
        readOnlyHint=True,
        destructiveHint=False,
        openWorldHint=True,
    )
)
async def analyze_stream(
    manifest_path: Annotated[
        str,
        _Docstring(
            """
            Absolute or relative path to the Argo Workflows manifest on disk.
            Provide plain YAML; if you only have a template, render it first or set `is_helm_template`.
            """
        ),
    ],
    is_helm_template: Annotated[
        bool,
        _Docstring(
            """
            Toggle Helm template rendering.
            Set to `true` when the path points to an unrendered Helm template so the tool renders before linting.
            """
        ),
    ] = False,
) -> SuccessResult | ErrorResult:
    """
    Run lint checks on an Argo Workflows manifest and surface any rule violations.

    Each reported issue includes the rule code, location, summary, suggested fix, and nearby source
    context so you can update the manifest or produce a diff.

    ### Example

    Manifest at `/path/to/whalesay.yaml`:

    ```yaml
    apiVersion: argoproj.io/v1alpha1
    kind: Workflow
    metadata:
      generateName: demo-
    spec:
      entrypoint: ducksay
      templates:
        - name: whalesay
          container:
            image: docker/whalesay:latest
    ```

    Analyzer output:

    ```json
    {"count":1,"issues":[{"code":"WF201","summary":"Invalid entrypoint","msg":"Entrypoint 'ducksay' is not defined in any template.","fix":"whalesay"}]}
    ```

    Suggested remediation:

    - Update `spec.entrypoint` to reference an existing template (here: `whalesay`).
    - Example patch:

      ```patch
      @@
      -  entrypoint: ducksay
      +  entrypoint: whalesay
      ```
    """
    # resolve the manifest path
    manifest = Path(manifest_path).resolve()
    logger.debug("Linting manifest %s", manifest)

    if not manifest.exists():
        return ErrorResult.model_validate(
            {
                "message": f"Manifest not found. Input path: {manifest}, resolved path: {manifest}",
            }
        )

    if not manifest.is_file():
        return ErrorResult.model_validate(
            {
                "message": f"Manifest path is not a file. Input path: {manifest}, resolved path: {manifest}",
            }
        )

    # read the manifest content
    if is_helm_template:
        # render Helm template
        try:
            manifest_content = await render_helm_template(manifest)
        except Exception as e:
            return ErrorResult.model_validate({"message": str(e)})

    else:
        # plain manifest file
        with manifest.open() as fd:
            manifest_content = fd.read()

    manifest_content_lines = manifest_content.splitlines()

    # analyze
    issues = []
    for diagnosis in analyze_yaml_stream(manifest_content, manifest):
        issue = diagnosis.model_dump()

        # get lines near the issue
        issue["sourceNearby"] = "\n".join(
            get_lines_near(manifest_content_lines, diagnosis.line)
        )

        issues.append(issue)

    return SuccessResult.model_validate(
        {
            "count": len(issues),
            "issues": issues,
        }
    )


async def render_helm_template(template: Path) -> str:
    """
    Give a path to the template file.
    Returns the rendered output of a helm template command.
    """
    # locate the Helm chart root (directory containing Chart.yaml).
    chart_dir: Path | None = None
    for candidate in template.parents:
        if (candidate / "Chart.yaml").is_file():
            chart_dir = candidate
            break

    if chart_dir is None:
        raise FileNotFoundError(
            f"Could not find Chart.yaml for template path {template}"
        )

    # render the template using `helm template`
    command = [
        "helm",
        "template",
        ".",
        "--show-only",
        str(template.relative_to(chart_dir)),
    ]

    logger.debug("Executing command: $ %s", " ".join(command))

    try:
        process = await asyncio.create_subprocess_exec(
            *command,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=chart_dir,
        )
    except FileNotFoundError:
        raise RuntimeError(
            "Helm executable not found; ensure Helm is installed and in PATH."
        ) from None

    stdout, stderr = await process.communicate()

    if stderr:
        error_msg = stderr.decode("utf-8", errors="replace").strip()
        logger.warning("Helm template command stderr: %s", error_msg)

        if process.returncode != 0:
            raise RuntimeError(f"Helm template command failed: {error_msg}")

    return stdout.decode("utf-8", errors="replace")


def get_lines_near(content: list[str], focus_line: int) -> Iterator[str]:
    focus_line -= 1  # 1-based to 0-based
    line_starting = max(0, focus_line - 1)
    line_ending = min(len(content), focus_line + 2)
    yield from content[line_starting : line_ending + 1]
