// Control UI component implements the file preview modal element.
import { css, html, type PropertyValues } from "lit";
import { property, query } from "lit/decorators.js";
import { t } from "../i18n/index.ts";
import { OpenClawLitElement } from "../lit/openclaw-element.ts";
import { renderCopyButton } from "./copy-button.ts";
import { icons } from "./icons.ts";
import "./modal-dialog.ts";

type FilePreviewModalFile = {
  path: string;
  size: string;
  contents: string;
};

export class OpenClawFilePreviewModal extends OpenClawLitElement {
  @property({ attribute: false }) files: FilePreviewModalFile[] = [];
  @property() activePath = "";
  @property() query = "";
  @property() label = "";
  @property() listLabel = "";
  @property() searchPlaceholder = "";
  @property() contextLabel = "";
  @property() readOnlyLabel = "";
  @property() emptyTitle = "";
  @property() emptySubtitle = "";
  @property() copyLabel = "";
  @query(".search") private searchInput?: HTMLInputElement;
  @query(".detail-body") private detailBody?: HTMLElement;

  private filteredFiles: FilePreviewModalFile[] = [];
  private activeFile?: FilePreviewModalFile;
  private derivedInputsReady = false;
  private codeSource?: string;
  private codeChunks: string[] = [];
  private resetScrollAfterUpdate = true;
  // Reconnection does not rerun firstUpdated; defer focus until shadow DOM is ready.
  private focusAfterUpdate = false;

