"""Push to Maeris portal tool - supports vulnerability scan results."""

import json
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from uuid import uuid4

import structlog
from mcp.types import TextContent, Tool

from maeris_mcp.maeris.client import MaerisClient
from maeris_mcp.maeris.types import (
    MaerisCVSS,
    MaerisSecurityFinding,
    MaerisSecurityScan,
)
from maeris_mcp.storage.security_store import SecurityScanStore
from maeris_mcp.types.security import SecurityFinding, StoredSecurityScan

logger = structlog.get_logger()


def _get_reports_dir() -> Path:
    """Get the reports directory from environment or use default."""
    reports_path = os.environ.get("MAERIS_REPORTS_DIR")
    if reports_path:
        return Path(reports_path)
    # Default to project root / security-reports
    return Path(__file__).parent.parent.parent.parent / "security-reports"


def push_to_maeris_tool() -> Tool:
    """Return the tool definition for push_to_maeris."""
    return Tool(
        name="push_to_maeris",
        description=(
            "Pushes security scan results to the Maeris portal. "
            "Authentication (token, application_id, user_id) is handled automatically from stored credentials."
        ),
        inputSchema={
            "type": "object",
            "required": ["data_type"],
            "properties": {
                "data_type": {
                    "type": "string",
                    "enum": ["vulnerabilities"],
                    "description": "Type of data to push. Currently supports: 'vulnerabilities'.",
                },
                "scan_id": {
                    "type": "string",
                    "description": "The scan ID for vulnerability data (from security_scan)",
                },
                "collection_name": {
                    "type": "string",
                    "description": "Optional: override the collection name",
                },
                "base_url": {
                    "type": "string",
                    "description": "Optional: override the Maeris API base URL",
                },
            },
        },
    )


async def handle_push_to_maeris(
    security_store: SecurityScanStore,
    arguments: dict,
) -> list[TextContent]:
    """Handle the push_to_maeris tool call."""
    data_type = arguments.get("data_type")

    if data_type != "vulnerabilities":
        return [TextContent(
            type="text",
            text="Error: data_type must be 'vulnerabilities'.",
        )]

    from maeris_mcp.auth.token_store import TokenStore
    token_store = TokenStore()
    application_id = token_store.get_app_id() or ""
    if not application_id:
        return [TextContent(type="text", text="Error: not authenticated. Run 'maeris login' first.")]

    collection_name = arguments.get("collection_name", "")
    base_url = arguments.get("base_url")

    return await _handle_push_vulnerabilities(
        security_store, arguments, application_id, collection_name, base_url
    )


async def _handle_push_vulnerabilities(
    security_store: SecurityScanStore,
    arguments: dict,
    application_id: str,
    collection_name: str,
    base_url: str | None,
) -> list[TextContent]:
    """Handle pushing vulnerability scan results to Maeris."""
    scan_id = arguments.get("scan_id")
    if not scan_id:
        scans = security_store.get_all()
        if not scans:
            return [TextContent(
                type="text",
                text="Error: No security scans available. Run security_scan first."
            )]

        scan_list = "\n".join(
            f"  - {s.result.scan_id}: {s.target_path} ({s.result.summary.total_findings} findings)"
            for s in scans
        )
        return [TextContent(
            type="text",
            text=f"Error: scan_id is required. Available scans:\n{scan_list}"
        )]

    stored_scan = security_store.get(scan_id)
    if not stored_scan:
        return [TextContent(type="text", text=f"Error: Scan not found: {scan_id}")]
    if stored_scan.status != "completed":
        return [TextContent(
            type="text",
            text=f"Error: Scan not finalized: {scan_id}. Submit findings with is_last=true first."
        )]
    if stored_scan.pushed_to_maeris:
        return [TextContent(
            type="text",
            text=f"Error: Scan already pushed to Maeris: {scan_id}"
        )]

    if not collection_name:
        collection_name = f"Security Scan - {stored_scan.target_path.split('/')[-1]}"

    # Save scan result as markdown file
    report_path = _save_scan_as_markdown(stored_scan)

    # Build Maeris-format payloads
    run_id = str(uuid4())
    timestamp = datetime.now(timezone.utc).isoformat(timespec="seconds")

    scan_payload = _convert_to_maeris_scan(
        stored_scan, application_id, run_id, timestamp
    )
    finding_payloads = [
        _convert_to_maeris_finding(
            finding, scan_payload.scan_id, run_id, application_id, timestamp
        )
        for finding in stored_scan.result.findings
    ]

    # Log payloads
    scan_dict = scan_payload.model_dump()
    findings_dicts = [f.model_dump() for f in finding_payloads]

    logger.info(
        "security_scan_payload",
        scan=scan_dict,
    )
    logger.info(
        "security_scan_findings_payload",
        findings_count=len(findings_dicts),
        findings=findings_dicts,
    )

    async with MaerisClient(base_url=base_url) as client:
        try:
            created_scan_id = await client.create_security_scan(scan_payload)
        except Exception as e:
            return [TextContent(type="text", text=f"Error: failed to create security scan: {e}")]

        findings_created: list[str] = []
        findings_failed: list[str] = []
        try:
            findings_to_send = finding_payloads
            if created_scan_id and created_scan_id != scan_payload.scan_id:
                findings_to_send = [
                    f.model_copy(update={"scan_id": created_scan_id})
                    for f in finding_payloads
                ]
            findings_created = await client.create_security_findings(findings_to_send)
        except Exception as e:
            findings_failed.append(f"batch: {e}")

    status = "pushed" if not findings_failed else "partial_failure"
    message = (
        f"Security scan pushed ({len(findings_created)} findings)"
        if not findings_failed
        else f"Security scan pushed with {len(findings_failed)} finding failures"
    )

    result: dict[str, Any] = {
        "status": status,
        "message": message,
        "collection_name": collection_name,
        "scan_id": scan_payload.scan_id,
        "created_scan_id": created_scan_id,
        "run_id": run_id,
        "target_path": stored_scan.target_path,
        "report_file": str(report_path),
        "findings_created": findings_created,
        "findings_failed": findings_failed,
        "scan_payload": scan_dict,
        "finding_payloads": findings_dicts,
    }

    security_store.mark_pushed(scan_id)
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


