#!/usr/bin/env python3
"""Write the CMS import sheet for a reuse map as <slug>.localization.xlsx and/or .csv.

Each mapping row turns into a single sheet row with the nine columns below.
Text is copied from the row's `new` block and the annotation columns from its
`cms` block; a column stays empty when the mapping has nothing for it. The
workbook is assembled from its XML parts with nothing beyond the stdlib.
"""
import argparse
import csv
import json
import os
import re
import sys
import zipfile
from xml.sax.saxutils import escape

from _shared import clean_key, key_is_dynamic, slug, xml_text

COLUMNS = [
    "Channel", "Property Group", "Property Module", "Key",
    "EN Value", "TR Value", "AR Value", "Anotation EN", "Anotation TR",
]
WIDTHS = [12, 18, 20, 48, 40, 40, 40, 40, 40]

DEFAULT_TAXONOMY = {
    "errors": {"group": "Errors"},
    "lookups": {"group": "Lookups"},
    "validation": {"module": "Validation", "group": "Core"},
    "fields": {"module": "Fields", "group": "Core"},
    "fallback": {"module": "Common", "group": "Core"},
    "domainGroup": "Domains",
    "domains": {
        "auth": ["login", "sign-in", "signin", "signup", "register", "password", "otp", "auth"],
        "account": ["profile", "account", "preferences", "settings"],
        "checkout": ["cart", "checkout", "billing", "payment", "order"],
        "search": ["results", "filter", "search"],
    },
}


def read_json(source):
    with open(source, encoding="utf-8") as stream:
        return json.load(stream)


def load_taxonomy(path):
    taxonomy = json.loads(json.dumps(DEFAULT_TAXONOMY))
    if not path:
        return taxonomy
    user = read_json(path)
    if not isinstance(user, dict):
        raise SystemExit(f"--taxonomy must hold a JSON object: {path}")
    for key, value in user.items():
        if isinstance(value, dict) and isinstance(taxonomy.get(key), dict):
            taxonomy[key].update(value)
        else:
            taxonomy[key] = value
    return taxonomy


def bucket(key, taxonomy):
    lowered = key.lower()
    if "error" in lowered:
        entry = taxonomy["errors"]
    elif "lookup" in lowered:
        entry = taxonomy["lookups"]
    elif "validation" in lowered or "invalid" in lowered or lowered.endswith(".valid"):
        entry = taxonomy["validation"]
    elif any(word in lowered for word in ("field", "placeholder", "hint", "label")):
        entry = taxonomy["fields"]
    else:
        first = re.split(r"[.\-_/]", lowered, maxsplit=1)[0]
        for domain, words in (taxonomy.get("domains") or {}).items():
            if first in words or any(word in lowered for word in words):
                return taxonomy.get("domainGroup", "Domains"), domain
        entry = taxonomy["fallback"]
    return entry.get("group", ""), entry.get("module", "")


def table_rows(mapping, channel, taxonomy):
    out, skipped = [], []
    for number, row in enumerate(mapping.get("rows") or [], 1):
        key = clean_key(row.get("newKey"))
        if not key or key_is_dynamic(key):
            skipped.append((number, key))
            continue
        group, module = bucket(key, taxonomy)
        if row.get("propertyGroup"):
            group = row["propertyGroup"]
        if row.get("propertyModule"):
            module = row["propertyModule"]
        new = row.get("new") or {}
        cms = row.get("cms") or {}
        out.append([
            channel, group, module, key,
            new.get("en") or "", new.get("tr") or "", new.get("ar") or "",
            cms.get("en") or "", cms.get("tr") or "",
        ])
    return out, skipped


def column_letter(index):
    letters = ""
    index += 1
    while index:
        index, rem = divmod(index - 1, 26)
        letters = chr(65 + rem) + letters
    return letters


XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
OOXML = "http://schemas.openxmlformats.org/"
NS_SHEET = OOXML + "spreadsheetml/2006/main"
NS_PKG_REL = OOXML + "package/2006/relationships"
NS_TYPES = OOXML + "package/2006/content-types"
NS_DOC_REL = OOXML + "officeDocument/2006/relationships"
MIME_OOXML = "application/vnd.openxmlformats-"
SHEET_NAME = "Localization"


def el(name, attrs=(), *children):
    opening = name + "".join(f' {k}="{v}"' for k, v in attrs)
    if not children:
        return f"<{opening}/>"
    return f"<{opening}>{''.join(children)}</{name}>"


def part(root, attrs, *children):
    return XML_DECL + el(root, attrs, *children)


def cell(ref, value, style):
    text = escape(xml_text(value)).replace("\n", "&#10;")
    attrs = [("r", ref), ("t", "inlineStr")]
    if style:
        attrs.append(("s", style))
    return el("c", attrs, el("is", (), el("t", [("xml:space", "preserve")], text)))


def sheet_xml(rows):
    span = f"A1:{column_letter(len(COLUMNS) - 1)}{len(rows) + 1}"
    widths = [
        el("col", [("min", n), ("max", n), ("width", width), ("customWidth", 1)])
        for n, width in enumerate(WIDTHS, 1)
    ]
    lines = []
    for number, values in enumerate([COLUMNS] + rows, 1):
        header = 1 if number == 1 else 0
        cells = [cell(f"{column_letter(c)}{number}", v, header) for c, v in enumerate(values)]
        lines.append(el("row", [("r", number)], *cells))
    frozen = el("pane", [("ySplit", 1), ("topLeftCell", "A2"), ("activePane", "bottomLeft"), ("state", "frozen")])
    return part(
        "worksheet", [("xmlns", NS_SHEET)],
        el("dimension", [("ref", span)]),
        el("sheetViews", (), el("sheetView", [("workbookViewId", 0)], frozen)),
        el("cols", (), *widths),
        el("sheetData", (), *lines),
        el("autoFilter", [("ref", span)]),
    )


def font(bold=False, rgb=None):
    pieces = [el("b")] if bold else []
    pieces.append(el("sz", [("val", 11)]))
    if rgb:
        pieces.append(el("color", [("rgb", rgb)]))
    pieces.append(el("name", [("val", "Calibri")]))
    return el("font", (), *pieces)


def fill(kind, *colors):
    return el("fill", (), el("patternFill", [("patternType", kind)], *colors))


def xf(font_id, fill_id, *extra):
    ids = [("numFmtId", 0), ("fontId", font_id), ("fillId", fill_id), ("borderId", 0)]
    return el("xf", ids + list(extra))


def counted(name, *items):
    return el(name, [("count", len(items))], *items)


def styles_xml():
    edges = [el(side) for side in ("left", "right", "top", "bottom", "diagonal")]
    solid = fill("solid", el("fgColor", [("rgb", "FF4472C4")]), el("bgColor", [("indexed", 64)]))
    return part(
        "styleSheet", [("xmlns", NS_SHEET)],
        counted("fonts", font(), font(bold=True, rgb="FFFFFFFF")),
        counted("fills", fill("none"), fill("gray125"), solid),
        counted("borders", el("border", (), *edges)),
        counted("cellStyleXfs", xf(0, 0)),
        counted("cellXfs", xf(0, 0, ("xfId", 0)), xf(1, 2, ("xfId", 0), ("applyFont", 1), ("applyFill", 1))),
    )


def content_types_xml():
    kinds = {
        "/xl/workbook.xml": "spreadsheetml.sheet.main",
        "/xl/worksheets/sheet1.xml": "spreadsheetml.worksheet",
        "/xl/styles.xml": "spreadsheetml.styles",
    }
    overrides = [
        el("Override", [("PartName", name), ("ContentType", f"{MIME_OOXML}officedocument.{kind}+xml")])
        for name, kind in kinds.items()
    ]
    return part(
        "Types", [("xmlns", NS_TYPES)],
        el("Default", [("Extension", "rels"), ("ContentType", MIME_OOXML + "package.relationships+xml")]),
        el("Default", [("Extension", "xml"), ("ContentType", "application/xml")]),
        *overrides,
    )