  static override styles = css`
    :host {
      display: contents;
    }

    .modal {
      width: 100%;
      height: min(780px, 86vh);
      background: var(--bg);
      border: 1px solid var(--border-strong);
      border-radius: var(--radius-lg);
      box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    .head {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 16px 20px;
      border-bottom: 1px solid var(--border);
      background: var(--bg);
    }

    .search-icon {
      color: var(--muted);
      font-size: 18px;
    }

    .search {
      flex: 1;
      background: transparent;
      border: none;
      outline: none;
      color: var(--text-strong);
      font: inherit;
      font-size: 18px;
      font-weight: 400;
      padding: 4px 0;
    }

    .search:focus,
    .search:focus-visible {
      outline: none;
      border: none;
      box-shadow: none;
    }

    .search::placeholder {
      color: var(--muted);
    }

    .state {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      font-size: 12px;
      color: var(--muted);
      padding: 5px 10px;
      border: 1px solid var(--border);
      border-radius: var(--radius-md);
      background: var(--bg-elevated);
    }

    .kbd {
      font-family: var(--mono);
      border: 1px solid var(--border);
      color: var(--muted);
    }

    .body {
      flex: 1;
      display: grid;
      grid-template-columns: 360px 1fr;
      min-height: 0;
    }

    .list {
      border-right: 1px solid var(--border);
      padding: 14px 10px;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }

    .list-section {
      font-size: 11px;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: var(--muted);
      padding: 4px 12px 8px;
    }

    .item {
      display: grid;
      grid-template-columns: 16px 1fr auto;
      gap: 12px;
      align-items: center;
      padding: 12px 14px;
      border-radius: var(--radius-md);
      border: none;
      background: transparent;
      color: var(--text);
      font: inherit;
      outline: none;
      text-align: left;
    }

    .item:focus-visible {
      box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
    }

    .item:hover {
      background: var(--bg-elevated);
    }

    .item.is-active {
      background: var(--accent-subtle);
    }

    .item.is-active .item-name {
      color: var(--text-strong);
    }

    .item-icon {
      width: 16px;
      height: 16px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      color: var(--muted);
      opacity: 0.85;
    }

    .item.is-active .item-icon {
      color: var(--accent);
      opacity: 1;
    }

    .item-icon svg {
      width: 16px;
      height: 16px;
      stroke: currentColor;
      fill: none;
      stroke-width: 1.5px;
      stroke-linecap: round;
      stroke-linejoin: round;
    }

    .item-name {
      font-family: var(--mono);
      font-size: 14px;
      color: var(--text);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .item-meta {
      color: var(--muted);
      font-size: 12px;
    }

    .empty-list {
      color: var(--muted);
      font-size: 13px;
      padding: 12px;
    }

    .detail {
      display: flex;
      flex-direction: column;
      min-width: 0;
      min-height: 0;
    }

    .detail.empty {
      align-items: center;
      justify-content: center;
      text-align: center;
      padding: 24px;
    }

    .detail-head {
      padding: 20px 24px 14px;
      border-bottom: 1px solid var(--border);
    }

    .detail-title-row {
      display: flex;
      align-items: center;
      gap: 12px;
      margin-bottom: 10px;
    }

    .title {
      flex: 1;
      min-width: 0;
      margin: 0;
      font-family: var(--mono);
      font-size: 22px;
      color: var(--text-strong);
      font-weight: 700;
      letter-spacing: -0.01em;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .chat-copy-btn {
      width: 32px;
      height: 32px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      padding: 0;
      border: 1px solid var(--border);
      border-radius: var(--radius-md);
      background: var(--bg-elevated);
      color: var(--muted);
    }

    .chat-copy-btn:hover {
      border-color: var(--border-strong);
      color: var(--text-strong);
    }

    .chat-copy-btn:focus-visible {
      outline: 2px solid var(--accent);
      outline-offset: 2px;
    }

    .chat-copy-btn__icon {
      display: inline-flex;
      width: 16px;
      height: 16px;
      position: relative;
    }

    .chat-copy-btn__icon-copy,
    .chat-copy-btn__icon-check {
      position: absolute;
      inset: 0;
      transition: opacity 150ms ease;
    }

    .chat-copy-btn__icon-check {
      opacity: 0;
    }

    .chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-copy {
      opacity: 0;
    }

    .chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-check {
      opacity: 1;
    }

    .chat-copy-btn[data-copying="1"] {
      opacity: 0;
      pointer-events: none;
    }

    .chat-copy-btn[data-error="1"] {
      border-color: var(--danger-subtle);
      background: var(--danger-subtle);
      color: var(--danger);
    }

    .chat-copy-btn[data-copied="1"] {
      border-color: var(--ok-subtle);
      background: var(--ok-subtle);
      color: var(--ok);
    }

    .chat-copy-btn svg {
      width: 16px;
      height: 16px;
      stroke: currentColor;
      fill: none;
      stroke-width: 1.5px;
      stroke-linecap: round;
      stroke-linejoin: round;
    }

    .chips {
      display: flex;
      gap: 6px;
      flex-wrap: wrap;
    }

    .chip {
      display: inline-flex;
      align-items: center;
      padding: 3px 10px;
      border-radius: 999px;
      font-size: 11.5px;
      background: var(--bg-elevated);
      border: 1px solid var(--border);
      color: var(--muted);
    }

    .chip.accent {
      background: var(--accent-subtle);
      border-color: color-mix(in srgb, var(--accent) 30%, transparent);
      color: var(--accent);
    }

    .chip.ok {
      background: color-mix(in srgb, var(--ok) 12%, transparent);
      border-color: color-mix(in srgb, var(--ok) 30%, transparent);
      color: var(--ok);
    }

    .detail-body {
      flex: 1;
      overflow-x: hidden;
      overflow-y: auto;
      padding: 20px 24px 24px;
    }

    .code-content {
      min-width: 0;
    }

    .code-chunk {
      margin: 0;
      min-width: 0;
      font-family: var(--mono);
      font-size: 13px;
      line-height: 1.7;
      color: var(--text);
      white-space: pre-wrap;
      word-break: break-word;
      content-visibility: auto;
      contain-intrinsic-block-size: auto 1414px;
    }

    .foot {
      display: flex;
      align-items: center;
      gap: 18px;
      padding: 12px 20px;
      border-top: 1px solid var(--border);
      background: var(--bg);
      font-size: 12px;
      color: var(--muted);
    }

    .foot-group {
      display: inline-flex;
      align-items: center;
      gap: 6px;
    }

    .kbd {
      font-size: 10.5px;
      padding: 2px 6px;
      border-radius: 4px;
      background: var(--bg-elevated);
      color: var(--text);
    }

    .spacer {
      flex: 1;
    }

    .button {
      height: 36px;
      padding: 0 14px;
      border-radius: var(--radius-md);
      border: 1px solid var(--border);
      background: var(--bg-elevated);
      color: var(--text);
      font-weight: 600;
    }

    .button:hover {
      border-color: var(--border-strong);
      color: var(--text-strong);
    }

    .empty-title {
      font-size: 16px;
      font-weight: 600;
      color: var(--text-strong);
      margin: 0 0 8px;
    }

    .empty-subtitle {
      margin: 0;
      font-size: 13px;
      color: var(--muted);
      max-width: 380px;
    }
  `;

  protected override willUpdate(changed: PropertyValues<this>) {
    const inputsChanged =
      !this.derivedInputsReady ||
      changed.has("activePath") ||
      changed.has("query") ||
      changed.has("files");
    if (!inputsChanged) {
      return;
    }

    this.derivedInputsReady = true;
    this.filteredFiles = this.filterFiles();
    const nextActiveFile = this.resolveActiveFile(this.filteredFiles);
    this.activeFile = nextActiveFile;

    const nextCodeSource = nextActiveFile?.contents;
    if (nextCodeSource !== this.codeSource) {
      this.codeSource = nextCodeSource;
      this.codeChunks = nextCodeSource === undefined ? [] : chunkFileContents(nextCodeSource);
    }

    this.resetScrollAfterUpdate = true;
  }

