#!/bin/bash
#
# parse-complaints.sh
# Normalize a batch of customer complaints into the uniform JSON list the
# /multi-agent:complaint-analysis command consumes. Accepts a file export
# (csv / xlsx / txt / json) or pasted free text on stdin, extracts any
# transaction / conversation ids embedded in the text, and redacts common
# PII shapes before anything leaves this script. The raw input is never
# copied into pipeline state - only the redacted view is.
#
# Input forms (exactly one source is required):
#   ./parse-complaints.sh --file <path> [--format csv|xlsx|txt|json|auto]
#   ./parse-complaints.sh --stdin [--format text]
#
# Options:
#   --no-redact   keep complaint text verbatim (debugging only; the skill
#                 never passes this flag)
#
# Column mapping (file formats, case-insensitive header match):
#   trxId   <- trxId | trx | transaction id / transaction_id / transactionId
#   convId  <- convId | conversationId | x-conversationId
#   date    <- date | created | createdAt | timestamp
#   platformHint <- platform | channel | os | device
#   text    <- comment | complaint | text | description | message, else the
#              longest text column
#
# Free-text mode splits on blank lines (or numbered bullets when the paste is
# one block) and pre-fills ids per block using the same label set as
# context-link-extractor.sh GRAYLOG_RE (x-conversationId | conversationId |
# convId | trxId | trx | transaction id).
#
# Redaction (default ON, applied after id extraction so ids survive):
#   email                              -> [REDACTED:email]
#   13-19 digit card-like runs         -> [REDACTED:card]
#   11 digit national-id-like numbers  -> [REDACTED:national-id]
#   9+ digit phone-like runs           -> [REDACTED:phone]
#   6-char PNR-like tokens (A-Z0-9 mix)-> [REDACTED:pnr]
#
# Output (stdout, single JSON object):
#   {
#     "parsedAt": "<ISO8601>",
#     "source": "<path>"|"stdin",
#     "format": "csv|xlsx|txt|json|text",
#     "complaints": [
#       { "id": "C-01", "text": "...", "trxId": "..."|null,
#         "convId": "..."|null, "date": "..."|null,
#         "platformHint": "..."|null }
#     ],
#     "redacted": <bool>, "degraded": <bool>, "degradeReason": "..."|null
#   }
#
# Exit codes:
#   0  success
#   4  bad usage (no source, unknown flag, unreadable file)
#   5  format degrade (xlsx requested but openpyxl is not installed; the
#      degraded JSON on stdout tells the user to export as CSV and re-run)

set -euo pipefail

FILE=""
USE_STDIN="false"
FORMAT="auto"
REDACT="true"

while [ $# -gt 0 ]; do
  case "$1" in
    --file)
      [ $# -ge 2 ] || { echo "ERR: --file needs a value" >&2; exit 4; }
      FILE="$2"; shift 2 ;;
    --stdin)     USE_STDIN="true"; shift ;;
    --format)
      [ $# -ge 2 ] || { echo "ERR: --format needs a value" >&2; exit 4; }
      FORMAT="$2"; shift 2 ;;
    --no-redact) REDACT="false"; shift ;;
    -h|--help)
      echo "usage: $0 --file <path> [--format csv|xlsx|txt|json|auto] [--no-redact] | --stdin [--format text]" >&2
      exit 4 ;;
    *)
      echo "ERR: unexpected arg $1" >&2; exit 4 ;;
  esac
done

if [ -z "$FILE" ] && [ "$USE_STDIN" != "true" ]; then
  echo "ERR: no source (pass --file <path> or --stdin)" >&2
  exit 4
fi
if [ -n "$FILE" ] && [ "$USE_STDIN" = "true" ]; then
  echo "ERR: pass either --file or --stdin, not both" >&2
  exit 4
fi
if [ -n "$FILE" ] && [ ! -r "$FILE" ]; then
  echo "ERR: file not readable: $FILE" >&2
  exit 4
fi