def _save_scan_as_markdown(scan: StoredSecurityScan) -> Path:
    """Save a security scan result as a markdown file."""
    # Get reports directory from environment or use default
    reports_dir = _get_reports_dir()

    # Ensure reports directory exists
    reports_dir.mkdir(parents=True, exist_ok=True)

    # Generate filename from scan ID and timestamp
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
    filename = f"security-scan-{timestamp}-{scan.id[:8]}.md"
    report_path = reports_dir / filename

    # Generate markdown content
    result = scan.result
    summary = result.summary

    md_lines = [
        "# Security Scan Report",
        "",
        "## Scan Details",
        "",
        "| Field | Value |",
        "|-------|-------|",
        f"| **Scan ID** | `{result.scan_id}` |",
        f"| **Target Path** | `{result.target_path}` |",
        f"| **Scan Date** | {result.scan_timestamp} |",
        f"| **Files Scanned** | {summary.files_scanned} |",
        "",
        "---",
        "",
        "## Summary",
        "",
        "| Severity | Count |",
        "|----------|-------|",
        f"| Critical | {summary.by_severity.critical} |",
        f"| High | {summary.by_severity.high} |",
        f"| Medium | {summary.by_severity.medium} |",
        f"| Low | {summary.by_severity.low} |",
        f"| **Total** | **{summary.total_findings}** |",
        "",
    ]

    # OWASP category breakdown
    if summary.by_owasp_category:
        md_lines.extend([
            "### OWASP Category Breakdown",
            "",
            "| Category | Count |",
            "|----------|-------|",
        ])
        for cat, count in sorted(summary.by_owasp_category.items()):
            md_lines.append(f"| {cat} | {count} |")
        md_lines.append("")
    else:
        md_lines.extend([
            "### OWASP Category Breakdown",
            "",
            "No findings detected.",
            "",
        ])

    md_lines.extend([
        "---",
        "",
        "## Findings",
        "",
    ])

    # List findings
    if result.findings:
        for i, finding in enumerate(result.findings, 1):
            md_lines.extend([
                f"### {i}. {finding.title}",
                "",
                f"- **Severity**: {finding.severity.upper()}",
                f"- **File**: `{finding.file_path}:{finding.line_start}`",
                f"- **Rule ID**: `{finding.rule_id}`",
            ])
            if finding.owasp_category:
                md_lines.append(f"- **OWASP Category**: {finding.owasp_category}")
            if finding.cwe_id:
                md_lines.append(f"- **CWE**: {finding.cwe_id}")
            md_lines.extend([
                "",
                f"**Description**: {finding.description}",
                "",
            ])
            if finding.code_snippet:
                md_lines.extend([
                    "**Code**:",
                    "```",
                    finding.code_snippet,
                    "```",
                    "",
                ])
            if finding.recommendation:
                md_lines.append(f"**Recommendation**: {finding.recommendation}")
                md_lines.append("")
    else:
        md_lines.append("**No vulnerabilities detected.**")
        md_lines.append("")

    # Errors section
    if result.errors:
        md_lines.extend([
            "---",
            "",
            "## Errors",
            "",
        ])
        for error in result.errors:
            md_lines.append(f"- {error}")
        md_lines.append("")

    md_lines.extend([
        "---",
        "",
        "*Generated by Maeris MCP Security Scanner*",
    ])

    # Write file
    report_path.write_text("\n".join(md_lines), encoding="utf-8")

    return report_path


