# -*- coding: utf-8 -*-
"""Where the administrative boundaries come from.

The toolkit ships Cambodian NCDD boundaries, but the map layout is not
Cambodia-specific: point it at a folder of admin polygons for any country
and the same cascade, insets and layout style apply.

A "custom folder" holds one to four polygon files (shp/gpkg/geojson), one
per administrative level. The name field, code field and parent-code field
are auto-detected, so the common public datasets work untouched:

  GADM        NAME_1 / GID_1, parent GID_0
  FAO GAUL    ADM1_NAME / ADM1_CODE, parent ADM0_CODE
  OCHA COD    ADM1_EN / ADM1_PCODE, parent ADM0_PCODE
  NCDD (KH)   Name_EN / KHET_CODE, parent (code prefix)

Nothing here talks to Earth Engine or QGIS layout code; it only inspects
vector files and returns plain tuples.
"""

import os
import re

from qgis.core import QgsVectorLayer
from qgis.PyQt.QtCore import QSettings

VECTOR_EXT = (".shp", ".gpkg", ".geojson", ".json")

# ---------------------------------------------------------------- detection
# Level-tagged patterns: the captured digit IS the administrative level, so
# a file carrying GID_0 *and* GID_1 can be told apart from one carrying only
# GID_0. Matching the level first and the field second is what stops a
# level-2 file from being linked through its grandparent's code.
_LEVEL_NAME_RX = [
    r"^name_(\d)$",                    # GADM   NAME_1
    r"^adm(\d)_?(?:en|name|nm)$",      # OCHA ADM1_EN / GAUL ADM1_NAME
    r"^adm(\d)nm\w*$",
    r"^shape(\d)_?name$",
]
_LEVEL_CODE_RX = [
    r"^gid_(\d)$",                     # GADM   GID_1
    r"^adm(\d)_?(?:pcode|code|id)$",   # OCHA ADM1_PCODE / GAUL ADM1_CODE
    r"^adm(\d)pcod\w*$",
    r"^hasc_(\d)$",
]

# Untagged fallbacks, most specific first.
_PLAIN_NAME_RX = [
    r"^name_en$", r"^name_kh$", r"^country$", r"^sovereignt$",
    r"^admin$", r"^name$", r"^nam$", r"^nom$",
    r"^.*_name$", r"^.*_en$",
]
_PLAIN_CODE_RX = [
    r"^(?:khet|srok|khum)_code$",      # NCDD Cambodia
    r"^pcode$", r"^p_code$", r"^code$", r"^iso$", r"^iso3$",
    r"^.*_pcode$", r"^.*_code$", r"^.*_id$",
]

# filename hints for the administrative level
_LEVEL_FROM_NAME = [
    (r"admin_?0|adm_?0(?!\d)|country|national|_0$", 0),
    (r"admin_?1|adm_?1(?!\d)|province|provinc|state|region|khet|_1$", 1),
    (r"admin_?2|adm_?2(?!\d)|district|county|srok|amphoe|_2$", 2),
    (r"admin_?3|adm_?3(?!\d)|commune|ward|khum|village|subdist|_3$", 3),
]


def _fields(path):
    lyr = QgsVectorLayer(path, "probe", "ogr")
    if not lyr.isValid():
        return None, []
    return lyr, [f.name() for f in lyr.fields()]


def _tagged(fields, patterns):
    """{level: field} for every field whose name encodes its level."""
    out = {}
    for f in fields:
        lf = f.lower()
        for pat in patterns:
            m = re.match(pat, lf)
            if m:
                try:
                    lvl = int(m.group(1))
                except (IndexError, ValueError, TypeError):
                    break
                out.setdefault(lvl, f)
                break
    return out


def _plain(fields, patterns):
    """First field matching the ranked untagged patterns."""
    for pat in patterns:
        rx = re.compile(pat)
        for f in fields:
            if rx.match(f.lower()):
                return f
    return None


def _level_from_filename(path):
    base = os.path.splitext(os.path.basename(path))[0].lower()
    for pat, lvl in _LEVEL_FROM_NAME:
        if re.search(pat, base):
            return lvl
    return None


def probe_file(path):
    """Inspect one vector file.

    Returns {path, name_field, code_field, parent_hint, level, level_src,
    count, fields} or None if the file is not a usable admin polygon layer.
    """
    lyr, fields = _fields(path)
    if lyr is None or not fields:
        return None
    try:
        if lyr.geometryType() != 2:          # 2 == polygon
            return None
    except (AttributeError, TypeError):  # QGIS API compatibility
        pass

    t_name = _tagged(fields, _LEVEL_NAME_RX)
    t_code = _tagged(fields, _LEVEL_CODE_RX)

    # The file's own level is the DEEPEST level mentioned by its fields: a
    # GADM level-2 file carries GID_1 (its parent) and GID_2 (itself).
    level = _level_from_filename(path)
    level_src = "filename"
    if level is None:
        seen = set(t_name) | set(t_code)
        if seen:
            level = max(seen)
            level_src = "fields"
        else:
            level_src = "inferred"

    if level is not None:
        nf = t_name.get(level) or _plain(fields, _PLAIN_NAME_RX)
        cf = t_code.get(level) or _plain(fields, _PLAIN_CODE_RX)
        parent_hint = t_code.get(level - 1) if level > 0 else None
    else:
        nf = _plain(fields, _PLAIN_NAME_RX)
        cf = _plain(fields, _PLAIN_CODE_RX)
        parent_hint = None

    # A layer with a code but no obvious name is still usable.
    if not nf:
        nf = cf
    if not nf:
        return None
    return {"path": path, "name_field": nf, "code_field": cf or nf,
            "parent_hint": parent_hint, "level": level,
            "level_src": level_src, "count": lyr.featureCount(),
            "fields": fields}