if [ "$FORMAT" = "auto" ]; then
  if [ "$USE_STDIN" = "true" ]; then
    FORMAT="text"
  else
    case "$FILE" in
      *.csv)  FORMAT="csv" ;;
      *.xlsx) FORMAT="xlsx" ;;
      *.json) FORMAT="json" ;;
      *.txt|*.md) FORMAT="txt" ;;
      *)
        echo "ERR: cannot infer format from extension; pass --format" >&2
        exit 4 ;;
    esac
  fi
fi

case "$FORMAT" in
  csv|xlsx|txt|json|text) : ;;
  *) echo "ERR: unknown format $FORMAT" >&2; exit 4 ;;
esac

STDIN_PAYLOAD=""
if [ "$USE_STDIN" = "true" ]; then
  STDIN_PAYLOAD=$(cat)
fi

PC_FILE="$FILE" PC_FORMAT="$FORMAT" PC_REDACT="$REDACT" PC_STDIN="$STDIN_PAYLOAD" \
python3 - <<'PY'
import csv, datetime, io, json, os, re, sys

fmt = os.environ["PC_FORMAT"]
path = os.environ.get("PC_FILE") or ""
redact_on = os.environ.get("PC_REDACT") == "true"

# Same label set as context-link-extractor.sh GRAYLOG_RE.
ID_LABEL = re.compile(
    r"(?P<label>x-conversationId|conversationId|convId|trxId|trx|transaction[ _-]?id)"
    r"\s*[:=#]?\s*(?P<value>[A-Za-z0-9][A-Za-z0-9._-]{5,63})",
    re.IGNORECASE,
)

def extract_ids(text):
    trx, conv = None, None
    for m in ID_LABEL.finditer(text or ""):
        label = m.group("label").lower()
        value = m.group("value")
        if "conv" in label:
            conv = conv or value
        else:
            trx = trx or value
    return trx, conv

# Order matters: card (13-19 digits, separators allowed) before national-id
# (exactly 11) before phone (9+); a 16-digit card must not survive as a
# "phone". PNR-like runs last so redacted placeholders are not re-matched.
REDACTIONS = [
    (re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "[REDACTED:email]"),
    (re.compile(r"\b\d(?:[ -]?\d){12,18}\b"), "[REDACTED:card]"),
    (re.compile(r"\b\d{11}\b"), "[REDACTED:national-id]"),
    (re.compile(r"(?<![\w.-])\+?\d(?:[ ()-]?\d){8,14}\b"), "[REDACTED:phone]"),
    (re.compile(r"\b(?=[A-Z0-9]{6}\b)(?=[A-Z0-9]*[A-Z])(?=[A-Z0-9]*\d)[A-Z0-9]{6}\b"), "[REDACTED:pnr]"),
    # All-letter PNRs exist too, but a bare 6-letter uppercase word is far too
    # common to redact blind - only take one that follows a PNR-ish keyword.
    (
        re.compile(r"\b(?P<kw>pnr|rezervasyon kodu|booking (?:ref|reference|code))(?P<sep>[^A-Za-z0-9\n]{0,3})[A-Z]{6}\b", re.IGNORECASE),
        lambda m: m.group("kw") + m.group("sep") + "[REDACTED:pnr]",
    ),
]

def redact(text):
    if not redact_on:
        return text
    for rx, repl in REDACTIONS:
        text = rx.sub(repl, text)
    return text

TEXT_KEYS = ("comment", "complaint", "text", "description", "message", "yorum", "sikayet")
TRX_KEYS = re.compile(r"^(trxid|trx|transaction[ _-]?id)$", re.IGNORECASE)
CONV_KEYS = re.compile(r"^(convid|conversationid|x-conversationid)$", re.IGNORECASE)
DATE_KEYS = re.compile(r"^(date|created|createdat|timestamp)$", re.IGNORECASE)
PLATFORM_KEYS = re.compile(r"^(platform|channel|os|device)$", re.IGNORECASE)

