"""
Turn an audit into a PDF booklet.

reportlab rather than Word: a PDF paginates predictably, keeps the logo and
footer on every page, and cannot reflow into blank pages on someone else's
copy of Office.

Charts come from matplotlib if it is installed. Without it the report still
builds, minus the pictures.
"""
import io
import logging
import os

logh = logging.getLogger(__name__)

NAVY = '#033152'
GREEN = '#15803d'
AMBER = '#b45309'
RED = '#b91c1c'
GREY = '#6b7280'
LINE = '#e5e7eb'

SEVERITY_LABEL = {
    'high': 'Needs attention',
    'medium': 'Worth doing',
    'low': 'Optional',
    'info': 'For information',
}
SEVERITY_COLOUR = {'high': RED, 'medium': AMBER, 'low': GREY, 'info': GREY}


def _available():
    try:
        import reportlab  # noqa: F401
        return True
    except ImportError:
        return False


def _grade_colour(grade):
    return {'A': GREEN, 'B': GREEN, 'C': AMBER, 'D': AMBER}.get(grade, RED)


def _site_logo_path():
    """The logo from Settings, if there is one and the file is still on disk."""
    try:
        from apps.pages.models import SiteSettings
        from apps.tenants.models import Tenant
        master = Tenant.objects.filter(is_master=True).first()
        site = SiteSettings.load(master)
        if site and site.logo and site.logo.path and os.path.exists(site.logo.path):
            return site.logo.path
    except Exception:
        logh.debug('No site logo available for the report header', exc_info=True)
    return None


# ── charts ───────────────────────────────────────────────────────────────────

def _fig_to_buf(fig):
    import matplotlib.pyplot as plt
    buf = io.BytesIO()
    fig.savefig(buf, format='png', transparent=True)
    plt.close(fig)
    buf.seek(0)
    return buf


