from __future__ import annotations

import html
import json
import logging
from collections import Counter
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple

from .gui.i18n import localization
from .localization import t
from .model import Finding, ScanRequest, ScanResult

LOGGER = logging.getLogger("web_sentinel.reporting")

SEVERITY_ORDER = ("critical", "high", "medium", "low", "info")


class HistoryStore:
    """
    Persists scan outputs to disk to compare deltas over time.
    """

    def __init__(self, path: Path):
        self.path = Path(path)
        self.path.parent.mkdir(parents=True, exist_ok=True)

    def load_latest(self, domain: str) -> Optional[ScanResult]:
        data = self._load()
        entries = data.get(domain)
        if not entries:
            return None
        LOGGER.debug("Loaded %d historic entries for %s", len(entries), domain)
        return ScanResult.from_dict(entries[-1])

    def record(self, result: ScanResult) -> None:
        data = self._load()
        domain = result.request.domain
        data.setdefault(domain, []).append(result.to_dict())
        self._write(data)
        LOGGER.info(t("messages.history_persisted").format(domain=domain, runs=len(data[domain])))

    def _load(self) -> Dict[str, List[Dict]]:
        if not self.path.exists():
            return {}
        try:
            return json.loads(self.path.read_text(encoding="utf-8"))
        except json.JSONDecodeError as exc:
            LOGGER.warning(t("messages.history_parse_failed").format(path=self.path, error=exc))
            return {}

    def _write(self, data: Dict[str, List[Dict]]) -> None:
        self.path.write_text(json.dumps(data, indent=2), encoding="utf-8")


class ReportEngine:
    """
    Builds HTML/JSON reports aggregating findings and delta analysis.
    """

    def __init__(self, history_store: Optional[HistoryStore] = None):
        self.history_store = history_store

    def build_context(self, result: ScanResult, previous: Optional[ScanResult] = None) -> Dict:
        previous = previous or (self.history_store.load_latest(result.request.domain) if self.history_store else None)
        severity_counts = Counter(f.severity for f in result.findings)
        regressions = detect_regressions(result.findings, previous.findings if previous else [])
        resolved = detect_resolved(previous.findings if previous else [], result.findings)

        context = {
            "request": result.request.to_dict(),
            "generated_at": result.generated_at.isoformat() + "Z",
            "findings": [finding.to_dict() for finding in result.findings],
            "counts": {severity: severity_counts.get(severity, 0) for severity in SEVERITY_ORDER},
            "regressions": [finding.to_dict() for finding in regressions],
            "resolved": [finding.to_dict() for finding in resolved],
        }

        context["language"] = localization.get_current_language()
        return context

    def render_json(self, context: Dict) -> str:
        return json.dumps(context, indent=2)

    def render_html(self, context: Dict, language: Optional[str] = None) -> str:
        previous_language = localization.get_current_language()
        desired_language = language or context.get("language") or previous_language
        if desired_language not in localization.get_available_languages():
            desired_language = previous_language
        localization.set_language(desired_language)

        try:
            localized_findings = [_localize_entry(entry) for entry in context["findings"]]
            localized_regressions = [_localize_entry(entry) for entry in context["regressions"]]
            localized_resolved = [_localize_entry(entry) for entry in context["resolved"]]

            findings_html = "".join(_render_finding_block(entry) for entry in localized_findings)
            regression_html = "".join(_render_finding_block(entry) for entry in localized_regressions)
            if not regression_html:
                regression_html = f"<p>{localization.t('report_no_regressions')}</p>"
            resolved_html = "".join(_render_finding_block(entry) for entry in localized_resolved)
            if not resolved_html:
                resolved_html = f"<p>{localization.t('report_no_resolved')}</p>"

            counts_html = "".join(
                f"<li><strong>{html.escape(localization.translate_severity(severity))}</strong>: {int(context['counts'].get(severity, 0))}</li>"
                for severity in SEVERITY_ORDER
            )
            
            # Échapper les valeurs du contexte pour prévenir XSS
            domain_escaped = html.escape(context['request']['domain'])
            generated_at_escaped = html.escape(context['generated_at'])
            lang_escaped = html.escape(desired_language)
            title_escaped = html.escape(localization.t('report_html_title'))
            main_heading_escaped = html.escape(localization.t('report_main_heading'))
            domain_label_escaped = html.escape(localization.t('report_domain_label'))
            date_label_escaped = html.escape(localization.t('report_date_label'))
            summary_escaped = html.escape(localization.t('report_section_summary'))
            regressions_escaped = html.escape(localization.t('report_section_regressions'))
            resolved_escaped = html.escape(localization.t('report_section_resolved'))
            findings_escaped = html.escape(localization.t('report_section_findings'))

            return f"""<!DOCTYPE html>
<html lang="{lang_escaped}">
<head>
  <meta charset="utf-8">
  <title>{title_escaped}</title>
  <style>
    body {{ font-family: Arial, sans-serif; margin: 2rem; background: #f5f7fb; color: #1b1f23; }}
    header {{ margin-bottom: 2rem; }}
    section {{ margin-bottom: 2rem; padding: 1.5rem; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }}
    h2 {{ margin-top: 0; }}
    .finding {{ border-left: 4px solid #0366d6; padding-left: 1rem; margin-bottom: 1rem; }}
    .finding.high {{ border-color: #d73a49; }}
    .finding.medium {{ border-color: #f9c513; }}
    .finding.low {{ border-color: #28a745; }}
    .finding.critical {{ border-color: #86181d; }}
    .finding.info {{ border-color: #6a737d; }}
    code {{ background: #f1f8ff; padding: 0.2rem 0.4rem; border-radius: 4px; }}
  </style>
</head>
<body>
  <header>
    <h1>{main_heading_escaped}</h1>
    <p>{domain_label_escaped} <strong>{domain_escaped}</strong></p>
    <p>{date_label_escaped} {generated_at_escaped}</p>
  </header>

  <section>
    <h2>{summary_escaped}</h2>
    <ul>{counts_html}</ul>
  </section>

  <section>
    <h2>{regressions_escaped}</h2>
    {regression_html}
  </section>

  <section>
    <h2>{resolved_escaped}</h2>
    {resolved_html}
  </section>

  <section>
    <h2>{findings_escaped}</h2>
    {findings_html}
  </section>
</body>
</html>"""
        finally:
            localization.set_language(previous_language)


