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

  const commitDateFormatter = new Intl.DateTimeFormat(undefined, {
    dateStyle: "medium",
    timeStyle: "short",
  });

  interface BlameGroup {
    id: string;
    hash: string;
    shortHash: string;
    authorName: string;
    authorEmail: string;
    summary: string;
    authorTime: number;
    isUncommitted: boolean;
    lines: GitBlameLine[];
  }

  interface TextSegment {
    text: string;
    matched: boolean;
  }

  interface Props {
    filePath: string;
    lines: GitBlameLine[];
    isBusy: boolean;
    isLoading: boolean;
    error: string;
    onClose: () => void;
  }

  let {
    filePath = "",
    lines = [],
    isBusy = false,
    isLoading = false,
    error = "",
    onClose = () => {},
  }: Props = $props();

  let blameSearch = $state("");

  function groupBlameLines(source: GitBlameLine[]): BlameGroup[] {
    const groups: BlameGroup[] = [];
    for (const line of source) {
      const last = groups[groups.length - 1];
      if (last && last.hash === line.commit_hash) {
        last.lines.push(line);
        continue;
      }
      groups.push({
        id: `${line.commit_hash}-${line.line_number}`,
        hash: line.commit_hash,
        shortHash: line.short_hash,
        authorName: line.author_name,
        authorEmail: line.author_email,
        summary: line.summary,
        authorTime: line.author_time,
        isUncommitted: line.is_uncommitted,
        lines: [line],
      });
    }
    return groups;
  }

  function formatBlameDate(seconds: number): string {
    if (!seconds) return "";
    const date = new Date(seconds * 1000);
    if (Number.isNaN(date.getTime())) return "";
    return commitDateFormatter.format(date);
  }

  function groupTooltip(group: BlameGroup): string {
    if (group.isUncommitted) return "Not committed yet";
    return `${group.authorName} <${group.authorEmail}>\n${group.summary}\n${group.hash}`;
  }

  function blameLineSearchText(line: GitBlameLine): string {
    return [
      line.line_number,
      line.content,
      line.commit_hash,
      line.short_hash,
      line.author_name,
      line.author_email,
      formatBlameDate(line.author_time),
      line.summary,
      line.is_uncommitted ? "uncommitted not committed yet" : "",
    ].join("\n").toLowerCase();
  }

  function lineMatchesSearch(line: GitBlameLine, query: string): boolean {
    return !query || blameLineSearchText(line).includes(query);
  }

  function textSegments(value: string | number): TextSegment[] {
    const text = String(value ?? "");
    const query = blameSearch.trim();
    if (!query || !text) return [{ text, matched: false }];

    const lowerText = text.toLowerCase();
    const lowerQuery = query.toLowerCase();
    const segments: TextSegment[] = [];
    let cursor = 0;
    let matchIndex = lowerText.indexOf(lowerQuery);

    while (matchIndex !== -1) {
      if (matchIndex > cursor) {
        segments.push({ text: text.slice(cursor, matchIndex), matched: false });
      }
      const end = matchIndex + query.length;
      segments.push({ text: text.slice(matchIndex, end), matched: true });
      cursor = end;
      matchIndex = lowerText.indexOf(lowerQuery, cursor);
    }

    if (cursor < text.length) {
      segments.push({ text: text.slice(cursor), matched: false });
    }

    return segments.length > 0 ? segments : [{ text, matched: false }];
  }

  let searchQuery = $derived(blameSearch.trim().toLowerCase());
  let searchActive = $derived(searchQuery.length > 0);
  let visibleLines = $derived(searchActive ? lines.filter((line) => lineMatchesSearch(line, searchQuery)) : lines);
  let groups = $derived(groupBlameLines(visibleLines));
</script>

<div class="dialog-backdrop" role="presentation">
  <div class="dialog blame-dialog" role="dialog" aria-modal="true" aria-label="File blame">
    <header class="dialog-header">
      <div>
        <span class="eyebrow">Blame</span>
        <p class="dialog-title" title={filePath}>{filePath}</p>
      </div>
      <div class="dialog-header-actions">
        <span class="pill pill-count">{lines.length}</span>
        <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="dialog-diff blame-body">
      {#if isLoading}
        <div class="blank-state">
          <LoaderCircle class="spin" size={18} aria-hidden="true" />
          Loading blame...
        </div>
      {:else if error}
        <div class="blank-state">{error}</div>
      {:else if lines.length === 0}
        <div class="blank-state">No blame information available for this file.</div>
      {:else}
        <div class="diff-header blame-code-header">
          <FileCode size={13} aria-hidden="true" />
          <span title={filePath}>{filePath}</span>
          <strong>{searchActive ? `${visibleLines.length} of ${lines.length} lines` : `${lines.length} lines`}</strong>
        </div>
        <div class="blame-search-bar">
          <Search size={14} aria-hidden="true" />
          <input
            bind:value={blameSearch}
            autocomplete="off"
            spellcheck="false"
            placeholder="Search blame"
            aria-label="Search blame"
          />
          {#if searchActive}
            <button class="btn-sm blame-search-clear" type="button" onclick={() => { blameSearch = ""; }} aria-label="Clear blame search">
              <X size={14} aria-hidden="true" />
            </button>
          {/if}
        </div>
        <div class="split-col-headers blame-column-headers">
          <div class="split-col-label blame-commit-col-label">Commit</div>
          <div class="split-col-label blame-code-col-label">Code</div>
        </div>
        <div class="split-diff blame-diff" role="table" aria-label="File blame">
          <div class="split-pane blame-scroll">
            {#if groups.length === 0}
              <div class="blank-state">No matches found.</div>
            {:else}
              <div class="blame-code-table">
                {#each groups as group (group.id)}
                  <div class="blame-group" class:uncommitted={group.isUncommitted} title={groupTooltip(group)}>
                    <div class="blame-meta">
                      <span class="blame-hash">
                        {#each textSegments(group.isUncommitted ? "Uncommitted" : group.shortHash) as segment, index (`hash-${group.id}-${index}`)}
                          {#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
                        {/each}
                      </span>
                      <span class="blame-author">
                        {#each textSegments(group.isUncommitted ? "Not committed yet" : group.authorName) as segment, index (`author-${group.id}-${index}`)}
                          {#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
                        {/each}
                      </span>
                      <span class="blame-summary">
                        {#each textSegments(group.summary || "No commit message") as segment, index (`summary-${group.id}-${index}`)}
                          {#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
                        {/each}
                      </span>
                      {#if !group.isUncommitted}
                        <span class="blame-date">
                          {#each textSegments(formatBlameDate(group.authorTime)) as segment, index (`date-${group.id}-${index}`)}
                            {#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
                          {/each}
                        </span>
                      {/if}
                    </div>
                    <div class="split-pane-grid blame-lines">
                      {#each group.lines as line (line.line_number)}
                        <span class="split-num blame-line-number">{line.line_number}</span>
                        <code class="split-cell blame-line-code">
                          {#each textSegments(line.content) as segment, index (`line-${line.line_number}-${index}`)}
                            {#if segment.matched}<mark class="blame-search-hit">{segment.text}</mark>{:else}{segment.text}{/if}
                          {/each}
                        </code>
                      {/each}
                    </div>
                  </div>
                {/each}
              </div>
            {/if}
          </div>
        </div>
      {/if}
    </div>
  </div>
</div>