def _uptime_chart(series, mono=False):
    try:
        import matplotlib
        matplotlib.use('Agg')
        import matplotlib.pyplot as plt
    except Exception:
        return None
    if not series:
        return None
    values = [s[1] for s in series]
    labels = [s[0] for s in series]
    fig, ax = plt.subplots(figsize=(6.6, 1.7), dpi=170)
    if mono:
        colours = ['#a1a1aa' if v >= 99.9 else '#71717a' if v >= 95 else '#3f3f46' for v in values]
    else:
        colours = ['#16a34a' if v >= 99.9 else '#d97706' if v >= 95 else '#dc2626' for v in values]
    ax.bar(range(len(values)), values, color=colours, width=0.72)
    ax.set_ylim(min(min(values) - 1, 95), 100.3)
    ax.tick_params(labelsize=6, length=0)
    ax.set_ylabel('%', fontsize=6, color='#6b7280')
    step = max(len(labels) // 8, 1)
    ax.set_xticks(range(0, len(labels), step))
    ax.set_xticklabels(labels[::step], color='#6b7280')
    for s in ('top', 'right', 'left'):
        ax.spines[s].set_visible(False)
    ax.spines['bottom'].set_color('#e5e7eb')
    ax.grid(axis='y', color='#f1f5f9', linewidth=.6)
    ax.set_axisbelow(True)
    fig.tight_layout(pad=0.3)
    return _fig_to_buf(fig)


def _score_chart(sections, mono=False):
    try:
        import matplotlib
        matplotlib.use('Agg')
        import matplotlib.pyplot as plt
    except Exception:
        return None
    pairs = [(s['name'], s['score']) for s in sections if s.get('score') is not None]
    if not pairs:
        return None
    names = [p[0] for p in pairs][::-1]
    values = [p[1] for p in pairs][::-1]
    fig, ax = plt.subplots(figsize=(6.6, 1.9), dpi=170)
    if mono:
        colours = ['#a1a1aa' if v >= 80 else '#71717a' if v >= 60 else '#3f3f46' for v in values]
    else:
        colours = ['#16a34a' if v >= 80 else '#d97706' if v >= 60 else '#dc2626' for v in values]
    ax.barh(names, values, color=colours, height=0.5)
    ax.set_xlim(0, 108)
    ax.tick_params(labelsize=7, length=0, colors='#374151')
    for i, v in enumerate(values):
        ax.text(v + 2, i, str(v), va='center', fontsize=7, color='#6b7280')
    for s in ('top', 'right', 'bottom'):
        ax.spines[s].set_visible(False)
    ax.spines['left'].set_color('#e5e7eb')
    ax.set_xticks([])
    fig.tight_layout(pad=0.3)
    return _fig_to_buf(fig)


# ── page furniture ───────────────────────────────────────────────────────────

def _make_page_decorator(logo_path):
    """Logo top left on every page, address and page number in the footer."""
    from reportlab.lib.colors import HexColor
    from reportlab.lib.units import mm

    def draw(canvas, doc):
        canvas.saveState()
        width, height = doc.pagesize

        # header
        y = height - 14 * mm
        if logo_path:
            try:
                from reportlab.lib.utils import ImageReader
                img = ImageReader(logo_path)
                iw, ih = img.getSize()
                h = 9 * mm
                w = h * (iw / float(ih))
                canvas.drawImage(img, 18 * mm, y - 2 * mm, width=w, height=h,
                                 mask='auto', preserveAspectRatio=True)
            except Exception:
                pass
        # Logo only. The site name and the rule under it were repeating what
        # the title block already says.

        # footer
        fy = 12 * mm
        canvas.setStrokeColor(HexColor(LINE))
        canvas.line(18 * mm, fy + 5 * mm, width - 18 * mm, fy + 5 * mm)
        canvas.setFont('Helvetica-Bold', 8)
        canvas.setFillColor(HexColor(NAVY))
        canvas.drawString(18 * mm, fy, 'www.finaltarget.com')
        canvas.setFont('Helvetica', 8)
        canvas.setFillColor(HexColor(GREY))
        canvas.drawRightString(width - 18 * mm, fy, f'Page {canvas.getPageNumber()}')
        canvas.restoreState()

    return draw


def build_report(audit):
    """Return the PDF as bytes."""
    # A report on a site nobody is watching should not look like a live one.
    # Printed in grey, it reads at a glance as a record rather than a status.
    global NAVY, GREEN, AMBER, RED, GREY, LINE
    _palette = (NAVY, GREEN, AMBER, RED, GREY, LINE)
    if audit.get('monochrome'):
        NAVY, GREEN, AMBER, RED, GREY, LINE = (
            '#3f3f46', '#71717a', '#52525b', '#27272a', '#71717a', '#d4d4d8')
    if not _available():
        raise RuntimeError(
            'reportlab is not installed on this server, so reports cannot be produced. '
            'Install it with: venv/bin/pip install reportlab'
        )

    from reportlab.lib import colors
    from reportlab.lib.colors import HexColor
    from reportlab.lib.enums import TA_CENTER, TA_LEFT
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
    from reportlab.lib.units import mm
    from reportlab.platypus import (BaseDocTemplate, Frame, Image, KeepTogether,
                                    PageTemplate, Paragraph, Spacer, Table, TableStyle)

    buf = io.BytesIO()
    doc = BaseDocTemplate(
        buf, pagesize=A4,
        leftMargin=18 * mm, rightMargin=18 * mm,
        topMargin=26 * mm, bottomMargin=20 * mm,
        title=f"Site Health Report, {audit['site']}",
        author='Final Target',
    )
    frame = Frame(doc.leftMargin, doc.bottomMargin,
                  doc.width, doc.height, id='body',
                  leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0)
    doc.addPageTemplates([
        PageTemplate(id='all', frames=[frame],
                     onPage=_make_page_decorator(_site_logo_path()))
    ])

    ss = getSampleStyleSheet()
    H1 = ParagraphStyle('H1', parent=ss['Normal'], fontName='Helvetica-Bold',
                        fontSize=19, leading=22, textColor=HexColor(NAVY), spaceAfter=2)
    SUB = ParagraphStyle('SUB', parent=ss['Normal'], fontName='Helvetica',
                         fontSize=9.5, leading=12, textColor=HexColor(GREY), spaceAfter=8)
    H2 = ParagraphStyle('H2', parent=ss['Normal'], fontName='Helvetica-Bold',
                        fontSize=11.5, leading=14, textColor=HexColor(NAVY),
                        spaceBefore=8, spaceAfter=3)
    BODY = ParagraphStyle('BODY', parent=ss['Normal'], fontName='Helvetica',
                          fontSize=9, leading=12.5, alignment=TA_LEFT, spaceAfter=4)
    WARN = ParagraphStyle('WARN', parent=ss['Normal'], fontName='Helvetica',
                          fontSize=8.5, leading=12, textColor=colors.HexColor('#7c2d12'),
                          backColor=colors.HexColor('#fff7ed'),
                          borderPadding=6, spaceBefore=2, spaceAfter=6)
    SMALL = ParagraphStyle('SMALL', parent=BODY, fontSize=8, leading=10.5,
                           textColor=HexColor(GREY), spaceAfter=3)
    CAP = ParagraphStyle('CAP', parent=ss['Normal'], fontName='Helvetica-Bold',
                         fontSize=8.5, textColor=HexColor(NAVY), spaceBefore=4, spaceAfter=2)
    # The grade letter needs its own style: centred, and with leading matched to
    # the glyph so it sits in the middle of the block rather than low and left.
    GRADE = ParagraphStyle('GRADE', parent=ss['Normal'], fontName='Helvetica-Bold',
                           fontSize=30, leading=32, alignment=TA_CENTER,
                           textColor=colors.white, spaceBefore=0, spaceAfter=0)

    story = []

    # ── title and grade, side by side, no cover page wasted ──────────────
    story.append(Paragraph('Site Health Report', H1))
    client = f" &nbsp;·&nbsp; {audit['client']}" if audit['client'] and audit['client'] != audit['site'] else ''
    story.append(Paragraph(f"{audit['site']}{client} &nbsp;·&nbsp; "
                           f"{audit['generated_at']:%d %B %Y, %H:%M}", SUB))

    # Monitoring off means the history stops at the last check. Without this the
    # report carries today's date and reads as a description of the site now.
    if not audit.get('monitored', True):
        _lc = audit.get('last_check')
        _until = f"{_lc:%d %B %Y, %H:%M}" if _lc else 'an earlier date'
        story.append(Spacer(1, 2))
        story.append(Paragraph(
            f"<b>Monitoring is switched off for this site.</b> Continuous checks stopped on "
            f"{_until}, so the availability history in this report covers the period up to "
            f"that point and not to the date above. The other checks were run live today.",
            WARN))
        story.append(Spacer(1, 4))

    if not audit.get('linked', True):
        story.append(Spacer(1, 2))
        story.append(Paragraph(
            "<b>This site is not connected to the panel.</b> It exists here as a client record "
            "only: it has never checked in, so nothing is known about the software behind the "
            "domain. Everything below was measured from outside, in the same way anyone on the "
            "internet could measure it. Design, content and code quality were not examined.",
            WARN))
        story.append(Spacer(1, 4))

    grade_cell = Paragraph(audit['grade'], GRADE)
    summary = Paragraph(
        f"<b><font size=11 color='{NAVY}'>{audit['grade_label']}</font></b><br/>"
        f"<font size=9 color='{GREY}'>Overall score {audit['overall']} out of 100, averaged "
        f"across the areas below, then held down by the weakest one. "
        f"{audit.get('cap_reason', '')} Everything here was measured from outside at the time of "
        f"writing. Design, content and code quality are not part of this score.</font>",
        BODY)
    t = Table([[grade_cell, summary]], colWidths=[26 * mm, doc.width - 26 * mm])
    t.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (0, 0), HexColor(_grade_colour(audit['grade']))),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
        ('ALIGN', (0, 0), (0, 0), 'CENTER'),
        ('LEFTPADDING', (1, 0), (1, 0), 8),
        ('TOPPADDING', (0, 0), (-1, -1), 8),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
        ('LEFTPADDING', (0, 0), (0, 0), 0),
        ('RIGHTPADDING', (0, 0), (0, 0), 0),
        ('BOX', (1, 0), (1, 0), 0.5, HexColor(LINE)),
    ]))
    story.append(t)
    story.append(Spacer(1, 8))

    story.append(Paragraph('How to read this', H2))
    story.append(Paragraph(
        'Each area is scored out of 100. A is excellent, E means something needs doing today. '
        'Findings are listed in order of importance with what we suggest. Nothing here needs '
        'technical knowledge to act on: if a line says something needs attention, tell us and '
        'we will handle it.', BODY))

    # ── charts ───────────────────────────────────────────────────────────
    chart = _score_chart(audit['sections'], audit.get('monochrome'))
    if chart:
        story.append(Paragraph('Score by area', CAP))
        story.append(Image(chart, width=doc.width, height=doc.width * (1.9 / 6.6)))

    up = _uptime_chart(audit['uptime_series'], audit.get('monochrome'))
    if up:
        story.append(Paragraph('Daily uptime, last 30 days', CAP))
        story.append(Image(up, width=doc.width, height=doc.width * (1.7 / 6.6)))

    # ── areas, two per row to keep it tight ──────────────────────────────
    story.append(Spacer(1, 4))
    story.append(Paragraph('The detail', H2))

    for s in audit['sections']:
        head = Paragraph(
            f"<b><font color='{NAVY}' size=10>{s['name']}</font></b>"
            + (f" &nbsp;<font size=9 color='"
               f"{GREEN if s['score'] >= 80 else AMBER if s['score'] >= 60 else RED}'>"
               f"{s['score']}/100</font>" if s.get('score') is not None
               else f" &nbsp;<font size=8 color='{GREY}'>not measured</font>"),
            BODY)
        bits = [head, Paragraph(s.get('note', ''), SMALL)]
        rows = s.get('rows') or []
        if rows:
            data = [[Paragraph(f"<font size=8 color='{GREY}'>{k}</font>", BODY),
                     Paragraph(f"<font size=8>{v}</font>", BODY)] for k, v in rows]
            rt = Table(data, colWidths=[52 * mm, doc.width - 52 * mm])
            rt.setStyle(TableStyle([
                ('LINEBELOW', (0, 0), (-1, -2), 0.4, HexColor('#f1f5f9')),
                ('TOPPADDING', (0, 0), (-1, -1), 2.5),
                ('BOTTOMPADDING', (0, 0), (-1, -1), 2.5),
                ('LEFTPADDING', (0, 0), (-1, -1), 0),
            ]))
            bits.append(rt)
        bits.append(Spacer(1, 6))
        story.append(KeepTogether(bits))

    # ── findings ─────────────────────────────────────────────────────────
    story.append(Paragraph('What we found', H2))
    if not audit['findings']:
        story.append(Paragraph('Nothing needs attention. Every check passed.', BODY))
    else:
        data = [[Paragraph("<b><font size=8>Priority</font></b>", BODY),
                 Paragraph("<b><font size=8>What we found</font></b>", BODY),
                 Paragraph("<b><font size=8>What we suggest</font></b>", BODY)]]
        for f in audit['findings']:
            col = SEVERITY_COLOUR.get(f['severity'], GREY)
            data.append([
                Paragraph(f"<font size=8 color='{col}'><b>"
                          f"{SEVERITY_LABEL.get(f['severity'], f['severity'])}</b></font>", BODY),
                Paragraph(f"<font size=8><b>{f['area']}</b>. {f['detail']}</font>", BODY),
                Paragraph(f"<font size=8 color='{GREY}'>{f.get('action', '')}</font>", BODY),
            ])
        ft = Table(data, colWidths=[26 * mm, (doc.width - 26 * mm) * 0.52,
                                    (doc.width - 26 * mm) * 0.48], repeatRows=1)
        ft.setStyle(TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), HexColor('#f8fafc')),
            ('LINEBELOW', (0, 0), (-1, -1), 0.4, HexColor(LINE)),
            ('VALIGN', (0, 0), (-1, -1), 'TOP'),
            ('TOPPADDING', (0, 0), (-1, -1), 4),
            ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
            ('LEFTPADDING', (0, 0), (-1, -1), 5),
            ('BOX', (0, 0), (-1, -1), 0.4, HexColor(LINE)),
        ]))
        story.append(ft)

    # ── how we would fix it ──────────────────────────────────────────────
    recs = audit.get('recommendations') or []
    if recs:
        story.append(Spacer(1, 6))
        story.append(Paragraph('How these can be put right', H2))
        story.append(Paragraph(
            'Every item below answers something measured in this report. Each is work you can '
            'carry out yourself; the offer is that we do it for you, with the result checked '
            'against a second audit afterwards.', BODY))
        rows = [[Paragraph("<b>What</b>", BODY),
                 Paragraph("<b>Why, from this report</b>", BODY)]]
        for r in recs:
            rows.append([
                Paragraph(f"<b>{r['title']}</b>", BODY),
                Paragraph(f"<font size=8 color='{GREY}'>{r['why']}</font><br/>{r['blurb']}", BODY),
            ])
        rt = Table(rows, colWidths=[45 * mm, doc.width - 45 * mm], repeatRows=1)
        rt.setStyle(TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), HexColor('#f8fafc')),
            ('LINEBELOW', (0, 0), (-1, -1), 0.4, HexColor(LINE)),
            ('VALIGN', (0, 0), (-1, -1), 'TOP'),
            ('TOPPADDING', (0, 0), (-1, -1), 5),
            ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
            ('LEFTPADDING', (0, 0), (-1, -1), 5),
            ('BOX', (0, 0), (-1, -1), 0.4, HexColor(LINE)),
        ]))
        story.append(rt)
        story.append(Spacer(1, 4))
        story.append(Paragraph(
            'To discuss any of these, reply to the email this report came with, or open a ticket '
            'in your panel. We will come back with a fixed price and a timescale.', SMALL))

    # ── scope ────────────────────────────────────────────────────────────
    story.append(Paragraph('What this report does not cover', H2))
    story.append(Paragraph(
        'This measures how the site behaves from outside, and what the platform reports about '
        'itself: availability, speed, certificate, security headers and software versions. It '
        'does not judge visual design, code quality or the accuracy of content, because those '
        'cannot be measured automatically and we will not imply otherwise. It is not a '
        'penetration test. Both are separate pieces of work we are happy to quote for.', SMALL))

    doc.build(story)
    NAVY, GREEN, AMBER, RED, GREY, LINE = _palette  # never leak the grey palette
    buf.seek(0)
    return buf.read()