def detect_regressions(current: Iterable[Finding], previous: Iterable[Finding]) -> List[Finding]:
    previous_index = _finding_index(previous)
    regressions: List[Finding] = []
    for finding in current:
        key = (finding.check, finding.title)
        if key not in previous_index and finding.severity in {"critical", "high", "medium"}:
            regressions.append(finding)
    return regressions


def detect_resolved(previous: Iterable[Finding], current: Iterable[Finding]) -> List[Finding]:
    current_index = _finding_index(current)
    resolved: List[Finding] = []
    for finding in previous:
        key = (finding.check, finding.title)
        if key not in current_index and finding.severity in {"critical", "high", "medium"}:
            resolved.append(finding)
    return resolved


def _finding_index(findings: Iterable[Finding]) -> Dict[Tuple[str, str], Finding]:
    return {(finding.check, finding.title): finding for finding in findings}


def _render_finding_block(entry: Dict) -> str:
    # Échapper toutes les valeurs utilisateur pour prévenir XSS (CWE-79)
    severity = html.escape(entry["severity"])
    severity_label = html.escape(localization.translate_severity(entry["severity"]))
    module_key = f"module_{entry['check'].replace('-', '_')}"
    module_label = html.escape(localization.translate_module(entry["check"]))
    if module_label == html.escape(module_key):
        module_label = html.escape(entry["check"])
    
    title = html.escape(entry['title'])
    description = html.escape(entry['description'])
    remediation = html.escape(entry['remediation'])

    evidence_block = ""
    if entry.get("evidence"):
        evidence = html.escape(entry['evidence'])
        evidence_label = html.escape(localization.t('finding_evidence'))
        evidence_block = f"<p><strong>{evidence_label}:</strong> <code>{evidence}</code></p>"
    
    impact_block = ""
    if entry.get("impact"):
        impact = html.escape(entry['impact'])
        impact_label = html.escape(localization.t('finding_impact'))
        impact_block = f"<p><strong>{impact_label}:</strong> {impact}</p>"
    
    remediation_label = html.escape(localization.t('finding_remediation'))
    
    return f"""
    <div class="finding {severity}">
      <h3>[{severity_label}] {module_label} - {title}</h3>
      <p>{description}</p>
      {impact_block}
      <p><strong>{remediation_label}:</strong> {remediation}</p>
      {evidence_block}
    </div>
    """


def _localize_entry(entry: Dict) -> Dict:
    finding = Finding(
        check=entry["check"],
        title=entry["title"],
        severity=entry["severity"],
        description=entry["description"],
        remediation=entry["remediation"],
        impact=entry.get("impact"),
        evidence=entry.get("evidence"),
        i18n_key=entry.get("i18n_key"),
        i18n_params=entry.get("i18n_params"),
    )
    translated = localization.translate_finding_content(finding)
    return {
        "check": getattr(translated, "check", finding.check),
        "title": getattr(translated, "title", finding.title),
        "severity": getattr(translated, "severity", finding.severity),
        "description": getattr(translated, "description", finding.description),
        "remediation": getattr(translated, "remediation", finding.remediation),
        "impact": getattr(translated, "impact", finding.impact),
        "evidence": getattr(translated, "evidence", finding.evidence),
    }
