"""Confidential metadata scanner (R-4).

docx: core properties (author, last-modified-by, company), every review comment, and
every tracked change (insertion/deletion with author and timestamp), read straight from
the package XML so nothing is missed. pdf: Author, Creator, Producer, and XMP fields.
images: EXIF including GPS, Artist, and Software. Findings carry the field path as the
location and a value preview.
"""
from __future__ import annotations

import xml.etree.ElementTree as ET
import zipfile
from typing import Optional

from ..models import Finding
from ..severity import severity_for

_W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
_DC = "{http://purl.org/dc/elements/1.1/}"
_CP = "{http://schemas.openxmlformats.org/package/2006/metadata/core-properties}"
_EP = "{http://schemas.openxmlformats.org/officeDocument/2006/extended-properties}"


def _finding(file: str, ftype: str, location: str, value: str,
             detail: Optional[dict] = None) -> Finding:
    preview = (value or "").strip()
    if len(preview) > 120:
        preview = preview[:117] + "..."
    return Finding(
        detector=f"metadata.{ftype}",
        type=ftype,
        severity=severity_for(ftype),
        file=file,
        location=location,
        preview=preview,
        detail=detail or {},
    )


def _text(el) -> str:
    return "".join(t.text or "" for t in el.iter(f"{_W}t"))


def _deltext(el) -> str:
    return "".join(t.text or "" for t in el.iter(f"{_W}delText"))


def _docx_metadata(path: str) -> list[Finding]:
    findings: list[Finding] = []
    with zipfile.ZipFile(path) as z:
        members = set(z.namelist())

        if "docProps/core.xml" in members:
            root = ET.fromstring(z.read("docProps/core.xml"))
            creator = root.findtext(f"{_DC}creator")
            last_mod = root.findtext(f"{_CP}lastModifiedBy")
            if creator:
                findings.append(_finding(path, "author_metadata", "docx.core.author", creator))
            if last_mod:
                findings.append(_finding(path, "author_metadata",
                                         "docx.core.lastModifiedBy", last_mod))

        if "docProps/app.xml" in members:
            root = ET.fromstring(z.read("docProps/app.xml"))
            company = root.findtext(f"{_EP}Company")
            if company:
                findings.append(_finding(path, "author_metadata", "docx.app.company", company))

        if "word/comments.xml" in members:
            root = ET.fromstring(z.read("word/comments.xml"))
            for c in root.findall(f"{_W}comment"):
                author = c.get(f"{_W}author") or "?"
                date = c.get(f"{_W}date") or ""
                findings.append(_finding(
                    path, "comment", f"docx.comment[{author}]", _text(c),
                    detail={"author": author, "date": date}))

        if "word/document.xml" in members:
            root = ET.fromstring(z.read("word/document.xml"))
            for ins in root.iter(f"{_W}ins"):
                author = ins.get(f"{_W}author") or "?"
                date = ins.get(f"{_W}date") or ""
                txt = _text(ins)
                if txt or author != "?":
                    findings.append(_finding(
                        path, "tracked_change", f"docx.insertion[{author}]", txt,
                        detail={"op": "insertion", "author": author, "date": date}))
            for dele in root.iter(f"{_W}del"):
                author = dele.get(f"{_W}author") or "?"
                date = dele.get(f"{_W}date") or ""
                txt = _deltext(dele)
                findings.append(_finding(
                    path, "tracked_change", f"docx.deletion[{author}]", txt,
                    detail={"op": "deletion", "author": author, "date": date}))
    return findings


def _pdf_metadata(path: str) -> list[Finding]:
    from pypdf import PdfReader

    findings: list[Finding] = []
    reader = PdfReader(path)
    md = reader.metadata or {}
    mapping = [
        ("/Author", "author_metadata", "pdf.author"),
        ("/Creator", "software_metadata", "pdf.creator"),
        ("/Producer", "software_metadata", "pdf.producer"),
    ]
    for key, ftype, loc in mapping:
        val = md.get(key)
        if val:
            findings.append(_finding(path, ftype, loc, str(val)))

    try:
        xmp = reader.xmp_metadata
    except Exception:  # noqa: BLE001
        xmp = None
    if xmp is not None:
        for attr, ftype, loc in [
            ("dc_creator", "author_metadata", "pdf.xmp.dc:creator"),
            ("xmp_creator_tool", "software_metadata", "pdf.xmp.xmp:CreatorTool"),
            ("pdf_producer", "software_metadata", "pdf.xmp.pdf:Producer"),
        ]:
            try:
                val = getattr(xmp, attr, None)
            except Exception:  # noqa: BLE001
                val = None
            if val:
                if isinstance(val, (list, tuple)):
                    val = ", ".join(str(v) for v in val)
                findings.append(_finding(path, ftype, loc, str(val)))
    return findings


def _gps_to_decimal(dms, ref) -> Optional[float]:
    try:
        deg, minutes, seconds = (float(x) for x in dms)
    except (TypeError, ValueError):
        return None
    dec = deg + minutes / 60.0 + seconds / 3600.0
    if str(ref).upper() in ("S", "W"):
        dec = -dec
    return dec


def _image_metadata(path: str) -> list[Finding]:
    from PIL import Image

    findings: list[Finding] = []
    with Image.open(path) as img:
        exif = img.getexif()
        if not exif:
            return []
        artist = exif.get(315)   # Artist
        software = exif.get(305)  # Software
        if artist:
            findings.append(_finding(path, "author_metadata", "image.exif.Artist", str(artist)))
        if software:
            findings.append(_finding(path, "software_metadata", "image.exif.Software",
                                     str(software)))
        try:
            gps = exif.get_ifd(0x8825)  # GPSInfo IFD
        except Exception:  # noqa: BLE001
            gps = None
        if gps:
            lat = _gps_to_decimal(gps.get(2), gps.get(1))
            lon = _gps_to_decimal(gps.get(4), gps.get(3))
            if lat is not None and lon is not None:
                findings.append(_finding(
                    path, "exif_gps", "image.exif.GPS",
                    f"{lat:.6f}, {lon:.6f}",
                    detail={"latitude": lat, "longitude": lon}))
    return findings


def scan_metadata(path: str, kind: str) -> list[Finding]:
    if kind == "docx":
        return _docx_metadata(path)
    if kind == "pdf":
        return _pdf_metadata(path)
    if kind == "image":
        return _image_metadata(path)
    return []
