#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# robocop-eu-to-json
#
# Copyright (C) 2025 Taler Systems SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
EU Consolidated Financial Sanctions List XML to robocop JSON converter.

Converts the EU's consolidated list (the <export>/<sanctionEntity> schema served
by the European Commission FISMA "FSD" download, default namespace
http://eu.europa.ec/fpi/fsd/export) into robocop's internal JSON format: a flat
JSON array of self-contained target records keyed by a string "ssid", with the
same registry field names emitted by robocop-ch-to-json (PERSON_FIRST_NAMES,
PERSON_LAST_NAME, FULL_NAME, DATE_OF_BIRTH, NATIONALITY, PERSON_NATIONAL_ID,
COMPANY_NAME, ADDRESS_* / REGISTERED_OFFICE_ADDRESS_*, ...).

Each record's ssid is namespaced "EU-<logicalId>" so records stay unique when
several authorities' lists are combined.

Usage:
    robocop-eu-to-json < eu.xml | robocop-json-postprocess > eu.json
"""

import xml.etree.ElementTree as ET
import json
import sys
import argparse


def strip_ns(root):
    """Drop XML namespaces so elements can be matched by their local name."""
    for el in root.iter():
        if isinstance(el.tag, str) and "}" in el.tag:
            el.tag = el.tag.split("}", 1)[1]
    return root


def add(rec, key, value):
    """Append a non-empty, stripped string value to a list field."""
    if value is None:
        return
    value = value.strip()
    if not value:
        return
    rec.setdefault(key, []).append(value)


def dedupe(rec):
    """Remove duplicate values from every list field, preserving order."""
    for key, val in rec.items():
        if isinstance(val, list):
            seen = set()
            rec[key] = [x for x in val if not (x in seen or seen.add(x))]
    return rec


def convert(root):
    targets = []
    for ent in root.findall("sanctionEntity"):
        logical_id = ent.get("logicalId") or ent.get("euReferenceNumber") or ""
        rec = {
            "ssid": "EU-{}".format(logical_id),
            "foreign_identifier": ent.get("euReferenceNumber") or None,
            "united_nation_id": ent.get("unitedNationId") or None,
            "justification": [],
            "other_information": [],
        }

        subject = ent.find("subjectType")
        code = subject.get("code") if subject is not None else None
        is_entity = code in ("enterprise", "vessel", "ship", "aircraft")
        rec["target_type"] = "entity" if is_entity else (
            "individual" if code == "person" else "other")

        addr_prefix = "REGISTERED_OFFICE_ADDRESS_" if is_entity else "ADDRESS_"

        # Names (one <nameAlias> per spelling / alias).
        for na in ent.findall("nameAlias"):
            first = na.get("firstName") or ""
            middle = na.get("middleName") or ""
            last = na.get("lastName") or ""
            whole = na.get("wholeName") or ""
            gender = na.get("gender") or ""
            if gender and "sex" not in rec:
                rec["sex"] = {"M": "male", "F": "female"}.get(gender, gender)
            if not whole:
                whole = " ".join(p for p in (first, middle, last) if p)
            if is_entity:
                add(rec, "COMPANY_NAME", whole)
                add(rec, "BUSINESS_DISPLAY_NAME", whole)
            else:
                add(rec, "PERSON_FIRST_NAMES", first)
                add(rec, "PERSON_FIRST_NAMES", middle)
                add(rec, "PERSON_LAST_NAME", last)
                add(rec, "FULL_NAME", whole)
            add(rec, "other_information", na.get("function"))

        # Citizenship -> nationality.
        for cit in ent.findall("citizenship"):
            add(rec, "NATIONALITY", cit.get("countryIso2Code"))

        # Birth dates and places.
        for bd in ent.findall("birthdate"):
            iso = bd.get("birthdate")
            if iso:
                add(rec, "DATE_OF_BIRTH", iso)
            elif bd.get("year"):
                add(rec, "DATE_OF_BIRTH", bd.get("year"))
            pob = ", ".join(p for p in (bd.get("city"), bd.get("countryDescription")) if p)
            if pob:
                add(rec, "other_information", "Place of birth: " + pob)

        # Addresses.
        for ad in ent.findall("address"):
            line = ", ".join(p for p in (ad.get("street"), ad.get("poBox"),
                                         ad.get("place")) if p)
            add(rec, addr_prefix + "LINES", line)
            add(rec, addr_prefix + "ZIPCODE", ad.get("zipCode"))
            add(rec, addr_prefix + "TOWN_LOCATION", ad.get("city"))
            add(rec, addr_prefix + "COUNTRY_SUBDIVISION", ad.get("region"))
            add(rec, addr_prefix + "COUNTRY", ad.get("countryIso2Code"))

        # Identification documents.
        for ident in ent.findall("identification"):
            number = ident.get("number") or ident.get("latinNumber")
            if number:
                add(rec, "PERSON_NATIONAL_ID", number)

        # Remarks / statement of reasons.
        for rmk in ent.findall("remark"):
            add(rec, "justification", rmk.text)

        targets.append(dedupe(rec))
    return targets


def main():
    parser = argparse.ArgumentParser(
        description="Convert the EU consolidated sanctions list (XML) to robocop JSON")
    parser.add_argument("--input", help="Input XML file (default: stdin)")
    parser.add_argument("--output", "-o", help="Output JSON file (default: stdout)")
    parser.add_argument("--indent", type=int, default=2)
    args = parser.parse_args()

    tree = ET.parse(args.input) if args.input else ET.parse(sys.stdin)
    root = strip_ns(tree.getroot())
    targets = convert(root)

    out = open(args.output, "w", encoding="utf-8") if args.output else sys.stdout
    json.dump(targets, out, indent=args.indent, ensure_ascii=False)
    if args.output:
        out.close()
    print("EU: converted {} targets".format(len(targets)), file=sys.stderr)


if __name__ == "__main__":
    main()
