"""Turning issues into fixes: a reviewable pandas script, or a cleaned copy of the table."""
import math
import re

from .csvio import Table
from .jscompat import js_str, py_str, trim
from .checks import py_list
from .values import MISSING_SET, is_missing_token, parse_num, parse_num_text

FIX_ORDER = ["missing", "numeric", "map", "date", "dropMissingTarget", "dedupe", "drop", "shuffle"]


def _rank(issue, no_action=99):
    if not issue.get("action"):
        return no_action
    k = next(iter(issue["action"]))
    return FIX_ORDER.index(k) if k in FIX_ORDER else 50


def cleaning_script(report, selected=None, filename=None):
    """pandas code that applies the fixes for the selected issue ids (default: every issue with
    an automatic fix)."""
    issues = report["issues"]
    sel = [i for i in issues if i["id"] in selected] if selected is not None else [i for i in issues if i.get("action")]
    fname = filename or "data.csv"
    lines = [
        "# Cleaning script generated by Dataset Doctor (williamc.me/projects/dataset-doctor)",
        "# Review each step: every fix is a suggestion, not a rule.",
        "import numpy as np",
        "import pandas as pd",
        "",
        "df = pd.read_csv(" + py_str(fname) + ")",
        "",
    ]
    drops = []
    for issue in sorted(sel, key=_rank):
        action = issue.get("action")
        if action and action.get("drop"):
            for c in action["drop"]:
                if c not in drops:
                    drops.append(c)
            continue
        lines += ["# " + re.sub(r"[\r\n]+", " ", issue["title"]), issue["fix"], ""]
    if drops:
        lines += ["# Drop columns flagged as constant, ID, personal data, redundant or leaking",
                  "df = df.drop(columns=" + py_list(drops) + ", errors=\"ignore\")", ""]
    lines.append("df.to_csv(" + py_str(re.sub(r"\.csv\Z", "", fname, flags=re.I) + "_clean.csv") + ", index=False)")
    return "\n".join(lines) + "\n"


def apply_fixes(table, report, selected=None):
    """Apply the selected automatic fixes to the raw table and return a new Table.

    The returned table has a `log` attribute listing what was changed.
    """
    sel = [i for i in report["issues"] if i.get("action") and (selected is None or i["id"] in selected)]
    cols = list(table.columns)
    rows = [list(r) for r in table.rows]
    by_name = {p.name: p for p in report["_prof"]}
    log = []
    sel.sort(key=lambda i: _rank(i))
    drops, dedupe, shuffle = [], None, False
    for issue in sel:
        a = issue["action"]
        if a.get("missing"):
            m = a["missing"]
            j, cnt = cols.index(m["column"]), 0
            for r in rows:
                v = r[j]
                t = trim(v)
                if t != "" and t.lower() in MISSING_SET:
                    r[j] = ""
                    cnt += 1
                    continue
                x = parse_num(v)
                if x is None:
                    x = parse_num_text(v)
                if x is not None and x in m["numbers"]:
                    r[j] = ""
                    cnt += 1
            log.append(m["column"] + ": " + str(cnt) + " placeholder values blanked")
        elif a.get("numeric"):
            j, c2 = cols.index(a["numeric"]), 0
            for r in rows:
                v = r[j]
                if trim(v) == "":
                    continue
                x = parse_num(v)
                if x is not None:
                    r[j] = trim(v)
                    continue
                x = parse_num_text(v)
                if x is not None:
                    r[j] = js_str(x)
                    c2 += 1
                    continue
                r[j] = ""
                c2 += 1
            log.append(a["numeric"] + ": " + str(c2) + " values converted to plain numbers")
        elif a.get("map"):
            j, mp, c3 = cols.index(a["map"]["column"]), a["map"]["mapping"], 0
            for r in rows:
                v = r[j]
                if v in mp:
                    r[j] = mp[v]
                    c3 += 1
                elif v != trim(v):
                    r[j] = trim(v)
            log.append(a["map"]["column"] + ": " + str(c3) + " spellings merged")
        elif a.get("date"):
            j, p, c4 = cols.index(a["date"]), by_name[a["date"]], 0
            for i, r in enumerate(rows):
                if p.iso and i < len(p.iso) and p.iso[i] and r[j] != p.iso[i]:
                    r[j] = p.iso[i]
                    c4 += 1
            log.append(a["date"] + ": " + str(c4) + " dates rewritten as YYYY-MM-DD")
        elif a.get("dropMissingTarget"):
            j, before = cols.index(a["dropMissingTarget"]), len(rows)
            rows = [r for r in rows if not is_missing_token(r[j])]
            log.append(str(before - len(rows)) + " rows without a label removed")
        elif a.get("dedupe"):
            if not dedupe or a["dedupe"] == "noid":
                dedupe = a
        elif a.get("drop"):
            for c in a["drop"]:
                if c not in drops:
                    drops.append(c)
        elif a.get("shuffle"):
            shuffle = True
    if dedupe:
        skip = set(cols.index(c) for c in dedupe["idCols"]) if dedupe["dedupe"] == "noid" else set()
        seen, before, kept = set(), len(rows), []
        for r in rows:
            k = "\x01".join(v for j, v in enumerate(r) if j not in skip)
            if k in seen:
                continue
            seen.add(k)
            kept.append(r)
        rows = kept
        log.append(str(before - len(rows)) + " duplicate rows removed")
    if drops:
        keep = [j for j, c in enumerate(cols) if c not in drops]
        cols = [cols[j] for j in keep]
        rows = [[r[j] for j in keep] for r in rows]
        log.append("dropped columns: " + ", ".join(drops))
    if shuffle:
        s = 12345                                   # seeded shuffle so results are reproducible
        for i in range(len(rows) - 1, 0, -1):
            s = (s * 1103515245 + 12345) % 4294967296
            k = math.floor(s / 4294967296 * (i + 1))       # high bits of the LCG
            rows[i], rows[k] = rows[k], rows[i]
        log.append("rows shuffled")
    out = Table(cols, rows, ",", 0)
    out.log = log
    return out
