<script lang="ts">
  import { LoaderCircle, X } from "@lucide/svelte";
  import type { GitFileStatus, PatchApplyAction } from "../types";

  type PatchLineKind = "context" | "add" | "delete" | "meta";

  interface PatchLine {
    id: string;
    text: string;
    kind: PatchLineKind;
  }

  interface PatchHunk {
    id: string;
    header: string;
    lines: PatchLine[];
  }

  interface ParsedPatch {
    headerLines: string[];
    hunks: PatchHunk[];
    binary: boolean;
  }

  interface Props {
    file: GitFileStatus;
    staged: boolean;
    patch: string;
    isBusy: boolean;
    isLoading: boolean;
    error: string;
    onClose: () => void;
    onRefresh: () => void | Promise<void>;
    onApply: (action: PatchApplyAction, patch: string) => void | Promise<void>;
  }

  let {
    file,
    staged = false,
    patch = "",
    isBusy = false,
    isLoading = false,
    error = "",
    onClose = () => {},
    onRefresh = () => {},
    onApply = () => {},
  }: Props = $props();

  let parsed = $state<ParsedPatch>({ headerLines: [], hunks: [], binary: false });

  let scopeLabel = $derived(staged ? "Staged changes" : "Unstaged changes");
  let displayPath = $derived(file.old_path ? `${file.old_path} -> ${file.path}` : file.path);

  $effect(() => {
    parsed = parsePatch(patch);
  });

  function parsePatch(input: string): ParsedPatch {
    const normalized = input.replace(/\r\n/g, "\n");
    const lines = normalized.split("\n");
    if (lines[lines.length - 1] === "") lines.pop();

    const headerLines: string[] = [];
    const hunks: PatchHunk[] = [];
    let current: PatchHunk | null = null;

    for (const line of lines) {
      if (line.startsWith("@@ ")) {
        current = { id: `hunk-${hunks.length}`, header: line, lines: [] };
        hunks.push(current);
        continue;
      }

      if (!current) {
        headerLines.push(line);
        continue;
      }

      const kind = patchLineKind(line);
      current.lines.push({
        id: `${current.id}-line-${current.lines.length}`,
        text: line,
        kind,
      });
    }

    return {
      headerLines,
      hunks,
      binary: /(^|\n)(Binary files|GIT binary patch|literal \d+)/.test(normalized),
    };
  }

  function patchLineKind(line: string): PatchLineKind {
    if (line.startsWith("+") && !line.startsWith("+++")) return "add";
    if (line.startsWith("-") && !line.startsWith("---")) return "delete";
    if (line.startsWith(" ")) return "context";
    return "meta";
  }

  function linePrefix(line: PatchLine): string {
    if (line.kind === "add") return "+";
    if (line.kind === "delete") return "-";
    if (line.kind === "meta") return "\\";
    return " ";
  }

  function lineBody(line: PatchLine): string {
    if (line.kind === "meta") return line.text;
    return line.text.slice(1);
  }

  function buildHunkPatch(hunk: PatchHunk): string {
    return `${[...parsed.headerLines, hunk.header, ...hunk.lines.map((line) => line.text)].join("\n")}\n`;
  }

  async function applyHunkAction(action: PatchApplyAction, hunk: PatchHunk) {
    if (isBusy || isLoading) return;
    await onApply(action, buildHunkPatch(hunk));
  }
</script>

<div class="dialog-backdrop" role="presentation">
  <div class="dialog line-patch-dialog" role="dialog" aria-modal="true" aria-label="Line patch">
    <header class="dialog-header">
      <div>
        <span class="eyebrow">{scopeLabel}</span>
        <p class="dialog-title" title={displayPath}>{displayPath}</p>
      </div>
      <div class="dialog-header-actions">
        <button class="btn-sm" type="button" onclick={onRefresh} disabled={isBusy || isLoading}>Refresh</button>
        <button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
          <X size={16} aria-hidden="true" />
        </button>
      </div>
    </header>

    <div class="line-patch-body">
      {#if isLoading}
        <div class="blank-state">
          <LoaderCircle class="spin" size={18} aria-hidden="true" />
          Loading patch...
        </div>
      {:else if error}
        <div class="blank-state">{error}</div>
      {:else if !patch.trim()}
        <div class="blank-state">No line patch available for this file.</div>
      {:else if parsed.binary || parsed.hunks.length === 0}
        <div class="blank-state">This change cannot be split into text lines.</div>
      {:else}
        <div class="line-patch-scroll">
          {#each parsed.hunks as hunk (hunk.id)}
            <section class="line-patch-hunk">
              <div class="line-patch-hunk-head">
                <code>{hunk.header}</code>
                <div class="line-patch-hunk-actions">
                  {#if staged}
                    <button class="line-patch-hunk-button discard" type="button" onclick={() => applyHunkAction("discard-staged", hunk)} disabled={isBusy}>
                      Discard Hunk
                    </button>
                    <button class="line-patch-hunk-button unstage" type="button" onclick={() => applyHunkAction("unstage", hunk)} disabled={isBusy}>
                      Unstage Hunk
                    </button>
                  {:else}
                    <button class="line-patch-hunk-button discard" type="button" onclick={() => applyHunkAction("discard-unstaged", hunk)} disabled={isBusy}>
                      Discard Hunk
                    </button>
                    <button class="line-patch-hunk-button stage" type="button" onclick={() => applyHunkAction("stage", hunk)} disabled={isBusy}>
                      Stage Hunk
                    </button>
                  {/if}
                </div>
              </div>

              <div class="line-patch-lines">
                {#each hunk.lines as line (line.id)}
                  <div class={`line-patch-row ${line.kind}`}>
                    <span class="line-patch-prefix">{linePrefix(line)}</span>
                    <code>{lineBody(line)}</code>
                  </div>
                {/each}
              </div>
            </section>
          {/each}
        </div>
      {/if}
    </div>
  </div>
</div>
