package report

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/pipeline"
)

// severityRank orders severities most-severe first for report ordering.
func severityRank(s pipeline.Severity) int {
	switch s {
	case pipeline.SeverityCritical:
		return 0
	case pipeline.SeverityHigh:
		return 1
	case pipeline.SeverityMedium:
		return 2
	case pipeline.SeverityLow:
		return 3
	default:
		return 4
	}
}

// orderFindings returns the findings sorted most-severe first and, within the
// same severity, VERIFIED (oracle-proven) ones first — so the report leads with
// proven, high-impact findings. Stable, and operates on a copy.
func orderFindings(fs []pipeline.ReportFinding) []pipeline.ReportFinding {
	out := append([]pipeline.ReportFinding(nil), fs...)
	sort.SliceStable(out, func(i, j int) bool {
		ri, rj := severityRank(out[i].Severity), severityRank(out[j].Severity)
		if ri != rj {
			return ri < rj
		}
		if out[i].Verified != out[j].Verified {
			return out[i].Verified // proven first
		}
		return false
	})
	return out
}

// Renderer converts a PentestReport to various output formats.
type Renderer struct{}

// NewRenderer creates a new report renderer.
func NewRenderer() *Renderer {
	return &Renderer{}
}

// ToMarkdown renders the report as GitHub-flavored Markdown.
func (r *Renderer) ToMarkdown(report *pipeline.PentestReport) ([]byte, error) {
	var b strings.Builder

	b.WriteString("# Penetration Test Report\n\n")
	b.WriteString(fmt.Sprintf("**Target:** %s\n\n", report.Target))
	b.WriteString(fmt.Sprintf("**Objective:** %s\n\n", report.Objective))
	b.WriteString(fmt.Sprintf("**Date:** %s\n\n", report.GeneratedAt.Format("January 2, 2006")))
	b.WriteString("---\n\n")

	// Executive Summary
	b.WriteString("## Executive Summary\n\n")
	b.WriteString(report.ExecutiveSummary + "\n\n")

	// Risk Summary
	b.WriteString("## Risk Summary\n\n")
	b.WriteString(fmt.Sprintf("| Severity | Count |\n|---|---|\n"))
	b.WriteString(fmt.Sprintf("| Critical | %d |\n", report.RiskSummary.CriticalCount))
	b.WriteString(fmt.Sprintf("| High | %d |\n", report.RiskSummary.HighCount))
	b.WriteString(fmt.Sprintf("| Medium | %d |\n", report.RiskSummary.MediumCount))
	b.WriteString(fmt.Sprintf("| Low | %d |\n", report.RiskSummary.LowCount))
	b.WriteString(fmt.Sprintf("| Info | %d |\n\n", report.RiskSummary.InfoCount))

	// Verification summary: how many findings carry external, non-model proof.
	// Only shown when at least one finding was externally verified, so a report
	// from a run without --verify is unchanged.
	if v := countVerified(report.Findings); v > 0 {
		b.WriteString(fmt.Sprintf("**✅ %d of %d findings externally verified** (proven against the live target by a non-LLM oracle).\n\n",
			v, len(report.Findings)))
	}

	// Findings — most-severe first, proven (VERIFIED) first within each severity.
	b.WriteString("## Findings\n\n")
	for i, f := range orderFindings(report.Findings) {
		b.WriteString(fmt.Sprintf("### %d. [%s] %s (CVSS: %.1f)\n\n", i+1, strings.ToUpper(string(f.Severity)), f.Title, f.CVSSScore))

		// VERIFIED badge: an external oracle proved this finding against the
		// live target, so it carries ground truth, not a model's confidence.
		if f.Verified {
			if f.VerifiedBy != "" {
				b.WriteString(fmt.Sprintf("> ✅ **VERIFIED** — %s\n\n", f.VerifiedBy))
			} else {
				b.WriteString("> ✅ **VERIFIED** by an external oracle\n\n")
			}
		}

		// Standardized classification labels, when mapped.
		if f.OWASP != "" || f.CWE != "" || f.ATTACK != "" {
			parts := make([]string, 0, 3)
			if f.OWASP != "" {
				parts = append(parts, "OWASP "+f.OWASP)
			}
			if f.CWE != "" {
				parts = append(parts, f.CWE)
			}
			if f.ATTACK != "" {
				parts = append(parts, "ATT&CK "+f.ATTACK)
			}
			b.WriteString("**Classification:** " + strings.Join(parts, " · ") + "\n\n")
		}

		b.WriteString(f.Description + "\n\n")

		if len(f.Evidence) > 0 {
			b.WriteString("**Evidence:**\n\n")
			for _, e := range f.Evidence {
				b.WriteString(fmt.Sprintf("```\n%s\n```\n\n", e.Content))
			}
		}

		// Steps to Reproduce — copy-pasteable command / raw HTTP request the
		// operator can re-run. Only rendered when there's real repro content.
		if rp := f.Reproduce; rp != nil && (rp.Command != "" || rp.HTTPRequest != "" || rp.ExpectedIndicator != "") {
			b.WriteString("**Steps to Reproduce:**\n\n")
			if rp.Command != "" {
				b.WriteString(fmt.Sprintf("```sh\n%s\n```\n\n", rp.Command))
			}
			if rp.HTTPRequest != "" {
				b.WriteString(fmt.Sprintf("```http\n%s\n```\n\n", rp.HTTPRequest))
			}
			if rp.ExpectedIndicator != "" {
				b.WriteString(fmt.Sprintf("Expected indicator: `%s`\n\n", rp.ExpectedIndicator))
			}
		}

		if f.Remediation != "" {
			b.WriteString("**Remediation:**\n\n")
			b.WriteString(f.Remediation + "\n\n")
		}

		b.WriteString("---\n\n")
	}

	// Planned attack path to the objective (attack-graph shortest path).
	if len(report.AttackPath) > 0 {
		b.WriteString("## Attack Path to Objective\n\n")
		b.WriteString(report.AttackPath[0] + "\n\n")
		for _, step := range report.AttackPath[1:] {
			b.WriteString(step + "\n")
		}
		b.WriteString("\n")
	}

	// Cross-finding kill-chains composed across the whole engagement.
	if len(report.KillChains) > 0 {
		b.WriteString("## Kill Chains\n\n")
		b.WriteString("Attack paths composed across findings (not isolated issues):\n\n")
		for _, k := range report.KillChains {
			b.WriteString("- " + k + "\n")
		}
		b.WriteString("\n")
	}

	// MITRE ATT&CK techniques exercised across the attack paths.
	if len(report.Techniques) > 0 {
		b.WriteString("## MITRE ATT&CK Techniques\n\n")
		for _, t := range report.Techniques {
			b.WriteString("- " + mitreLabel(t) + "\n")
		}
		b.WriteString("\n")
	}

	// Attack Narrative
	if report.AttackNarrative != "" {
		b.WriteString("## Attack Narrative\n\n")
		b.WriteString(report.AttackNarrative + "\n\n")
	}

	// Remediation Plan
	b.WriteString("## Remediation Plan\n\n")
	b.WriteString("| Priority | Finding | Action | Effort | Impact |\n|---|---|---|---|---|\n")
	for _, item := range report.RemediationPlan {
		b.WriteString(fmt.Sprintf("| %d | %s | %s | %s | %s |\n",
			item.Priority, item.Finding, item.Action, item.Effort, item.Impact))
	}

	// ROI footer (4.5.7) — only renders when the runner supplied spend data.
	if report.ROIFooter != "" {
		b.WriteString("\n---\n\n")
		b.WriteString(report.ROIFooter)
		b.WriteString("\n")
	}

	return []byte(b.String()), nil
}

