#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# KSeFService.sh – Linux Debian
# Wersja: 1.07
# Właściciel: ksefservice.pl
# Data: 2026-03-30
#
# Zmiany:
# - Dodano DEMO do konfiguracji api_env
# - Poprawiono validate_xsd w trybie Batch (bierze z konfiguracji)
# - Stabilniejsze ustalanie ścieżki skryptu dla harmonogramu
# - Konfiguracja tylko przy pierwszym uruchomieniu
# - Hasło: libsecret (secret-tool) lub fallback do pliku (chmod 600)
# - Tryb: interactive / batch
# - Batch: ZIP lub XML pakowane do ZIP
# - Archiwizacja wejściowych plików do:
#   OUTPUT/archive/sent oraz OUTPUT/archive/error
# - Opcjonalnie: instalacja/usunięcie zadania przez cron
# - Obsługa upoXml / upo_xml / raw_upo
# - Obsługa ksefNumber
# - Fallback pobierania UPO z endpointu /api/v1/invoices/upo
# - Zapis mapowania plik -> numer KSeF
# - NOWE: poprawne mapowanie InvoiceNumber -> KsefNumber
# - NOWE: odczyt numerów faktur z XML i ZIP
# - NOWE: wykrywanie duplikatów / odrzuceń w Batch
# - NOWE: lepsza decyzja o archiwizacji (UPO / numer KSeF / success)
# - NOWE: obsługa referenceNumber / invoiceReference dla pobierania UPO
# - NOWE: filtrowanie UPO / map / logów z katalogu wejściowego
# - NOWE: ostrzeżenie przy wspólnym INPUT_DIR i OUTPUT_DIR
# ============================================================

APP_NAME="KSeFService"