  override render() {
    const filteredFiles = this.filteredFiles;
    const activeFile = this.activeFile;
    const fileCount =
      filteredFiles.length === this.files.length
        ? t("filePreview.fileCount", { count: String(this.files.length) })
        : t("filePreview.filteredFileCount", {
            count: String(filteredFiles.length),
            total: String(this.files.length),
          });
    const label = this.label || t("filePreview.label");
    const listLabel = this.listLabel || t("filePreview.listLabel");
    const searchPlaceholder = this.searchPlaceholder || t("filePreview.searchPlaceholder");

    return html`
      <openclaw-modal-dialog
        label=${label}
        style="--openclaw-modal-width: min(1100px, 92vw); --openclaw-modal-max-height: 86vh;"
        @modal-cancel=${this.emitClose}
        @keydown=${this.handleKeydown}
      >
        <div class="modal">
          <header class="head">
            <span class="search-icon">⌕</span>
            <input
              class="search"
              placeholder=${searchPlaceholder}
              .value=${this.query}
              @input=${this.handleQueryInput}
            />
            <span class="state">${fileCount}</span>
          </header>
          <div class="body">
            <aside class="list">
              <div class="list-section">${listLabel} · ${filteredFiles.length}</div>
              ${filteredFiles.length === 0
                ? html`<div class="empty-list">${t("filePreview.noMatches")}</div>`
                : filteredFiles.map(
                    (file) => html`
                      <button
                        class="item ${file.path === activeFile?.path ? "is-active" : ""}"
                        @pointerdown=${this.preventItemPointerFocus}
                        @mousedown=${this.preventItemPointerFocus}
                        @click=${() => this.emitSelect(file.path)}
                      >
                        <span class="item-icon">${iconForFile(file.path)}</span>
                        <span class="item-name">${file.path}</span>
                        <span class="item-meta">${file.size}</span>
                      </button>
                    `,
                  )}
            </aside>
            ${activeFile ? this.renderFile(activeFile) : this.renderEmpty()}
          </div>
          <footer class="foot">
            <span class="foot-group"><span class="kbd">↑↓</span> ${t("filePreview.navigate")}</span>
            <span class="spacer"></span>
            <button class="button" @click=${this.emitClose}>
              ${t("common.close")} <span class="kbd">esc</span>
            </button>
          </footer>
        </div>
      </openclaw-modal-dialog>
    `;
  }

  private renderFile(file: FilePreviewModalFile) {
    return html`
      <section class="detail">
        <div class="detail-head">
          <div class="detail-title-row">
            <h2 class="title">${file.path}</h2>
            ${file.contents
              ? renderCopyButton(file.contents, this.copyLabel || t("filePreview.copyFile"))
              : ""}
          </div>
          <div class="chips">
            <span class="chip accent">${fileKind(file.path)}</span>
            <span class="chip">${file.size}</span>
            <span class="chip">${this.readOnlyLabel || t("filePreview.readOnly")}</span>
            ${this.contextLabel ? html`<span class="chip ok">${this.contextLabel}</span>` : ""}
          </div>
        </div>
        <div class="detail-body">
          <div class="code-content">
            ${this.codeChunks.map(
              (chunk, index) => html`<pre class="code-chunk" data-chunk=${index}>${chunk}</pre>`,
            )}
          </div>
        </div>
      </section>
    `;
  }

  private renderEmpty() {
    return html`
      <section class="detail empty">
        <p class="empty-title">${this.emptyTitle || t("filePreview.emptyTitle")}</p>
        <p class="empty-subtitle">${this.emptySubtitle || t("filePreview.emptySubtitle")}</p>
      </section>
    `;
  }

  private filterFiles(): FilePreviewModalFile[] {
    const normalizedQuery = this.query.trim().toLowerCase();
    if (!normalizedQuery) {
      return this.files;
    }
    return this.files.filter((file) => {
      const haystack = `${file.path}\n${file.contents}`.toLowerCase();
      return haystack.includes(normalizedQuery);
    });
  }

  private resolveActiveFile(files: FilePreviewModalFile[]): FilePreviewModalFile | undefined {
    return files.find((file) => file.path === this.activePath) ?? files[0];
  }

  override connectedCallback() {
    super.connectedCallback();
    this.resetScrollAfterUpdate = true;
    this.focusAfterUpdate = true;
    this.requestUpdate();
  }