def scan_folder(folder):
    """All usable admin polygon files in `folder`, ordered coarse -> fine.

    Level comes from the filename or the level-tagged field names. Only when
    a file offers neither is it ordered by feature count, which is a decent
    proxy (a country has fewer polygons than its provinces) but is flagged
    as inferred so the user can see it was a guess.
    """
    if not folder or not os.path.isdir(folder):
        return []
    found = []
    for fn in sorted(os.listdir(folder)):
        if not fn.lower().endswith(VECTOR_EXT):
            continue
        info = probe_file(os.path.join(folder, fn))
        if info:
            found.append(info)
    if not found:
        return []

    known = [f for f in found if f["level"] is not None]
    unknown = [f for f in found if f["level"] is None]
    known.sort(key=lambda d: (d["level"], d["count"]))
    unknown.sort(key=lambda d: d["count"])

    # give the unlevelled files the next free slots below the known ones
    nxt = (known[-1]["level"] + 1) if known else 1
    for f in unknown:
        while any(k["level"] == nxt for k in known):
            nxt += 1
        f["level"] = nxt
        nxt += 1

    out = known + unknown
    out.sort(key=lambda d: d["level"])

    # two files claiming the same level: keep the one with fewer polygons
    # (the coarser of the two) and drop the duplicate rather than guessing
    deduped, taken = [], set()
    for f in out:
        if f["level"] in taken:
            f["duplicate"] = True
            continue
        taken.add(f["level"])
        deduped.append(f)
    return deduped


_PARENT_RX = [
    r"^parent$", r"^parent_?(?:code|id|pcode)$", r"^p_?code$",
    r"^.*_parent$", r"^parent_.*$", r"^upper_?code$",
]


def _parent_field(child, parent):
    """The field in `child` that links it to `parent`, if any.

    Must never return the child's OWN code field: with generic schemas both
    layers often call their key "code", and matching on that would filter
    child codes against parent codes and silently return an empty list.
    """
    own = (child.get("code_field") or "").lower()

    hint = child.get("parent_hint")
    if hint and hint.lower() != own:
        return hint

    explicit = _plain([f for f in child["fields"] if f.lower() != own],
                      _PARENT_RX)
    if explicit:
        return explicit

    if parent is None:
        return None
    pcf = parent["code_field"]
    if pcf.lower() == own:
        return None                      # same name on both levels: unusable
    if pcf in child["fields"]:
        return pcf
    low = {f.lower(): f for f in child["fields"]}
    return low.get(pcf.lower())


DEFAULT_LABELS = ["Country", "Province / State",
                  "District / County", "Commune / Ward"]


def build_config(folder):
    """Turn a folder into the (admin, levels, labels, report) the layout uses.

    `admin` maps a level key to (filename, name_field, code_field,
    parent_code_field) - the same shape as the bundled Cambodia table, so
    every downstream caller works unchanged. Keys are admin<N> where N is
    the DETECTED depth, so a folder holding only province and district
    files is labelled province/district, not country/province.
    """
    files = scan_folder(folder)
    if not files:
        return None, [], {}, "No usable polygon files found in this folder."

    admin, levels, labels = {}, [], {}
    lines = []
    all_inferred = all(f["level_src"] == "inferred" for f in files)
    for i, info in enumerate(files):
        lvl = info["level"]
        key = "admin{}".format(lvl)
        parent = files[i - 1] if i > 0 else None
        pf = _parent_field(info, parent)
        admin[key] = (os.path.basename(info["path"]), info["name_field"],
                      info["code_field"], pf)
        levels.append(key)
        if all_inferred:
            labels[key] = "Level {}".format(i + 1)
        else:
            labels[key] = (DEFAULT_LABELS[lvl] if lvl < len(DEFAULT_LABELS)
                           else "Level {}".format(lvl))
        lines.append(
            "{:<18} {:<28} name='{}'  code='{}'  parent={}  "
            "[{} features, level from {}]".format(
                labels[key], os.path.basename(info["path"]),
                info["name_field"], info["code_field"], pf or "-",
                info["count"], info["level_src"]))
        if i > 0 and pf is None:
            lines.append(
                "     ! no parent-code field found, so this level is NOT "
                "filtered by the level above - every unit will be listed.")
    if all_inferred:
        lines.append("")
        lines.append("! No level could be read from the file names or "
                     "fields, so the order above was guessed from polygon "
                     "counts. Rename the files (admin1, admin2, ...) if "
                     "the order is wrong.")
    return admin, levels, labels, "\n".join(lines)


# ---------------------------------------------------------------- settings
_KEY_KIND = "rupp_gee_lulc/admin_source"      # "bundled" | "custom"
_KEY_FOLDER = "rupp_gee_lulc/admin_folder"


def saved_source():
    s = QSettings()
    return (s.value(_KEY_KIND, "bundled", type=str) or "bundled",
            s.value(_KEY_FOLDER, "", type=str) or "")


def save_source(kind, folder=""):
    s = QSettings()
    s.setValue(_KEY_KIND, kind)
    s.setValue(_KEY_FOLDER, folder or "")
