#!/bin/sh
set -eu

allow_loose=0

die() {
  printf 'apply_patch: %s\n' "$*" >&2
  exit 1
}

mk_temp() {
  mktemp "${TMPDIR:-/tmp}/unidesk-apply-patch.XXXXXX" || exit 1
}

ensure_parent() {
  case "$1" in
    */*)
      dir=${1%/*}
      [ -n "$dir" ] || dir=/
      mkdir -p "$dir"
      ;;
  esac
}

read_text_preserve_newlines() {
  marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
  text=$(cat "$1"; printf '%s' "$marker") || die "failed to read $1"
  printf '%s' "${text%"$marker"}"
}

write_file() {
  target=$1
  source=$2
  ensure_parent "$target"
  cp "$source" "$target" || die "failed to write $target"
}

line_number_for_prefix() {
  newline_count=$(printf '%s' "$1" | tr -cd '\n' | wc -c | tr -d ' ')
  printf '%s\n' $((newline_count + 1))
}

replace_once_with_perl() {
  command -v perl >/dev/null 2>&1 || return 127
  perl -0777 -e '
use strict;
use warnings;

sub fail {
  print STDERR "apply_patch: ", @_, "\n";
  exit 1;
}

sub read_all {
  my ($path, $label) = @_;
  open my $fh, "<", $path or fail("failed to read $label");
  binmode $fh;
  local $/;
  my $data = <$fh>;
  close $fh;
  return defined $data ? $data : "";
}

my ($target, $search_file, $replace_file, $hunk_id, $allow_loose, $out) = @ARGV;
my $old = read_all($target, $target);
my $search = read_all($search_file, "hunk search");
my $replacement = read_all($replace_file, "hunk replacement");
my $new;

if ($search eq "") {
  fail("hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review") if $allow_loose ne "1";
  print STDERR "apply_patch: hunk $hunk_id matched $target:1 (loose)\n";
  $new = $replacement . $old;
} else {
  my $pos = -1;
  my $offset = 0;
  my $count = 0;
  while (1) {
    my $found = index($old, $search, $offset);
    last if $found < 0;
    $pos = $found if $count == 0;
    $count += 1;
    last if $count > 1 && $allow_loose ne "1";
    $offset = $found + length($search);
  }
  fail("hunk $hunk_id context not found in $target") if $count == 0;
  fail("hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review") if $count > 1 && $allow_loose ne "1";
  my $prefix = substr($old, 0, $pos);
  my $suffix = substr($old, $pos + length($search));
  my $line = ($prefix =~ tr/\n//) + 1;
  print STDERR "apply_patch: hunk $hunk_id matched $target:$line\n";
  $new = $prefix . $replacement . $suffix;
}

open my $ofh, ">", $out or fail("failed to render patched file");
binmode $ofh;
print $ofh $new or fail("failed to render patched file");
close $ofh or fail("failed to render patched file");
' "$@"
}

replace_once() {
  target=$1
  search_file=$2
  replace_file=$3
  hunk_id=$4
  [ -e "$target" ] || die "file not found: $target"

  fast_out=$(mk_temp)
  if replace_once_with_perl "$target" "$search_file" "$replace_file" "$hunk_id" "$allow_loose" "$fast_out"; then
    write_file "$target" "$fast_out"
    rm -f "$fast_out"
    return 0
  else
    status=$?
  fi
  rm -f "$fast_out"
  [ "$status" = 127 ] || exit "$status"

  marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
  old=$(cat "$target"; printf '%s' "$marker") || die "failed to read $target"
  old=${old%"$marker"}
  search=$(cat "$search_file"; printf '%s' "$marker") || die "failed to read hunk search"
  search=${search%"$marker"}
  replacement=$(cat "$replace_file"; printf '%s' "$marker") || die "failed to read hunk replacement"
  replacement=${replacement%"$marker"}

  if [ -z "$search" ]; then
    [ "$allow_loose" = 1 ] || die "hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
    printf 'apply_patch: hunk %s matched %s:1 (loose)\n' "$hunk_id" "$target" >&2
    new=$replacement$old
  else
    case "$old" in
      *"$search"*)
        prefix=${old%%"$search"*}
        suffix=${old#*"$search"}
        case "$suffix" in
          *"$search"*)
            [ "$allow_loose" = 1 ] || die "hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review"
            ;;
        esac
        printf 'apply_patch: hunk %s matched %s:%s\n' "$hunk_id" "$target" "$(line_number_for_prefix "$prefix")" >&2
        new=$prefix$replacement$suffix
        ;;
      *)
        die "hunk $hunk_id context not found in $target"
        ;;
    esac
  fi

  out=$(mk_temp)
  printf '%s' "$new" > "$out" || die "failed to render patched file"
  write_file "$target" "$out"
  rm -f "$out"
}

apply_update() {
  target=$1
  body=$2
  in_hunk=0
  search_file=
  replace_file=
  changed=0
  hunk_number=0
  has_add=0
  has_delete=0
  seen_add=0
  anchor_before_add=0
  anchor_after_add=0
  explicit_eof=0

  finish_hunk() {
    [ "$in_hunk" = 1 ] || return 0
    if [ "$allow_loose" != 1 ]; then
      [ -s "$search_file" ] || die "hunk $hunk_number in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
      if [ "$has_add" = 1 ] && [ "$has_delete" = 0 ]; then
        if [ "$anchor_before_add" != 1 ] || { [ "$anchor_after_add" != 1 ] && [ "$explicit_eof" != 1 ]; }; then
          die "hunk $hunk_number in $target is insert-only without both leading and trailing context; include nearby unchanged lines after the insertion or pass --allow-loose after manual review"
        fi
      fi
    fi
    replace_once "$target" "$search_file" "$replace_file" "$hunk_number"
    rm -f "$search_file" "$replace_file"
    search_file=
    replace_file=
    in_hunk=0
    changed=1
  }

  while IFS= read -r line || [ -n "$line" ]; do
    case "$line" in
      "*** End of File"*)
        continue
        ;;
      "@@"*)
        finish_hunk
        search_file=$(mk_temp)
        replace_file=$(mk_temp)
        hunk_number=$((hunk_number + 1))
        has_add=0
        has_delete=0
        seen_add=0
        anchor_before_add=0
        anchor_after_add=0
        explicit_eof=0
        in_hunk=1
        continue
        ;;
    esac
    [ "$in_hunk" = 1 ] || die "expected hunk header in $target"
    case "$line" in
      " "*)
        text=${line#?}
        printf '%s\n' "$text" >> "$search_file"
        printf '%s\n' "$text" >> "$replace_file"
        if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
        ;;
      "-"*)
        text=${line#?}
        printf '%s\n' "$text" >> "$search_file"
        has_delete=1
        if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
        ;;
      "+"*)
        text=${line#?}
        printf '%s\n' "$text" >> "$replace_file"
        has_add=1
        seen_add=1
        ;;
      "\\ No newline at end of file")
        ;;
      "*** End of File"*)
        explicit_eof=1
        ;;
      *)
        die "bad hunk line in $target: $line"
        ;;
    esac
  done < "$body"

  finish_hunk
  [ "$changed" = 1 ] || [ -e "$target" ] || die "file not found: $target"
}

is_top_header() {
  case "$1" in
    "*** Add File: "*|"*** Update File: "*|"*** Delete File: "*|"*** End Patch")
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

pushed=0
pushed_line=
next_patch_line() {
  if [ "$pushed" = 1 ]; then
    line=$pushed_line
    pushed=0
    pushed_line=
    return 0
  fi
  if IFS= read -r line; then
    return 0
  fi
  [ -n "${line:-}" ]
}

push_patch_line() {
  pushed_line=$1
  pushed=1
}

parse_add_file() {
  target=$1
  [ ! -e "$target" ] || die "file already exists: $target"
  tmp=$(mk_temp)
  while next_patch_line; do
    if is_top_header "$line"; then
      push_patch_line "$line"
      break
    fi
    case "$line" in
      "+"*)
        printf '%s\n' "${line#?}" >> "$tmp"
        ;;
      *)
        rm -f "$tmp"
        die "add file lines must start with + for $target"
        ;;
    esac
  done
  write_file "$target" "$tmp"
  rm -f "$tmp"
}

parse_update_file() {
  target=$1
  move_to=
  body=$(mk_temp)
  if next_patch_line; then
    case "$line" in
      "*** Move to: "*)
        move_to=${line#"*** Move to: "}
        ;;
      *)
        push_patch_line "$line"
        ;;
    esac
  fi
  while next_patch_line; do
    if is_top_header "$line"; then
      push_patch_line "$line"
      break
    fi
    case "$line" in
      "*** Move to: "*)
        rm -f "$body"
        die "move marker must appear before update hunks"
        ;;
      *)
        printf '%s\n' "$line" >> "$body"
        ;;
    esac
  done
  if [ -s "$body" ]; then
    apply_update "$target" "$body"
  elif [ -z "$move_to" ] && [ ! -e "$target" ]; then
    rm -f "$body"
    die "file not found: $target"
  fi
  rm -f "$body"
  if [ -n "$move_to" ]; then
    [ -e "$target" ] || die "file not found: $target"
    [ ! -e "$move_to" ] || die "target file already exists: $move_to"
    ensure_parent "$move_to"
    mv "$target" "$move_to" || die "failed to move $target to $move_to"
  fi
}

main() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      -h|--help)
        printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin; --allow-loose bypasses low-context hunk guards\n'
        return 0
        ;;
      --allow-loose)
        allow_loose=1
        shift
        ;;
      *)
        die "unsupported option: $1"
        ;;
    esac
  done
  next_patch_line || die "patch must start with *** Begin Patch"
  [ "$line" = "*** Begin Patch" ] || die "patch must start with *** Begin Patch"
  while next_patch_line; do
    case "$line" in
      "*** End Patch")
        printf 'Done!\n'
        return 0
        ;;
      "*** Add File: "*)
        parse_add_file "${line#"*** Add File: "}"
        ;;
      "*** Delete File: "*)
        target=${line#"*** Delete File: "}
        rm -f "$target" || die "failed to delete $target"
        ;;
      "*** Update File: "*)
        parse_update_file "${line#"*** Update File: "}"
        ;;
      *)
        die "unexpected patch line: $line"
        ;;
    esac
  done
  die "missing *** End Patch"
}

main "$@"