def row_to_complaint(row):
    text_val, trx, conv, date, platform = None, None, None, None, None
    longest = ""
    for key, raw in row.items():
        if key is None or raw is None:
            continue
        val = str(raw).strip()
        if not val:
            continue
        k = str(key).strip()
        if TRX_KEYS.match(k):
            trx = val
        elif CONV_KEYS.match(k):
            conv = val
        elif DATE_KEYS.match(k):
            date = val
        elif PLATFORM_KEYS.match(k):
            platform = val
        elif k.lower() in TEXT_KEYS:
            text_val = val
        elif len(val) > len(longest):
            longest = val
    text = text_val or longest
    if not text:
        return None
    found_trx, found_conv = extract_ids(text)
    return {
        "text": text,
        "trxId": trx or found_trx,
        "convId": conv or found_conv,
        "date": date,
        "platformHint": platform,
    }

def blocks_from_text(payload):
    payload = payload.strip()
    if not payload:
        return []
    blocks = [b.strip() for b in re.split(r"\n\s*\n", payload) if b.strip()]
    if len(blocks) == 1 and re.search(r"^\s*(?:\d+[.)]|-)\s+", payload, re.MULTILINE):
        blocks = [
            b.strip()
            for b in re.split(r"^\s*(?:\d+[.)]|-)\s+", payload, flags=re.MULTILINE)
            if b.strip()
        ]
    return blocks

degraded = False
degrade_reason = None
raw_complaints = []

if fmt == "text":
    for block in blocks_from_text(os.environ.get("PC_STDIN") or ""):
        trx, conv = extract_ids(block)
        raw_complaints.append(
            {"text": block, "trxId": trx, "convId": conv, "date": None, "platformHint": None}
        )
elif fmt == "txt":
    with open(path, encoding="utf-8-sig", errors="replace") as f:
        payload = f.read()
    for block in blocks_from_text(payload):
        trx, conv = extract_ids(block)
        raw_complaints.append(
            {"text": block, "trxId": trx, "convId": conv, "date": None, "platformHint": None}
        )
elif fmt == "csv":
    with open(path, encoding="utf-8-sig", errors="replace", newline="") as f:
        sample = f.read(4096)
        f.seek(0)
        try:
            dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
        except csv.Error:
            dialect = csv.excel
        for row in csv.DictReader(f, dialect=dialect):
            c = row_to_complaint(row)
            if c:
                raw_complaints.append(c)
elif fmt == "json":
    with open(path, encoding="utf-8-sig") as f:
        data = json.load(f)
    if isinstance(data, dict):
        data = data.get("complaints") or data.get("items") or []
    if not isinstance(data, list):
        data = []
    for row in data:
        if isinstance(row, str):
            trx, conv = extract_ids(row)
            raw_complaints.append(
                {"text": row.strip(), "trxId": trx, "convId": conv, "date": None, "platformHint": None}
            )
        elif isinstance(row, dict):
            c = row_to_complaint(row)
            if c:
                raw_complaints.append(c)
elif fmt == "xlsx":
    try:
        import openpyxl
    except ImportError:
        degraded = True
        degrade_reason = "openpyxl not installed; export the sheet as CSV and re-run with --format csv"
    if not degraded:
        wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
        ws = wb.active
        rows = ws.iter_rows(values_only=True)
        header = next(rows, None)
        if header:
            keys = [str(h).strip() if h is not None else "" for h in header]
            for values in rows:
                row = {k: v for k, v in zip(keys, values) if k}
                c = row_to_complaint(row)
                if c:
                    raw_complaints.append(c)
        wb.close()

complaints = []
for i, c in enumerate(raw_complaints, start=1):
    complaints.append(
        {
            "id": "C-%02d" % i,
            "text": redact(c["text"]),
            "trxId": c["trxId"],
            "convId": c["convId"],
            "date": c["date"],
            "platformHint": (c["platformHint"] or None) and str(c["platformHint"]).strip().lower(),
        }
    )

result = {
    "parsedAt": datetime.datetime.utcnow().isoformat() + "Z",
    "source": path or "stdin",
    "format": fmt,
    "complaints": complaints,
    "redacted": redact_on,
    "degraded": degraded,
    "degradeReason": degrade_reason,
}
print(json.dumps(result, ensure_ascii=False))
sys.exit(5 if degraded else 0)
PY
