# topaze/report_utils.py
# Auto Markdown → HTML / PDF (prefers Qt-native; falls back to Markdown→HTML)

import os
import shutil
import subprocess
from datetime import datetime

from topaze.toolbelt.i18n import tr

# ---------- Optional Python Markdown backends ----------
_MD_BACKEND = None
try:
    import markdown  # Python-Markdown

    _MD_BACKEND = "markdown"
except Exception:
    try:
        import markdown2  # markdown2

        _MD_BACKEND = "markdown2"
    except Exception:
        _MD_BACKEND = None

from PyQt5.QtCore import QMarginsF, QUrl

# ---------- Qt imports (QGIS ships PyQt5) ----------
from PyQt5.QtGui import QTextDocument
from PyQt5.QtPrintSupport import QPrinter

try:
    from PyQt5.QtGui import QPageLayout, QPageSize

    _HAS_QPAGELAYOUT = True
except Exception:
    _HAS_QPAGELAYOUT = False

DEFAULT_CSS = """
/* --- GitHub-like, print-friendly CSS (works in Qt QTextDocument) --- */

html, body, .markdown-body {
  font-family: -apple-system, Segoe UI, Roboto, "Noto Sans", Ubuntu, Arial, sans-serif;
  font-size: 10pt;            /* good for A4 printing */
  line-height: 1.5;
  color: #24292f;             /* GitHub text color */
  background: #ffffff;
  margin: 0;
  padding: 0;
}

/* Headings */
h1, h2, h3, h4, h5, h6 {
  color: #24292f;
  line-height: 1.25;
  margin-top: 0.75em;
  margin-bottom: 0.5em;
  font-weight: 600;
}
h1 { font-size: 22pt; border-bottom: 1px solid #d0d7de; padding-bottom: .3em; }
h2 { font-size: 18pt; border-bottom: 1px solid #d0d7de; padding-bottom: .3em; }
h3 { font-size: 14pt; }
h4 { font-size: 13pt; }
h5 { font-size: 12pt; }
h6 { font-size: 11pt; color: #57606a; }

/* Paragraphs & links */
p { margin: .75em 0; }
a { color: #0969da; text-decoration: none; }
a:hover { text-decoration: underline; }

/* Inline code */
code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: 8pt;
  background: #f6f8fa;
  border: 1px solid #d0d7de;  /* thin border like GFM */
  padding: 0 .25em;
  border-radius: 6px;
}

/* Code blocks */
pre {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: 8pt;
  background: #f6f8fa;
  border: 1px solid #d0d7de;  /* thin border */
  border-radius: 6px;
  padding: 12px;
  overflow: auto;             /* respected by Qt for horizontal scroll */
  margin: .85em 0;
}
pre code {
  background: transparent;    /* avoid double background */
  border: 0;
  padding: 0;
}

/* Blockquotes */
blockquote {
  color: #57606a;             /* muted text */
  border-left: 4px solid #d0d7de;
  background: #f8f9fb;        /* subtle */
  margin: .75em 0;
  padding: .5em .75em;
}

/* Horizontal rule */
hr {
  border: 0;
  border-top: 1px solid #d0d7de;
  margin: 1.2em 0;
}

/* Lists */
ul, ol { padding-left: 2em; }
li { margin: .25em 0; }

/* Tables (GFM-like) */
table {
  border-collapse: collapse;
  border-spacing: 0;
  margin: 1em 0;
  width: 100%;                /* makes better use of page width */
}
th, td {
  border: 1px solid #d0d7de;  /* thin border */
  padding: 6px 13px;          /* GFM paddings */
  vertical-align: top;
}
th {
  background: #f6f8fa;        /* GFM header bg */
  font-weight: 600;
  text-align: left;
}
tr:nth-child(2n) td {
  background: #fbfcfd;        /* subtle row striping (close to GitHub) */
}

/* Images */
img {
  max-width: 100%;
  display: block;
  margin: .5em 0;
}

/* Definition lists (basic) */
dt { font-weight: 600; }
dd { margin: 0 0 .6em 1.5em; }

/* Inline keyboard / kbd */
kbd {
  display: inline-block;
  padding: 0 .35em;
  font: 10pt ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  line-height: 1.2;
  color: #24292f;
  background-color: #f6f8fa;
  border: 1px solid #d0d7de;
  border-bottom-color: #b9c1c8;
  border-radius: 6px;
}

/* Page setup for print (Qt respects @page margins) */
@page {
  size: A4;
  margin: 10mm;
}
"""


# ---------- small utilities ----------


def _read_text(path: str) -> str:
    if not os.path.isfile(path):
        try:
            raise FileNotFoundError(tr("File not found: {p}").format(p=path))
        except RuntimeError as e:
            from topaze.toolbelt.log_handler import PlgLogger

            PlgLogger.log(str(e), log_level=2)

    with open(path, "r", encoding="utf-8") as f:
        return f.read()


def _write_text(path: str, text: str) -> None:
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(text)


def _ensure_ext(out_path: str, ext: str) -> str:
    root, _ = os.path.splitext(out_path)
    return root + ext