# --- Security scan conversion helpers ---

# OWASP category mapping from short code to full identifier
_OWASP_FULL_ID: dict[str, str] = {
    "A01": "A01:2021",
    "A02": "A02:2021",
    "A03": "A03:2021",
    "A04": "A04:2021",
    "A05": "A05:2021",
    "A06": "A06:2021",
    "A07": "A07:2021",
    "A08": "A08:2021",
    "A09": "A09:2021",
    "A10": "A10:2021",
}

# Severity-to-CVSS base score mapping
_SEVERITY_CVSS: dict[str, float] = {
    "critical": 9.0,
    "high": 7.5,
    "medium": 5.0,
    "low": 3.1,
}


def _compute_grade(score: int) -> str:
    """Compute letter grade from a numeric score (0-100)."""
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    return "F"


def _compute_risk_level(critical: int, high: int, medium: int) -> str:
    """Compute risk level based on severity counts."""
    if critical > 0:
        return "critical"
    if high > 0:
        return "high"
    if medium > 0:
        return "medium"
    return "low"


def _compute_score(critical: int, high: int, medium: int, low: int) -> int:
    """Compute a 0-100 score. Starts at 100, deducted per finding severity."""
    score = 100 - (critical * 15) - (high * 10) - (medium * 5) - (low * 2)
    return max(0, min(100, score))


def _convert_to_maeris_scan(
    stored_scan: StoredSecurityScan,
    application_id: str,
    run_id: str,
    timestamp: str,
) -> MaerisSecurityScan:
    """Convert a stored security scan to Maeris backend format."""
    summary = stored_scan.result.summary
    sev = summary.by_severity

    score = _compute_score(sev.critical, sev.high, sev.medium, sev.low)
    grade = _compute_grade(score)
    risk_level = _compute_risk_level(sev.critical, sev.high, sev.medium)

    enabled_categories = list(summary.by_owasp_category.keys()) if summary.by_owasp_category else []

    project_name = stored_scan.target_path.split("/")[-1] or stored_scan.target_path

    return MaerisSecurityScan(
        appid=application_id,
        scan_id=stored_scan.result.scan_id,
        run_id=run_id,
        scan_name=f"OWASP Security Scan \u2014 {project_name}",
        scan_intensity="quick",
        status="completed",
        score=score,
        grade=grade,
        risk_level=risk_level,
        scan_duration=0.0,
        enabled_categories=enabled_categories,
        created_at=stored_scan.result.scan_timestamp,
        updated_at=timestamp,
    )


def _convert_to_maeris_finding(
    finding: SecurityFinding,
    scan_id: str,
    run_id: str,
    application_id: str,
    timestamp: str,
) -> MaerisSecurityFinding:
    """Convert a SecurityFinding to Maeris backend format."""
    severity = finding.severity.value if finding.severity else "medium"
    cvss_score = _SEVERITY_CVSS.get(severity, 5.0)

    owasp = ""
    if finding.owasp_category:
        short_code = finding.owasp_category.value
        owasp = _OWASP_FULL_ID.get(short_code, short_code)

    # Compute impact (0-10 scale) and priority from severity
    impact = {"critical": 9.0, "high": 7.0, "medium": 5.0, "low": 2.0}.get(severity, 5.0)
    priority = {"critical": "immediate", "high": "high", "medium": "medium", "low": "low"}.get(severity, "medium")

    # Determine testing category from rule_id
    rule = finding.rule_id
    if rule.startswith("a06_vulnerable_dep"):
        testing_category = "dependency_scan"
    elif rule.startswith(("sast_", "code_injection", "hardcoded_secret")):
        testing_category = "sast"
    else:
        testing_category = "sast"

    # Build richer evidence with file path, line range, and code snippet
    evidence_data: dict[str, Any] = {
        "file_path": finding.file_path,
        "line_start": finding.line_start,
    }
    if finding.line_end:
        evidence_data["line_end"] = finding.line_end
    if finding.code_snippet:
        evidence_data["code_snippet"] = finding.code_snippet

    return MaerisSecurityFinding(
        scan_finding_id=finding.id,
        scan_id=scan_id,
        run_id=run_id,
        appid=application_id,
        title=finding.title,
        description=finding.description,
        severity=severity,
        confidence=finding.confidence.value if finding.confidence else "medium",
        status="active",
        cvss=MaerisCVSS(score=cvss_score),
        cwe=finding.cwe_id or "",
        owasp_category=owasp,
        scan_type=finding.rule_id,
        testing_category=testing_category,
        url="",
        api="",
        evidence=json.dumps(evidence_data),
        recommendation=finding.recommendation or "",
        impact=impact,
        priority=priority,
        reference_links=finding.reference_links or [],
        occurrences=1,
        created_at=timestamp,
        updated_at=timestamp,
    )