# ---------------- stabilne ustalenie ścieżki skryptu ----------------
resolve_script_path() {
  local src="${BASH_SOURCE[0]:-$0}"

  while [[ -L "$src" ]]; do
    local dir
    dir="$(cd -P "$(dirname "$src")" >/dev/null 2>&1 && pwd)"
    src="$(readlink "$src")"
    [[ "$src" != /* ]] && src="$dir/$src"
  done

  cd -P "$(dirname "$src")" >/dev/null 2>&1 && pwd
}

SCRIPT_DIR="$(resolve_script_path)"
SCRIPT_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]:-$0}")"

CFG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ksefservice"
CFG_FILE="$CFG_DIR/config.env"

ENDPOINT_INTERACTIVE="https://ksefservice.pl/api/v1/invoices/send"
ENDPOINT_BATCH="https://ksefservice.pl/api/v1/invoices/send-batch"
ENDPOINT_UPO="https://ksefservice.pl/api/v1/invoices/upo"

DEFAULT_ENV="TEST"
DEFAULT_WAIT="0"
DEFAULT_VALIDATE="1"

CRON_MARKER="# KSeFService cron"

# ---------------- utils ----------------
ts() { date +"%Y%m%d_%H%M%S_%3N" 2>/dev/null || date +"%Y%m%d_%H%M%S"; }
die() { echo "BLAD: $*" >&2; exit 1; }
info() { echo "$*"; }
ensure_dir() { mkdir -p "$1"; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }
chmod600() { chmod 600 "$1" 2>/dev/null || true; }

require_cmds() {
  local miss=0
  local c
  for c in "$@"; do
    have_cmd "$c" || { echo "Brak komendy: $c" >&2; miss=1; }
  done
  [[ "$miss" -eq 0 ]] || exit 1
}

is_01() { [[ "${1:-}" == "0" || "${1:-}" == "1" ]]; }

save_text() {
  local path="$1"
  local content="$2"
  printf '%s' "$content" > "$path"
}

append_text() {
  local path="$1"
  local content="$2"
  printf '%s' "$content" >> "$path"
}

trim() {
  local s="${1:-}"
  s="${s#"${s%%[![:space:]]*}"}"
  s="${s%"${s##*[![:space:]]}"}"
  printf '%s' "$s"
}

unique_lines() {
  awk 'NF && !seen[$0]++'
}

# ---------------- secret (libsecret) ----------------
SECRET_SERVICE="ksefservice"

secret_available() { have_cmd secret-tool; }

secret_set() {
  secret-tool store --label="KSeFService password" \
    service "$SECRET_SERVICE" login "$1" clientId "$2" <<<"$3" >/dev/null
}

secret_get() {
  secret-tool lookup service "$SECRET_SERVICE" login "$1" clientId "$2" 2>/dev/null || true
}

# ---------------- config ----------------
test_configured() {
  [[ "$RECONFIGURE" == "1" ]] && return 1
  [[ -f "$CFG_FILE" ]] || return 1

  # shellcheck disable=SC1090
  source "$CFG_FILE" || return 1

  local required=(
    CLIENT_ID
    LOGIN
    API_ENV
    INPUT_DIR
    OUTPUT_DIR
    PASSWORD_STORAGE
  )

  local r
  for r in "${required[@]}"; do
    [[ -n "${!r:-}" ]] || return 1
  done

  return 0
}

load_config() {
  [[ -f "$CFG_FILE" ]] || return 1
  # shellcheck disable=SC1090
  source "$CFG_FILE"

  : "${CLIENT_ID:?}"
  : "${LOGIN:?}"
  : "${API_ENV:?}"
  : "${INPUT_DIR:?}"
  : "${OUTPUT_DIR:?}"
  : "${PASSWORD_STORAGE:?}"

  WAIT_FINAL="${WAIT_FINAL:-$DEFAULT_WAIT}"
  VALIDATE_XSD="${VALIDATE_XSD:-$DEFAULT_VALIDATE}"

  is_01 "$WAIT_FINAL" || WAIT_FINAL="$DEFAULT_WAIT"
  is_01 "$VALIDATE_XSD" || VALIDATE_XSD="$DEFAULT_VALIDATE"

  return 0
}

save_config() {
  ensure_dir "$CFG_DIR"

  cat >"$CFG_FILE" <<EOF
CLIENT_ID="$CLIENT_ID"
LOGIN="$LOGIN"
API_ENV="$API_ENV"
INPUT_DIR="$INPUT_DIR"
OUTPUT_DIR="$OUTPUT_DIR"
WAIT_FINAL="$WAIT_FINAL"
VALIDATE_XSD="$VALIDATE_XSD"
PASSWORD_STORAGE="$PASSWORD_STORAGE"
TASK_OFFERED="${TASK_OFFERED:-0}"
${PASSWORD_PLAIN:+PASSWORD_PLAIN="$PASSWORD_PLAIN"}
EOF

  chmod600 "$CFG_FILE"
}

get_password() {
  if [[ "$PASSWORD_STORAGE" == "secret" ]]; then
    local p
    p="$(secret_get "$LOGIN" "$CLIENT_ID")"
    [[ -n "$p" ]] || die "Brak hasla w keyring. Uzyj --reconfigure."
    echo "$p"
  else
    [[ -n "${PASSWORD_PLAIN:-}" ]] || die "Brak hasla w pliku. Uzyj --reconfigure."
    echo "$PASSWORD_PLAIN"
  fi
}

ask_path() {
  local prompt="$1"
  local default_dir="$SCRIPT_DIR"
  local p=""

  while true; do
    read -r -p "$prompt [Enter = $default_dir]: " p
    p="${p:-$default_dir}"

    ensure_dir "$p" || true
    [[ -d "$p" ]] || continue

    (
      cd "$p" >/dev/null 2>&1 && pwd
    )
    return 0
  done
}

# ---------------- konfiguracja ----------------
configure_first_run() {
  info "=== Pierwsza konfiguracja $APP_NAME ==="

  read -r -p "Podaj clientId (GUID): " CLIENT_ID
  read -r -p "Podaj login (email): " LOGIN
  read -r -s -p "Podaj haslo: " PASSWORD_PLAIN
  echo

  while true; do
    read -r -p "Wybierz api_env (TEST/PROD/DEMO) [$DEFAULT_ENV]: " API_ENV
    API_ENV="${API_ENV:-$DEFAULT_ENV}"
    API_ENV="${API_ENV^^}"
    [[ "$API_ENV" == "TEST" || "$API_ENV" == "PROD" || "$API_ENV" == "DEMO" ]] && break
  done

  INPUT_DIR="$(ask_path "Podaj katalog wejsciowy (tu leza ZIP/XML do wysylki)")"
  OUTPUT_DIR="$(ask_path "Podaj katalog wyjsciowy (tu zapisze UPO/logi)")"

  while true; do
    read -r -p "Walidowac XSD w batch? (1=tak, 0=nie) [domyslnie $DEFAULT_VALIDATE]: " VALIDATE_XSD
    VALIDATE_XSD="${VALIDATE_XSD:-$DEFAULT_VALIDATE}"
    is_01 "$VALIDATE_XSD" && break
  done

  while true; do
    read -r -p "Batch: czekac na finalny status paczki? wait (1=tak, 0=nie) [domyslnie $DEFAULT_WAIT]: " WAIT_FINAL
    WAIT_FINAL="${WAIT_FINAL:-$DEFAULT_WAIT}"
    is_01 "$WAIT_FINAL" && break
  done

  TASK_OFFERED="0"

  if secret_available; then
    local yn
    read -r -p "Zapisac haslo w keyring (libsecret)? (t/n) [t]: " yn
    yn="${yn:-t}"
    if [[ "${yn,,}" == "t" ]]; then
      PASSWORD_STORAGE="secret"
      secret_set "$LOGIN" "$CLIENT_ID" "$PASSWORD_PLAIN"
      unset PASSWORD_PLAIN
    else
      PASSWORD_STORAGE="file"
    fi
  else
    PASSWORD_STORAGE="file"
  fi

  save_config
  info "Zapisano konfiguracje w: $CFG_FILE"
}

# ---------------- filtrowanie plików wejściowych ----------------
is_input_invoice_file() {
  local path="$1"
  local name
  name="$(basename "$path")"
  name="${name,,}"

  if [[ "$name" == upo_* ]]; then return 1; fi
  if [[ "$name" == map_* ]]; then return 1; fi
  if [[ "$name" == interactive_* ]]; then return 1; fi
  if [[ "$name" == batch_* ]]; then return 1; fi
  if [[ "$name" == upo_lookup_* ]]; then return 1; fi
  if [[ "$name" == *.error.txt ]]; then return 1; fi

  return 0
}

# ---------------- archiwum ----------------
ensure_archive_structure() {
  ensure_dir "$OUTPUT_DIR/archive"
  ensure_dir "$OUTPUT_DIR/archive/sent"
  ensure_dir "$OUTPUT_DIR/archive/error"
}

archive_file() {
  local file_path="$1"
  local success="$2"

  [[ -f "$file_path" ]] || return 0

  local target_dir="$OUTPUT_DIR/archive/error"
  [[ "$success" == "1" ]] && target_dir="$OUTPUT_DIR/archive/sent"

  local filename ext basename_noext dest
  filename="$(basename "$file_path")"
  ext="${filename##*.}"
  basename_noext="${filename%.*}"

  if [[ "$filename" == "$ext" ]]; then
    dest="$target_dir/${filename}_$(ts)"
  else
    dest="$target_dir/${basename_noext}_$(ts).${ext}"
  fi

  mv -f "$file_path" "$dest"
  echo "$dest"
}

# ---------------- json helpers ----------------
json_success() {
  if have_cmd jq; then
    jq -e '
      (.success == true) or
      (.data.code == 200) or
      ((.data.ksefNumber // .data.KsefNumber // "") != "") or
      ((.data.upoXml // .data.upo_xml // .data.raw_upo // "") != "")
    ' >/dev/null 2>&1
  else
    grep -Eq '"success"[[:space:]]*:[[:space:]]*true|"code"[[:space:]]*:[[:space:]]*200|"ksefNumber"[[:space:]]*:|"KsefNumber"[[:space:]]*:|"upoXml"[[:space:]]*:|"upo_xml"[[:space:]]*:|"raw_upo"[[:space:]]*:'
  fi
}

should_archive_success() {
  local json="$1"
  local got_upo="${2:-0}"
  local ksef_count="${3:-0}"

  [[ "$got_upo" == "1" ]] && return 0
  [[ "${ksef_count:-0}" =~ ^[0-9]+$ ]] && (( ksef_count > 0 )) && return 0

  echo "$json" | json_success
}

extract_upo_xml() {
  local json="$1"

  if have_cmd jq; then
    jq -r '
      .data.upoXml //
      .data.upo_xml //
      .data.raw_upo //
      .upoXml //
      .upo_xml //
      .raw_upo //
      empty
    ' <<<"$json" 2>/dev/null || true
  else
    echo ""
  fi
}

extract_ksef_numbers() {
  local json="$1"

  if have_cmd jq; then
    jq -r '
      [
        .. | .ksefNumber?,
        .. | .KsefNumber?,
        .. | .NumerKSeFDokumentu?,
        .. | .numerKSeF?,
        .. | .numberKsef?
      ]
      | flatten
      | map(select(type == "string" and length > 0))
      | unique
      | .[]
    ' <<<"$json" 2>/dev/null || true
  else
    printf '%s' "$json" \
      | grep -Eo '"(ksefNumber|KsefNumber|NumerKSeFDokumentu|numerKSeF|numberKsef)"[[:space:]]*:[[:space:]]*"[^"]+"' \
      | sed -E 's/^.*:[[:space:]]*"([^"]+)".*$/\1/' \
      | unique_lines || true
  fi
}

extract_reference_numbers() {
  local json="$1"

  if have_cmd jq; then
    jq -r '
      [
        .. | .referenceNumber?,
        .. | .ReferenceNumber?,
        .. | .reference_number?,
        .. | .refNumber?,
        .. | .RefNumber?,
        .. | .invoiceReference?,
        .. | .InvoiceReference?
      ]
      | flatten
      | map(select(type == "string" and length > 0))
      | unique
      | .[]
    ' <<<"$json" 2>/dev/null || true
  else
    printf '%s' "$json" \
      | grep -Eo '"(referenceNumber|ReferenceNumber|reference_number|refNumber|RefNumber|invoiceReference|InvoiceReference)"[[:space:]]*:[[:space:]]*"[^"]+"' \
      | sed -E 's/^.*:[[:space:]]*"([^"]+)".*$/\1/' \
      | unique_lines || true
  fi
}

extract_invoice_pairs_csv() {
  local json="$1"

  if ! have_cmd jq; then
    return 0
  fi

  jq -r '
    [
      .. | objects |
      {
        InvoiceNumber: (
          .invoiceNumber //
          .InvoiceNumber //
          .numerFaktury //
          .NumerFaktury //
          .number //
          .fullNumber //
          .documentNumber //
          .faNumber //
          .invoice_no //
          ""
        ),
        KsefNumber: (
          .ksefNumber //
          .KsefNumber //
          .NumerKSeFDokumentu //
          .numerKSeF //
          .numberKsef //
          ""
        )
      }
      | select((.InvoiceNumber != "") or (.KsefNumber != ""))
    ]
    | unique
    | .[]
    | "\(.InvoiceNumber|gsub(";"; ","));\(.KsefNumber|gsub(";"; ","))"
  ' <<<"$json" 2>/dev/null || true
}

extract_batch_issues() {
  local json="$1"

  if ! have_cmd jq; then
    return 0
  fi

  jq -r '
    [
      .. | objects |
      {
        InvoiceNumber: (
          .invoiceNumber //
          .InvoiceNumber //
          .numerFaktury //
          .NumerFaktury //
          .number //
          .fullNumber //
          .documentNumber //
          .faNumber //
          ""
        ),
        KsefNumber: (
          .ksefNumber //
          .KsefNumber //
          .NumerKSeFDokumentu //
          .numerKSeF //
          .numberKsef //
          ""
        ),
        Status: (
          .status //
          .state //
          .result //
          .code //
          ""
        ),
        Message: (
          .message //
          .error //
          .errorMessage //
          .description //
          .details //
          .reason //
          ""
        )
      }
      | ._full = (((.Status|tostring) + " " + (.Message|tostring)) | ascii_downcase)
      | select(
          (._full | test("duplicate|already exists|duplikat|rejected|failed|error|conflict|juz istnieje|już istnieje"))
        )
    ]
    | unique
    | .[]
    | "\(.InvoiceNumber|gsub(";"; ","));\(.KsefNumber|gsub(";"; ","));\(.Status|tostring|gsub(";"; ","));\(.Message|gsub(";"; ","))"
  ' <<<"$json" 2>/dev/null || true
}

save_invoice_pairs() {
  local base="$1"
  local pairs_content="$2"

  [[ -n "${pairs_content:-}" ]] || return 0

  local cleaned
  cleaned="$(printf '%s\n' "$pairs_content" | sed '/^[[:space:]]*$/d' | unique_lines)"
  [[ -n "$cleaned" ]] || return 0

  local map_path="$OUTPUT_DIR/map_${base}_$(ts).csv"
  {
    echo "InvoiceNumber;KsefNumber"
    printf '%s\n' "$cleaned"
  } > "$map_path"

  info "   Zapisano mapowanie: $map_path"
}

repair_invoice_pairs() {
  local pairs="$1"
  local known_invoices="$2"
  local known_ksef="$3"
  local default_invoice="$4"

  local result=""
  local invoice_count=0
  local ksef_count=0

  [[ -n "${known_invoices:-}" ]] && invoice_count="$(printf '%s\n' "$known_invoices" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')"
  [[ -n "${known_ksef:-}" ]] && ksef_count="$(printf '%s\n' "$known_ksef" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')"

  if [[ -n "${pairs:-}" ]]; then
    while IFS=';' read -r inv kse; do
      inv="$(trim "${inv:-}")"
      kse="$(trim "${kse:-}")"

      [[ -n "$inv" || -n "$kse" ]] || continue

      if [[ -z "$inv" ]]; then
        if [[ "$invoice_count" == "1" ]]; then
          inv="$(printf '%s\n' "$known_invoices" | sed '/^[[:space:]]*$/d' | head -n1)"
        elif [[ -n "$default_invoice" ]]; then
          inv="$default_invoice"
        fi
      fi

      result+="${inv};${kse}"$'\n'
    done <<< "$pairs"
  fi

  if [[ -z "${result:-}" && -n "${known_ksef:-}" ]]; then
    while IFS= read -r k; do
      [[ -n "$k" ]] || continue

      local inv=""
      if [[ "$invoice_count" == "1" ]]; then
        inv="$(printf '%s\n' "$known_invoices" | sed '/^[[:space:]]*$/d' | head -n1)"
      elif [[ -n "$default_invoice" ]]; then
        inv="$default_invoice"
      fi

      result+="${inv};${k}"$'\n'
    done <<< "$known_ksef"
  fi

  printf '%s' "$result" | sed '/^[[:space:]]*$/d' | unique_lines
}

# ---------------- XML / ZIP helpers ----------------
get_invoice_number_from_xml_file() {
  local xml="$1"
  [[ -f "$xml" ]] || return 0

  if have_cmd xmllint; then
    local x
    x="$(xmllint --xpath 'string(//*[local-name()="P_2"][1])' "$xml" 2>/dev/null || true)"
    x="$(trim "$x")"
    [[ -n "$x" ]] && { echo "$x"; return 0; }

    x="$(xmllint --xpath 'string(//*[local-name()="NumerFaktury"][1])' "$xml" 2>/dev/null || true)"
    x="$(trim "$x")"
    [[ -n "$x" ]] && { echo "$x"; return 0; }

    x="$(xmllint --xpath 'string(//*[local-name()="InvoiceNumber"][1])' "$xml" 2>/dev/null || true)"
    x="$(trim "$x")"
    [[ -n "$x" ]] && { echo "$x"; return 0; }

    x="$(xmllint --xpath 'string(//*[local-name()="DocumentNumber"][1])' "$xml" 2>/dev/null || true)"
    x="$(trim "$x")"
    [[ -n "$x" ]] && { echo "$x"; return 0; }
  fi

  tr '\n' ' ' < "$xml" \
    | sed -nE 's/.*<(P_2|NumerFaktury|InvoiceNumber|DocumentNumber|FaNumer)[^>]*>([^<]+)<\/(P_2|NumerFaktury|InvoiceNumber|DocumentNumber|FaNumer)>.*/\2/p' \
    | head -n1 \
    | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}

get_invoice_numbers_from_zip() {
  local zip_path="$1"
  [[ -f "$zip_path" ]] || return 0
  have_cmd unzip || return 0

  local tmp_dir
  tmp_dir="$(mktemp -d)"

  (
    cd "$tmp_dir" >/dev/null 2>&1
    unzip -qq "$zip_path" >/dev/null 2>&1 || true
  )

  local found=""
  while IFS= read -r -d '' f; do
    local inv
    inv="$(get_invoice_number_from_xml_file "$f" || true)"
    [[ -n "${inv:-}" ]] && found+="${inv}"$'\n'
  done < <(find "$tmp_dir" -type f -iname '*.xml' -print0 2>/dev/null || true)

  rm -rf "$tmp_dir"
  printf '%s' "$found" | sed '/^[[:space:]]*$/d' | unique_lines
}

# ---------------- http ----------------
LAST_HTTP_CODE="000"

post_multipart() {
  local url="$1" file_field="$2" file_path="$3" content_type="$4"; shift 4

  local tmp_body
  tmp_body="$(mktemp)"
  local code="000"

  code="$(
    curl -sS -L \
      --connect-timeout 10 \
      --max-time 180 \
      --retry 2 \
      --retry-delay 1 \
      --retry-connrefused \
      -H "Accept: application/json" \
      "$@" \
      -F "$file_field=@$file_path;type=$content_type" \
      -o "$tmp_body" \
      -w "%{http_code}" \
      "$url" || echo "000"
  )"

  LAST_HTTP_CODE="$code"
  cat "$tmp_body"
  rm -f "$tmp_body"

  [[ "$code" =~ ^2[0-9][0-9]$ ]] || return 22
  return 0
}

post_form() {
  local url="$1"; shift

  local tmp_body
  tmp_body="$(mktemp)"
  local code="000"

  code="$(
    curl -sS -L \
      --connect-timeout 10 \
      --max-time 180 \
      --retry 2 \
      --retry-delay 1 \
      --retry-connrefused \
      -H "Accept: application/json" \
      "$@" \
      -o "$tmp_body" \
      -w "%{http_code}" \
      "$url" || echo "000"
  )"

  LAST_HTTP_CODE="$code"
  cat "$tmp_body"
  rm -f "$tmp_body"

  [[ "$code" =~ ^2[0-9][0-9]$ ]] || return 22
  return 0
}

# ---------------- zip ----------------
zip_from_xml() {
  local xml="$1"
  local tmp_dir zip_path
  tmp_dir="$(mktemp -d)"

  cp "$xml" "$tmp_dir/"

  zip_path="/tmp/$(basename "${xml%.*}")_$(date +%s)_$$.zip"
  (
    cd "$tmp_dir" >/dev/null 2>&1
    zip -q "$zip_path" "$(basename "$xml")"
  )

  rm -rf "$tmp_dir"
  echo "$zip_path"
}

# ---------------- UPO ----------------
get_upo_by_reference_number() {
  local reference_number="$1"
  local base="$2"
  local pwd="$3"

  local raw=""
  if raw="$(post_form "$ENDPOINT_UPO" \
    --data-urlencode "clientId=$CLIENT_ID" \
    --data-urlencode "login=$LOGIN" \
    --data-urlencode "password=$pwd" \
    --data-urlencode "api_env=$API_ENV" \
    --data-urlencode "referenceNumber=$reference_number")"; then

    local log_path="$OUTPUT_DIR/upo_lookup_${base}_$(ts).json"
    save_text "$log_path" "$raw"

    local upo=""
    upo="$(extract_upo_xml "$raw")"

    if [[ -n "$upo" && "$upo" != "null" ]]; then
      local out="$OUTPUT_DIR/UPO_${base}_$(ts).xml"
      printf '%s' "$upo" > "$out"
      info "   Zapisano UPO: $out"
      return 0
    else
      info "   Endpoint UPO nie zwrocil XML. Log: $log_path"
      return 1
    fi
  else
    info "   Nie udalo sie pobrac UPO dla referenceNumber $reference_number (HTTP $LAST_HTTP_CODE)"
    return 1
  fi
}

save_upo_interactive_if_present() {
  local json="$1"
  local base="$2"

  local upo=""
  upo="$(extract_upo_xml "$json")"
  [[ -n "$upo" && "$upo" != "null" ]] || return 1

  local out="$OUTPUT_DIR/UPO_${base}_$(ts).xml"
  printf '%s' "$upo" > "$out"
  info "   Zapisano UPO: $out"
  return 0
}

save_upo_batch_if_present() {
  local json="$1"
  local base="$2"

  local top_upo=""
  top_upo="$(extract_upo_xml "$json")"
  if [[ -n "$top_upo" && "$top_upo" != "null" ]]; then
    local out="$OUTPUT_DIR/UPO_batch_${base}_$(ts).xml"
    printf '%s' "$top_upo" > "$out"
    info "   Zapisano UPO z odpowiedzi batch: $out"
    return 0
  fi

  have_cmd jq || return 1

  local count
  count="$(jq 'if (.data.invoices // empty) then (.data.invoices | length) else 0 end' <<<"$json" 2>/dev/null || echo 0)"
  [[ "$count" =~ ^[0-9]+$ ]] || count=0
  [[ "$count" -gt 0 ]] || return 1

  local saved=1
  local i=0
  while [[ "$i" -lt "$count" ]]; do
    local upo
    upo="$(jq -r "
      .data.invoices[$i].upoXml //
      .data.invoices[$i].upo_xml //
      .data.invoices[$i].raw_upo //
      empty
    " <<<"$json" 2>/dev/null || true)"

    if [[ -n "$upo" && "$upo" != "null" ]]; then
      local out="$OUTPUT_DIR/UPO_batch_${base}_$((i+1))_$(ts).xml"
      printf '%s' "$upo" > "$out"
      info "   Zapisano UPO (invoice#$((i+1))): $out"
      saved=0
    fi

    i=$((i+1))
  done

  return "$saved"
}

# ---------------- cron ----------------
install_cron_task() {
  local mode="$1"
  local env_override="$2"

  have_cmd crontab || die "Brak crontab."

  local cron_expr=""
  local freq=""

  info "=== Instalacja zadania cron dla trybu: $mode ==="

  while true; do
    read -r -p "Czestotliwosc? (MINUTE/HOURLY/DAILY): " freq
    freq="${freq^^}"
    [[ "$freq" == "MINUTE" || "$freq" == "HOURLY" || "$freq" == "DAILY" ]] && break
  done

  case "$freq" in
    MINUTE)
      local mo="15"
      local mo_in=""
      read -r -p "Co ile minut? [15]: " mo_in
      mo="${mo_in:-15}"
      [[ "$mo" =~ ^[0-9]+$ ]] || die "Niepoprawna liczba minut."
      (( mo >= 1 && mo <= 59 )) || die "Minuty musza byc z zakresu 1..59."
      cron_expr="*/$mo * * * *"
      ;;
    HOURLY)
      local ho="1"
      local ho_in=""
      read -r -p "Co ile godzin? [1]: " ho_in
      ho="${ho_in:-1}"
      [[ "$ho" =~ ^[0-9]+$ ]] || die "Niepoprawna liczba godzin."
      (( ho >= 1 && ho <= 23 )) || die "Godziny musza byc z zakresu 1..23."
      cron_expr="0 */$ho * * *"
      ;;
    DAILY)
      local start_time="21:00"
      local st=""
      read -r -p "Godzina startu (HH:mm) [21:00]: " st
      start_time="${st:-21:00}"
      [[ "$start_time" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] || die "Niepoprawny format godziny."
      local hh="${start_time%:*}"
      local mm="${start_time#*:}"
      cron_expr="$mm $hh * * *"
      ;;
  esac

  local cmd
  cmd="cd \"$SCRIPT_DIR\" && /usr/bin/env bash \"$SCRIPT_PATH\" --mode $mode"
  [[ -n "$env_override" ]] && cmd="$cmd --env $env_override"

  local entry="$cron_expr $cmd $CRON_MARKER [$mode]"

  local current
  current="$(crontab -l 2>/dev/null || true)"
  current="$(echo "$current" | grep -Fv "$CRON_MARKER [$mode]" || true)"

  {
    [[ -n "$current" ]] && echo "$current"
    echo "$entry"
  } | crontab -

  info "OK: dodano zadanie cron dla trybu $mode"
  info "Wpis: $entry"
}