// ToJSON renders the report as formatted JSON.
func (r *Renderer) ToJSON(report *pipeline.PentestReport) ([]byte, error) {
	return json.MarshalIndent(report, "", "  ")
}

// ToHTML renders the report as self-contained HTML with embedded CSS.
func (r *Renderer) ToHTML(report *pipeline.PentestReport) ([]byte, error) {
	md, err := r.ToMarkdown(report)
	if err != nil {
		return nil, err
	}

	// Wrap markdown in a basic HTML template with dark styling
	html := fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pentest Report — %s</title>
<style>
  body { font-family: -apple-system, system-ui, sans-serif; background: #0a0a0f; color: #e0e0e0; max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
  h1 { color: #fff; border-bottom: 2px solid #333; padding-bottom: 0.5rem; }
  h2 { color: #60a5fa; margin-top: 2rem; }
  h3 { color: #f59e0b; }
  table { border-collapse: collapse; width: 100%%; margin: 1rem 0; }
  th, td { border: 1px solid #333; padding: 0.5rem 1rem; text-align: left; }
  th { background: #1a1a2e; color: #60a5fa; }
  code, pre { background: #1a1a2e; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.9rem; }
  pre { padding: 1rem; overflow-x: auto; border: 1px solid #333; }
  hr { border: none; border-top: 1px solid #333; margin: 2rem 0; }
  .severity-critical { color: #ef4444; font-weight: bold; }
  .severity-high { color: #f97316; font-weight: bold; }
  .severity-medium { color: #eab308; }
  .severity-low { color: #22c55e; }
</style>
</head>
<body>
<pre style="white-space: pre-wrap;">%s</pre>
</body>
</html>`, report.Target, string(md))

	return []byte(html), nil
}

// countVerified returns how many findings an external oracle proved.
func countVerified(fs []pipeline.ReportFinding) int {
	n := 0
	for _, f := range fs {
		if f.Verified {
			n++
		}
	}
	return n
}