def _has_qt_markdown() -> bool:
    # QTextDocument.setMarkdown exists since Qt 5.14 (QGIS 3.x récents)
    return hasattr(QTextDocument(), "setMarkdown")


def _page_setup(printer: QPrinter, page_size: str, margins_mm):
    # Page size (robuste)
    try:
        from PyQt5.QtGui import QPageLayout, QPageSize

        size_map = {
            "A4": QPageSize.A4,
            "Letter": QPageSize.Letter,
            "Legal": QPageSize.Legal,
        }
        printer.setPageSize(QPageSize(size_map.get(page_size, QPageSize.A4)))
        has_qpagelayout = True
    except Exception:
        has_qpagelayout = False
        # Ancienne API
        if hasattr(QPrinter, "A4"):
            try:
                printer.setPaperSize(QPrinter.A4)
            except Exception:
                pass

    # Margins (en mm) – essayer plusieurs overloads
    l, t, r, b = margins_mm
    try:
        if has_qpagelayout:
            # Overload récent: QMarginsF + QPageLayout.Unit
            from PyQt5.QtGui import QPageLayout

            printer.setPageMargins(QMarginsF(l, t, r, b), QPageLayout.Millimeter)
            return
    except Exception:
        pass

    try:
        # Overload classique: left, top, right, bottom, QPrinter.Unit
        printer.setPageMargins(
            float(l), float(t), float(r), float(b), QPrinter.Millimeter
        )
        return
    except Exception:
        pass

    # Dernier recours: convertir mm → points et appeler l’overload en points
    mm_to_pt = lambda mm: float(mm) * 72.0 / 25.4
    try:
        printer.setPageMargins(mm_to_pt(l), mm_to_pt(t), mm_to_pt(r), mm_to_pt(b))
    except Exception:
        # On ignore : nombreuses anciennes bindings acceptent silencieusement ces valeurs
        pass


def _inject_css_into_html(html: str, css_text: str, title: str = None) -> str:
    """Inject a <style>…</style> into the <head> of an existing HTML string."""
    if not css_text:
        return html
    tag = f"<style>\n{css_text}\n</style>"
    # Try to inject before </head>
    lower = html.lower()
    idx = lower.find("</head>")
    if idx != -1:
        return html[:idx] + tag + html[idx:]
    # Otherwise, wrap with a minimal head
    return f"<!DOCTYPE html><html><head>{tag}</head><body>{html}</body></html>"


# ---------- Markdown → HTML (string) with optional CSS ----------


def _md_to_html_string(md_text: str, title: str = None, css_text: str = None) -> str:
    """
    Convert Markdown (rich) → full HTML string.
    Prefers Python backends for richer features; if none, falls back to Qt Markdown → HTML.
    """
    now_iso = datetime.now().isoformat(timespec="seconds")
    page_title = title or tr("Document")
    css = css_text or DEFAULT_CSS

    html_body = None

    if _MD_BACKEND == "markdown":
        html_body = markdown.markdown(
            md_text,
            extensions=[
                "extra",
                "toc",
                "codehilite",
                "sane_lists",
                "attr_list",
                "admonition",
                "smarty",
            ],
            output_format="xhtml1",
        )
    elif _MD_BACKEND == "markdown2":
        import markdown2

        html_body = markdown2.markdown(
            md_text,
            extras=[
                "fenced-code-blocks",
                "tables",
                "strike",
                "toc",
                "smarty-pants",
                "code-friendly",
            ],
        )
    elif _has_qt_markdown():
        # Fallback: render with Qt's Markdown and export to HTML (limited features)
        doc = QTextDocument()
        doc.setMarkdown(md_text)
        html_body = doc.toHtml()
    else:
        try:
            raise RuntimeError(
                tr(
                    "No Markdown renderer available (install 'markdown' or 'markdown2', or use a Qt build with setMarkdown)."
                )
            )
        except RuntimeError as e:
            from topaze.toolbelt.log_handler import PlgLogger

            PlgLogger.log(str(e), log_level=2)

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="Topaze Markdown Export" />
  <meta name="created" content="{now_iso}" />
  <title>{page_title}</title>
  <style>
{css}
  </style>