uninstall_cron_task() {
  local mode="$1"

  have_cmd crontab || die "Brak crontab."

  local current
  current="$(crontab -l 2>/dev/null || true)"
  [[ -n "$current" ]] || {
    info "Nie ma zadnych wpisow crontab."
    return 0
  }

  local filtered
  filtered="$(echo "$current" | grep -Fv "$CRON_MARKER [$mode]" || true)"

  if [[ -n "$filtered" ]]; then
    printf '%s\n' "$filtered" | crontab -
  else
    crontab -r 2>/dev/null || true
  fi

  info "OK: usunieto zadanie cron dla trybu $mode"
}

offer_install_task_prompt() {
  local ans=""
  read -r -p "Czy chcesz dodac zadanie do cron? (t/n): " ans
  [[ "${ans,,}" == "t" ]] || return 1

  local m=""
  while true; do
    read -r -p "Dla jakiego trybu utworzyc zadanie? (interactive/batch): " m
    m="${m,,}"
    [[ "$m" == "interactive" || "$m" == "batch" ]] && break
  done

  install_cron_task "$m" ""
  TASK_OFFERED="1"
  save_config
  return 0
}

# ---------------- interactive ----------------
run_interactive() {
  ensure_dir "$OUTPUT_DIR"
  ensure_archive_structure

  local pwd
  pwd="$(get_password)"

  shopt -s nullglob
  local xmls=("$INPUT_DIR"/*.xml)
  shopt -u nullglob

  local filtered=()
  local f
  for f in "${xmls[@]}"; do
    is_input_invoice_file "$f" && filtered+=("$f")
  done
  xmls=("${filtered[@]}")

  [[ "${#xmls[@]}" -gt 0 ]] || {
    info "Brak plikow XML w: $INPUT_DIR"
    return 0
  }

  for f in "${xmls[@]}"; do
    local name base log_path raw moved err_log got_upo map_content known_invoice ksef_numbers reference_numbers
    name="$(basename "$f")"
    base="${name%.*}"
    known_invoice="$(get_invoice_number_from_xml_file "$f" || true)"

    info ">> INTERACTIVE: wysylam $name"

    log_path="$OUTPUT_DIR/interactive_${base}_$(ts).json"

    raw=""
    if raw="$(post_multipart "$ENDPOINT_INTERACTIVE" file "$f" application/xml \
      -F "clientId=$CLIENT_ID" \
      -F "login=$LOGIN" \
      -F "password=$pwd" \
      -F "api_env=$API_ENV")"; then

      save_text "$log_path" "$raw"
      got_upo=0

      if save_upo_interactive_if_present "$raw" "$base"; then
        got_upo=1
      else
        info "   Brak UPO bezposrednio w odpowiedzi (sprawdz log): $log_path"
      fi

      ksef_numbers="$(extract_ksef_numbers "$raw" || true)"
      local ksef_count=0
      if [[ -n "${ksef_numbers:-}" ]]; then
        while IFS= read -r ksef_no; do
          [[ -n "$ksef_no" ]] || continue
          info "   Wykryto numer KSeF: $ksef_no"
          ksef_count=$((ksef_count + 1))
        done <<< "$ksef_numbers"
      fi

      reference_numbers="$(extract_reference_numbers "$raw" || true)"
      if [[ -n "${reference_numbers:-}" ]]; then
        while IFS= read -r ref_no; do
          [[ -n "$ref_no" ]] || continue
          info "   Wykryto referenceNumber: $ref_no"
        done <<< "$reference_numbers"
      fi

      if [[ "$got_upo" -eq 0 && -n "${reference_numbers:-}" ]]; then
        while IFS= read -r ref_no; do
          [[ -n "$ref_no" ]] || continue
          if get_upo_by_reference_number "$ref_no" "$base" "$pwd"; then
            got_upo=1
            break
          fi
        done <<< "$reference_numbers"
      elif [[ "$got_upo" -eq 0 && -n "${ksef_numbers:-}" ]]; then
        info "   Wykryto numer KSeF, ale endpoint UPO oczekuje referenceNumber."
      fi

      map_content="$(extract_invoice_pairs_csv "$raw" || true)"
      map_content="$(repair_invoice_pairs "$map_content" "${known_invoice:-}" "$ksef_numbers" "$base")"

      if [[ -n "${map_content:-}" ]]; then
        save_invoice_pairs "$base" "$map_content"
      fi

      if should_archive_success "$raw" "$got_upo" "$ksef_count"; then
        moved="$(archive_file "$f" 1 || true)"
      else
        moved="$(archive_file "$f" 0 || true)"
      fi

      [[ -n "${moved:-}" ]] && info "   Zarchiwizowano plik wejsciowy do: $moved"
    else
      local err_msg="HTTP $LAST_HTTP_CODE"
      err_log="$OUTPUT_DIR/interactive_${base}_$(ts).error.txt"
      save_text "$err_log" "$err_msg"
      info "   BLAD: $err_msg"
      info "   Zapisano blad: $err_log"

      moved="$(archive_file "$f" 0 || true)"
      [[ -n "${moved:-}" ]] && info "   Zarchiwizowano plik wejsciowy do: $moved"
    fi
  done
}

# ---------------- batch ----------------
run_batch() {
  ensure_dir "$OUTPUT_DIR"
  ensure_archive_structure

  local pwd
  pwd="$(get_password)"

  shopt -s nullglob
  local zips=("$INPUT_DIR"/*.zip)
  local xmls=("$INPUT_DIR"/*.xml)
  shopt -u nullglob

  local filtered_zips=()
  local filtered_xmls=()
  local z
  local x

  for z in "${zips[@]}"; do
    is_input_invoice_file "$z" && filtered_zips+=("$z")
  done
  for x in "${xmls[@]}"; do
    is_input_invoice_file "$x" && filtered_xmls+=("$x")
  done

  zips=("${filtered_zips[@]}")
  xmls=("${filtered_xmls[@]}")

  if [[ "${#zips[@]}" -eq 0 && "${#xmls[@]}" -eq 0 ]]; then
    info "Brak plikow ZIP ani XML w: $INPUT_DIR"
    return 0
  fi

  local jobs=()

  for z in "${zips[@]}"; do
    jobs+=("zip|$z|0|$z")
  done

  for x in "${xmls[@]}"; do
    local tmp_zip
    tmp_zip="$(zip_from_xml "$x")"
    jobs+=("xml|$tmp_zip|1|$x")
  done

  local job
  for job in "${jobs[@]}"; do
    IFS='|' read -r kind path temp source <<<"$job"

    local display name_for_log log_path raw err_log moved ksef_numbers map_content batch_issues reference_numbers
    local got_upo=0
    local known_invoices=""
    local ksef_count=0

    if [[ "$kind" == "zip" ]]; then
      display="$(basename "$path")"
      name_for_log="$(basename "${path%.*}")"
      known_invoices="$(get_invoice_numbers_from_zip "$path" || true)"
    else
      display="$(basename "$source") (spakowane)"
      name_for_log="$(basename "${source%.*}")"
      local one_inv=""
      one_inv="$(get_invoice_number_from_xml_file "$source" || true)"
      [[ -n "${one_inv:-}" ]] && known_invoices="$one_inv"
    fi

    info ">> BATCH: wysylam $display"

    log_path="$OUTPUT_DIR/batch_${name_for_log}_$(ts).json"

    raw=""
    if raw="$(post_multipart "$ENDPOINT_BATCH" zip "$path" application/zip \
      -F "clientId=$CLIENT_ID" \
      -F "login=$LOGIN" \
      -F "password=$pwd" \
      -F "api_env=$API_ENV" \
      -F "wait=$WAIT_FINAL" \
      -F "validate_xsd=$VALIDATE_XSD")"; then

      save_text "$log_path" "$raw"
      info "   Zapisano log: $log_path"

      if save_upo_batch_if_present "$raw" "$name_for_log"; then
        got_upo=1
      fi

      ksef_numbers="$(extract_ksef_numbers "$raw" || true)"
      if [[ -n "${ksef_numbers:-}" ]]; then
        while IFS= read -r ksef_no; do
          [[ -n "$ksef_no" ]] || continue
          info "   Wykryto numer KSeF: $ksef_no"
          ksef_count=$((ksef_count + 1))
        done <<< "$ksef_numbers"
      fi

      reference_numbers="$(extract_reference_numbers "$raw" || true)"
      if [[ -n "${reference_numbers:-}" ]]; then
        while IFS= read -r ref_no; do
          [[ -n "$ref_no" ]] || continue
          info "   Wykryto referenceNumber: $ref_no"
        done <<< "$reference_numbers"
      fi

      batch_issues="$(extract_batch_issues "$raw" || true)"
      if [[ -n "${batch_issues:-}" ]]; then
        while IFS=';' read -r inv kse st msg; do
          [[ -n "${inv}${kse}${st}${msg}" ]] || continue
          [[ -z "${inv:-}" ]] && inv="(brak numeru faktury)"
          if [[ -n "${msg:-}" ]]; then
            info "   ODRZUCENIE / UWAGA: $inv -> $msg"
          else
            info "   ODRZUCENIE / UWAGA: $inv -> ${st:-nieznany status}"
          fi
        done <<< "$batch_issues"
      fi

      map_content="$(extract_invoice_pairs_csv "$raw" || true)"
      map_content="$(repair_invoice_pairs "$map_content" "$known_invoices" "$ksef_numbers" "$name_for_log")"

      if [[ -n "${map_content:-}" ]]; then
        save_invoice_pairs "$name_for_log" "$map_content"
      fi

      if [[ "$got_upo" -eq 0 && -n "${reference_numbers:-}" ]]; then
        while IFS= read -r ref_no; do
          [[ -n "$ref_no" ]] || continue
          if get_upo_by_reference_number "$ref_no" "$name_for_log" "$pwd"; then
            got_upo=1
            break
          fi
        done <<< "$reference_numbers"
      fi

      if should_archive_success "$raw" "$got_upo" "$ksef_count"; then
        if [[ "$kind" == "zip" ]]; then
          moved="$(archive_file "$path" 1 || true)"
          [[ -n "${moved:-}" ]] && info "   Zarchiwizowano ZIP do: $moved"
        else
          moved="$(archive_file "$source" 1 || true)"
          [[ -n "${moved:-}" ]] && info "   Zarchiwizowano XML do: $moved"
        fi
      else
        if [[ "$kind" == "zip" ]]; then
          moved="$(archive_file "$path" 0 || true)"
          [[ -n "${moved:-}" ]] && info "   Zarchiwizowano ZIP do: $moved"
        else
          moved="$(archive_file "$source" 0 || true)"
          [[ -n "${moved:-}" ]] && info "   Zarchiwizowano XML do: $moved"
        fi
      fi
    else
      local err_msg="HTTP $LAST_HTTP_CODE"
      err_log="$OUTPUT_DIR/batch_${name_for_log}_$(ts).error.txt"
      save_text "$err_log" "$err_msg"
      info "   BLAD: $err_msg"
      info "   Zapisano blad: $err_log"

      if [[ "$kind" == "zip" ]]; then
        moved="$(archive_file "$path" 0 || true)"
        [[ -n "${moved:-}" ]] && info "   Zarchiwizowano ZIP do: $moved"
      else
        moved="$(archive_file "$source" 0 || true)"
        [[ -n "${moved:-}" ]] && info "   Zarchiwizowano XML do: $moved"
      fi
    fi

    if [[ "$temp" == "1" ]]; then
      rm -f "$path" 2>/dev/null || true
    fi
  done
}

# ---------------- args ----------------
MODE="interactive"
ENV_OVERRIDE=""
RECONFIGURE=0
INSTALL_TASK=0
UNINSTALL_TASK=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --mode)
      [[ $# -ge 2 ]] || die "Brak wartosci dla --mode"
      MODE="$2"
      shift 2
      ;;
    --env)
      [[ $# -ge 2 ]] || die "Brak wartosci dla --env"
      ENV_OVERRIDE="$2"
      shift 2
      ;;
    --reconfigure)
      RECONFIGURE=1
      shift
      ;;
    --install-task)
      INSTALL_TASK=1
      shift
      ;;
    --uninstall-task)
      UNINSTALL_TASK=1
      shift
      ;;
    *)
      die "Nieznana opcja: $1"
      ;;
  esac
done

MODE="${MODE,,}"
[[ "$MODE" == "interactive" || "$MODE" == "batch" ]] || die "mode: interactive|batch"

require_cmds curl zip
have_cmd unzip || info "UWAGA: brak unzip - odczyt numerow faktur z ZIP bedzie ograniczony."
have_cmd jq || info "UWAGA: brak jq - analiza JSON bedzie ograniczona."
have_cmd xmllint || info "UWAGA: brak xmllint - odczyt numeru faktury z XML bedzie uproszczony."

if ! test_configured; then
  configure_first_run
fi

load_config || die "Nie udalo sie wczytac konfiguracji."

[[ -n "$ENV_OVERRIDE" ]] && API_ENV="${ENV_OVERRIDE^^}"

ensure_dir "$INPUT_DIR"
ensure_dir "$OUTPUT_DIR"

IN_NORM="$(cd "$INPUT_DIR" >/dev/null 2>&1 && pwd)"
OUT_NORM="$(cd "$OUTPUT_DIR" >/dev/null 2>&1 && pwd)"

if [[ "$IN_NORM" == "$OUT_NORM" ]]; then
  info "UWAGA: katalog wejściowy i wyjściowy są takie same."
  info "Skrypt będzie filtrował UPO, mapowania i logi, ale zalecane są osobne katalogi."
fi

if [[ "$UNINSTALL_TASK" == "1" ]]; then
  uninstall_cron_task "$MODE"
  exit 0
fi

if [[ "$INSTALL_TASK" == "1" ]]; then
  install_cron_task "$MODE" "$ENV_OVERRIDE"
  exit 0
fi

if [[ "${TASK_OFFERED:-0}" != "1" ]]; then
  offer_install_task_prompt || true
fi

info "Tryb: $MODE, api_env: $API_ENV"
info "Wejscie: $INPUT_DIR"
info "Wyjscie:  $OUTPUT_DIR"

case "$MODE" in
  interactive) run_interactive ;;
  batch) run_batch ;;
esac