  protected override updated(changed: PropertyValues<this>) {
    if (this.resetScrollAfterUpdate) {
      this.resetScrollAfterUpdate = false;
      const body = this.detailBody;
      if (body) {
        body.scrollTop = 0;
        body.scrollLeft = 0;
      }
    }
    if (changed.has("activePath") || changed.has("query") || changed.has("files")) {
      this.scrollActiveFileIntoView();
    }
    if (this.focusAfterUpdate && this.isConnected) {
      this.focusAfterUpdate = false;
      this.focusModal();
    }
  }

  private handleQueryInput = (event: Event) => {
    const nextQuery = (event.target as HTMLInputElement).value ?? "";
    this.dispatchEvent(
      new CustomEvent<string>("file-preview-query-change", {
        bubbles: true,
        composed: true,
        detail: nextQuery,
      }),
    );
  };

  private preventItemPointerFocus = (event: Event) => {
    event.preventDefault();
  };

  private handleKeydown = (event: KeyboardEvent) => {
    switch (event.key) {
      case "Escape":
        event.preventDefault();
        event.stopPropagation();
        this.emitClose();
        return;
      case "ArrowDown":
        this.moveSelection(1, event);
        return;
      case "ArrowUp":
        this.moveSelection(-1, event);
      default:
    }
  };

  private focusModal() {
    const target = this.searchInput ?? this.shadowRoot?.querySelector<HTMLElement>(".modal");
    target?.focus({ preventScroll: true });
  }

  private moveSelection(offset: number, event: KeyboardEvent) {
    event.preventDefault();
    event.stopPropagation();
    const files = this.filterFiles();
    if (files.length === 0) {
      return;
    }
    const activeFile = this.resolveActiveFile(files);
    const currentIndex = activeFile ? files.findIndex((file) => file.path === activeFile.path) : -1;
    const nextIndex = Math.max(0, Math.min(files.length - 1, currentIndex + offset));
    const nextFile = files[nextIndex];
    if (nextFile && nextFile.path !== activeFile?.path) {
      this.emitSelect(nextFile.path);
    }
  }

  private scrollActiveFileIntoView() {
    this.updateComplete
      .then(() => {
        if (!this.isConnected) {
          return;
        }
        this.shadowRoot
          ?.querySelector<HTMLElement>(".item.is-active")
          ?.scrollIntoView({ block: "nearest" });
      })
      .catch(() => {});
  }

  private emitSelect(path: string) {
    this.dispatchEvent(
      new CustomEvent<string>("file-preview-select", {
        bubbles: true,
        composed: true,
        detail: path,
      }),
    );
    this.focusModal();
  }

  private emitClose = () => {
    this.dispatchEvent(
      new CustomEvent("file-preview-close", {
        bubbles: true,
        composed: true,
      }),
    );
  };
}

const FILE_PREVIEW_CHUNK_LINES = 64;

function chunkFileContents(contents: string): string[] {
  const lines = contents.split("\n");
  const chunks: string[] = [];
  for (let index = 0; index < lines.length; index += FILE_PREVIEW_CHUNK_LINES) {
    chunks.push(lines.slice(index, index + FILE_PREVIEW_CHUNK_LINES).join("\n"));
  }
  return chunks;
}

function fileKind(path: string): string {
  const ext = path.split(".").pop()?.toLowerCase() ?? "";
  const map: Record<string, string> = {
    md: "Markdown",
    txt: t("filePreview.kind.text"),
    json: "JSON",
    yaml: "YAML",
    yml: "YAML",
    ts: "TypeScript",
    js: "JavaScript",
    py: "Python",
    sh: t("filePreview.kind.shell"),
  };
  return map[ext] ?? (ext ? ext.toUpperCase() : t("filePreview.kind.file"));
}

const CODE_EXTENSIONS = new Set([
  "ts",
  "tsx",
  "js",
  "jsx",
  "mjs",
  "cjs",
  "py",
  "sh",
  "bash",
  "zsh",
  "rb",
  "go",
  "rs",
  "java",
  "kt",
  "swift",
  "c",
  "cc",
  "cpp",
  "h",
  "hpp",
  "json",
  "yaml",
  "yml",
  "toml",
  "xml",
  "html",
  "css",
  "scss",
  "sql",
]);

function iconForFile(path: string) {
  const ext = path.split(".").pop()?.toLowerCase() ?? "";
  return CODE_EXTENSIONS.has(ext) ? icons.fileCode : icons.fileText;
}

declare global {
  interface HTMLElementTagNameMap {
    "openclaw-file-preview-modal": OpenClawFilePreviewModal;
  }
}