</head>
<body>
{html_body}
</body>
</html>"""


# ---------- Public API ----------


class ReportUtils:
    @staticmethod
    def markdown_to_html(
        md_path: str, html_path: str = None, css_path: str = None, title: str = None
    ) -> str:
        """
        Convert a Markdown file to an HTML file.
        Uses Python Markdown backends if available, else Qt's Markdown → HTML.
        """
        md_text = _read_text(md_path)
        css_text = _read_text(css_path) if css_path else None
        html = _md_to_html_string(md_text, title=title, css_text=css_text)
        if html_path is None:
            html_path = _ensure_ext(md_path, ".html")
        _write_text(html_path, html)
        # print(tr("HTML written to: {p}").format(p=html_path))
        return html_path

    @staticmethod
    def markdown_to_pdf(
        md_path: str,
        pdf_path: str = None,
        css_path: str = None,
        title: str = None,
        page_size: str = "A4",
        margins_mm=(7, 15, 7, 15),
        prefer_qt_markdown: bool = True,
        force_css_on_qt: bool = True,
    ) -> str:
        """
        Convert Markdown to PDF.
        - prefer_qt_markdown=True: use Qt's Markdown if available (no deps).
        - force_css_on_qt=True: even on Qt path, inject CSS by converting the Qt-rendered Markdown to HTML and refeeding it.
        """
        if pdf_path is None:
            pdf_path = _ensure_ext(md_path, ".pdf")
        os.makedirs(os.path.dirname(pdf_path) or ".", exist_ok=True)

        printer = QPrinter(QPrinter.HighResolution)
        printer.setOutputFormat(QPrinter.PdfFormat)
        printer.setOutputFileName(pdf_path)
        _page_setup(printer, page_size, margins_mm)

        base_dir = os.path.abspath(os.path.dirname(md_path)) or os.getcwd()
        md_text = _read_text(md_path)
        css_text = _read_text(css_path) if css_path else DEFAULT_CSS

        # Prepare document
        doc = QTextDocument()
        doc.setBaseUrl(QUrl.fromLocalFile(base_dir + os.sep))

        if prefer_qt_markdown and _has_qt_markdown():
            # 1) Parse Markdown nativement
            doc.setMarkdown(md_text)

            if force_css_on_qt and css_text:
                # 2) Récupère le HTML généré par Qt
                html_from_qt = doc.toHtml()
                # 3) Injecte le CSS puis re-charge en HTML
                html_with_css = _inject_css_into_html(
                    html_from_qt, css_text, title=title
                )
                doc = QTextDocument()  # nouveau document pour éviter styles résiduels
                doc.setBaseUrl(QUrl.fromLocalFile(base_dir + os.sep))
                doc.setHtml(html_with_css)
        else:
            # Chemin HTML classique (Markdown backend si dispo, sinon Qt fallback déjà géré dans _md_to_html_string)
            html = _md_to_html_string(md_text, title=title, css_text=css_text)
            doc.setHtml(html)

        doc.print_(printer)
        # print(tr("PDF written to: {p}").format(p=pdf_path))
        return pdf_path

    @staticmethod
    def markdown_to_odt(md_path: str, odt_path: str = None) -> str:
        """
        Convert a Markdown file to an OpenDocument Text (.odt) file.

        Strategy (in order):
          1) Use pypandoc (Python wrapper around pandoc) if available.
          2) Use 'pandoc' CLI binary if available.
          3) Use LibreOffice / soffice (via HTML intermediate).
        Raises a RuntimeError if no backend is available or all conversions fail.
        """
        if odt_path is None:
            odt_path = _ensure_ext(md_path, ".odt")

        # Ensure output directory exists
        os.makedirs(os.path.dirname(odt_path) or ".", exist_ok=True)

        # --- 1) Try pypandoc -------------------------------------------------
        try:
            import pypandoc  # type: ignore

            pypandoc.convert_file(md_path, "odt", outputfile=odt_path)
            return odt_path
        except Exception:
            # pypandoc not installed or failed → try next backend
            pass

        # --- 2) Try pandoc CLI ----------------------------------------------
        pandoc_bin = shutil.which("pandoc")
        if pandoc_bin:
            try:
                subprocess.run(
                    [pandoc_bin, md_path, "-o", odt_path],
                    check=True,
                )
                return odt_path
            except subprocess.CalledProcessError:
                # pandoc installed but failed → try next backend
                pass

        # --- 3) Try LibreOffice / soffice (via HTML intermediate) -----------
        lo_bin = shutil.which("soffice") or shutil.which("libreoffice")
        if lo_bin:
            base_dir = os.path.abspath(os.path.dirname(md_path)) or os.getcwd()
            tmp_html = os.path.join(base_dir, "__tmp_md_to_odt__.html")

            try:
                # Reuse existing Markdown → HTML engine (with default CSS)
                md_text = _read_text(md_path)
                html = _md_to_html_string(md_text)
                _write_text(tmp_html, html)

                outdir = os.path.dirname(odt_path) or "."
                subprocess.run(
                    [
                        lo_bin,
                        "--headless",
                        "--convert-to",
                        "odt",
                        tmp_html,
                        "--outdir",
                        outdir,
                    ],
                    check=True,
                )

                produced = os.path.join(
                    outdir,
                    os.path.splitext(os.path.basename(tmp_html))[0] + ".odt",
                )
                if os.path.isfile(produced):
                    os.replace(produced, odt_path)
                    try:
                        os.remove(tmp_html)
                    except OSError:
                        pass
                    return odt_path
            except subprocess.CalledProcessError:
                # LibreOffice installed but conversion failed
                pass
            finally:
                # Clean up tmp_html if it still exists
                try:
                    if os.path.isfile(tmp_html):
                        os.remove(tmp_html)
                except OSError:
                    pass

        # If we reach here, everything failed
        try:
            raise RuntimeError(
                tr(
                    "No ODT backend available to convert Markdown → ODT. "
                    "Install 'pypandoc' or 'pandoc', or LibreOffice in headless mode."
                )
            )
        except RuntimeError as e:
            from topaze.toolbelt.log_handler import PlgLogger

            PlgLogger.log(str(e), log_level=0)