def relationships_xml(*links):
    items = [
        el("Relationship", [("Id", f"rId{n}"), ("Type", f"{NS_DOC_REL}/{kind}"), ("Target", target)])
        for n, (kind, target) in enumerate(links, 1)
    ]
    return part("Relationships", [("xmlns", NS_PKG_REL)], *items)


def workbook_xml():
    last = column_letter(len(COLUMNS) - 1)
    filter_name = el(
        "definedName", [("name", "_xlnm._FilterDatabase"), ("localSheetId", 0), ("hidden", 1)],
        f"{SHEET_NAME}!$A$1:${last}$1",
    )
    return part(
        "workbook", [("xmlns", NS_SHEET), ("xmlns:r", NS_DOC_REL)],
        el("sheets", (), el("sheet", [("name", SHEET_NAME), ("sheetId", 1), ("r:id", "rId1")])),
        el("definedNames", (), filter_name),
    )


def write_xlsx(path, rows):
    members = [
        ("[Content_Types].xml", content_types_xml()),
        ("_rels/.rels", relationships_xml(("officeDocument", "xl/workbook.xml"))),
        ("xl/workbook.xml", workbook_xml()),
        ("xl/_rels/workbook.xml.rels", relationships_xml(("worksheet", "worksheets/sheet1.xml"), ("styles", "styles.xml"))),
        ("xl/styles.xml", styles_xml()),
        ("xl/worksheets/sheet1.xml", sheet_xml(rows)),
    ]
    with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
        for member, xml in members:
            archive.writestr(member, xml)


def write_csv(path, rows):
    with open(path, "w", newline="", encoding="utf-8-sig") as sink:
        out = csv.writer(sink)
        out.writerow(COLUMNS)
        out.writerows(rows)


def main():
    cli = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    cli.add_argument("mapping", help="reuse-map JSON file; pass - to read it from stdin")
    cli.add_argument("--out", dest="target_dir", default=".", help="folder that receives the generated files")
    cli.add_argument("--slug", help="stem for the file names; derived from the screen name when omitted")
    cli.add_argument("--channel", default="Mobile", help="text placed in every Channel cell")
    cli.add_argument("--taxonomy", help="JSON object laid over the built-in Property Group / Module rules")
    cli.add_argument("--csv", action="store_true", help="emit a CSV next to the workbook")
    cli.add_argument("--csv-only", action="store_true", help="emit only the CSV, no workbook")
    opts = cli.parse_args()

    mapping = json.load(sys.stdin) if opts.mapping == "-" else read_json(opts.mapping)
    if not isinstance(mapping, dict) or "rows" not in mapping:
        cli.error("mapping must be a JSON object with rows")

    rows, skipped = table_rows(mapping, opts.channel, load_taxonomy(opts.taxonomy))
    stem = opts.slug or slug(mapping.get("screen"))
    os.makedirs(opts.target_dir, exist_ok=True)

    def target(ext):
        return os.path.join(opts.target_dir, f"{stem}.localization.{ext}")

    produced = []
    if not opts.csv_only:
        produced.append(target("xlsx"))
        write_xlsx(produced[-1], rows)
    if opts.csv_only or opts.csv:
        produced.append(target("csv"))
        write_csv(produced[-1], rows)

    summary = {"slug": stem, "rows": len(rows), "columns": COLUMNS, "channel": opts.channel, "written": produced}
    json.dump(summary, sys.stdout, indent=2, ensure_ascii=False)
    sys.stdout.write("\n")
    if skipped:
        print(f"# {len(skipped)} row(s) left out of the sheet: no literal key (empty, dynamic or <placeholder>)", file=sys.stderr)
        for number, key in skipped:
            print(f"#   row {number}: {key or '(empty)'}", file=sys.stderr)


if __name__ == "__main__":
    main()
