import ArchBadge from "@/components/ArchBadge";
import { Button } from "@/components/ui/Button";
import type { DiffItem, SkippedRegion } from "@/lib/bump/planner";
import type { Architecture, Region, SectionKey } from "@/lib/types";
import type { FileSyncDiffRow } from "@/lib/sync/file-sync/types";

interface PRMetaPreview {
  branchName: string;
  prTitle: string;
  prBody: string;
  scope: string;
}

interface AtomicValueDiff {
  subPath: string;
  oldValue: unknown;
  newValue: unknown;
}

function groupRowsByRegion(rows: FileSyncDiffRow[]) {
  const regionMap = new Map<string, FileSyncDiffRow[]>();

  for (const row of rows) {
    const items = regionMap.get(row.regionId) ?? [];
    items.push(row);
    regionMap.set(row.regionId, items);
  }

  return Array.from(regionMap.entries())
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([regionId, items]) => ({
      regionId,
      rows: items.sort((a, b) => {
        const fileCmp = a.filePath.localeCompare(b.filePath);
        if (fileCmp !== 0) return fileCmp;
        return a.path.localeCompare(b.path);
      }),
    }));
}

function stringifyOneLine(value: unknown): string {
  if (value === undefined) return "undefined";
  if (value === null) return "null";
  if (typeof value === "string") return JSON.stringify(value);
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  try {
    return JSON.stringify(value) ?? "undefined";
  } catch {
    return String(value);
  }
}

function truncateMiddle(input: string, keep = 80): string {
  if (input.length <= keep) return input;
  const head = Math.floor(keep / 2);
  const tail = keep - head;
  return `${input.slice(0, head)}...${input.slice(input.length - tail)}`;
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function collectAtomicDiffs(args: {
  oldValue: unknown;
  newValue: unknown;
  currentPath: string;
  out: AtomicValueDiff[];
  limit: number;
}) {
  const { oldValue, newValue, currentPath, out, limit } = args;
  if (out.length >= limit) return;
  if (Object.is(oldValue, newValue)) return;

  if (Array.isArray(oldValue) && Array.isArray(newValue)) {
    const maxLen = Math.max(oldValue.length, newValue.length);
    for (let i = 0; i < maxLen; i += 1) {
      if (out.length >= limit) return;
      const hasOld = i < oldValue.length;
      const hasNew = i < newValue.length;
      const nextPath = `${currentPath}[${i}]`;

      if (!hasOld || !hasNew) {
        out.push({
          subPath: nextPath,
          oldValue: hasOld ? oldValue[i] : undefined,
          newValue: hasNew ? newValue[i] : undefined,
        });
        continue;
      }

      collectAtomicDiffs({
        oldValue: oldValue[i],
        newValue: newValue[i],
        currentPath: nextPath,
        out,
        limit,
      });
    }
    return;
  }

  if (isPlainObject(oldValue) && isPlainObject(newValue)) {
    const keys = Array.from(
      new Set([...Object.keys(oldValue), ...Object.keys(newValue)])
    ).sort();

    for (const key of keys) {
      if (out.length >= limit) return;
      const hasOld = Object.prototype.hasOwnProperty.call(oldValue, key);
      const hasNew = Object.prototype.hasOwnProperty.call(newValue, key);
      const nextPath = currentPath ? `${currentPath}.${key}` : key;

      if (!hasOld || !hasNew) {
        out.push({
          subPath: nextPath,
          oldValue: hasOld ? oldValue[key] : undefined,
          newValue: hasNew ? newValue[key] : undefined,
        });
        continue;
      }

      collectAtomicDiffs({
        oldValue: oldValue[key],
        newValue: newValue[key],
        currentPath: nextPath,
        out,
        limit,
      });
    }
    return;
  }

  out.push({
    subPath: currentPath || "(value)",
    oldValue,
    newValue,
  });
}

function buildAtomicDiffPreview(oldValue: unknown, newValue: unknown, force = false): {
  items: AtomicValueDiff[];
  hasMore: boolean;
} | null {
  const limit = 9;
  const all: AtomicValueDiff[] = [];
  collectAtomicDiffs({
    oldValue,
    newValue,
    currentPath: "",
    out: all,
    limit,
  });

  if (all.length === 0 || (all.length === 1 && !force)) {
    return null;
  }

  const maxVisible = 8;
  return {
    items: all.slice(0, maxVisible),
    hasMore: all.length > maxVisible,
  };
}

function projectOldValueForMergePatch(oldValue: unknown, patchValue: unknown): unknown {
  if (Array.isArray(oldValue) || Array.isArray(patchValue)) {
    return oldValue;
  }

  if (!isPlainObject(oldValue) || !isPlainObject(patchValue)) {
    return oldValue;
  }

  const projected: Record<string, unknown> = {};
  for (const key of Object.keys(patchValue)) {
    projected[key] = projectOldValueForMergePatch(oldValue[key], patchValue[key]);
  }
  return projected;
}

function getPreviewValues(row: FileSyncDiffRow): { oldValue: unknown; newValue: unknown } {
  if (row.op !== "merge") {
    return { oldValue: row.oldValue, newValue: row.newValue };
  }

  return {
    oldValue: projectOldValueForMergePatch(row.oldValue, row.newValue),
    newValue: row.newValue,
  };
}

interface FocusedLine {
  leftEllipsis: boolean;
  left: string;
  changed: string;
  right: string;
  rightEllipsis: boolean;
}

interface FocusedDiffLines {
  oldLine: FocusedLine;
  newLine: FocusedLine;
}

function buildFocusedDiffLines(oldValue: unknown, newValue: unknown): FocusedDiffLines {
  const oldText = stringifyOneLine(oldValue);
  const newText = stringifyOneLine(newValue);

  if (oldText === newText) {
    const same = truncateMiddle(oldText);
    return {
      oldLine: {
        leftEllipsis: false,
        left: "",
        changed: same,
        right: "",
        rightEllipsis: false,
      },
      newLine: {
        leftEllipsis: false,
        left: "",
        changed: same,
        right: "",
        rightEllipsis: false,
      },
    };
  }

  let prefix = 0;
  const maxPrefix = Math.min(oldText.length, newText.length);
  while (prefix < maxPrefix && oldText[prefix] === newText[prefix]) {
    prefix += 1;
  }

  let oldSuffix = oldText.length - 1;
  let newSuffix = newText.length - 1;
  while (oldSuffix >= prefix && newSuffix >= prefix && oldText[oldSuffix] === newText[newSuffix]) {
    oldSuffix -= 1;
    newSuffix -= 1;
  }

  const oldChanged = oldText.slice(prefix, oldSuffix + 1);
  const newChanged = newText.slice(prefix, newSuffix + 1);
  const leftContext = 36;
  const rightContext = 36;

  const oldLeftStart = Math.max(0, prefix - leftContext);
  const newLeftStart = Math.max(0, prefix - leftContext);
  const oldRightEnd = Math.min(oldText.length, oldSuffix + 1 + rightContext);
  const newRightEnd = Math.min(newText.length, newSuffix + 1 + rightContext);

  const oldLeft = oldText.slice(oldLeftStart, prefix);
  const newLeft = newText.slice(newLeftStart, prefix);
  const oldRight = oldText.slice(oldSuffix + 1, oldRightEnd);
  const newRight = newText.slice(newSuffix + 1, newRightEnd);

  const maxChangedLength = 72;

  return {
    oldLine: {
      leftEllipsis: oldLeftStart > 0,
      left: oldLeft,
      changed: truncateMiddle(oldChanged, maxChangedLength),
      right: oldRight,
      rightEllipsis: oldRightEnd < oldText.length,
    },
    newLine: {
      leftEllipsis: newLeftStart > 0,
      left: newLeft,
      changed: truncateMiddle(newChanged, maxChangedLength),
      right: newRight,
      rightEllipsis: newRightEnd < newText.length,
    },
  };
}

function renderHighlightedDiffLine(args: {
  sign: "-" | "+";
  line: FocusedLine;
  tone: "old" | "new";
}) {
  const { sign, line, tone } = args;
  const highlightClass =
    tone === "old"
      ? "bg-danger/20 text-danger px-0.5 rounded-sm"
      : "bg-success/20 text-success px-0.5 rounded-sm";

  return (
    <>
      {sign}{" "}
      {line.leftEllipsis ? "..." : ""}
      {line.left}
      <span className={highlightClass}>{line.changed || "∅"}</span>
      {line.right}
      {line.rightEllipsis ? "..." : ""}
    </>
  );
}

function renderSimpleValue(value: unknown): string {
  return truncateMiddle(stringifyOneLine(value), 96);
}

const SECTION_LABELS: Record<SectionKey, string> = {
  k8s: "K8s",
  tidbcluster: "TidbCluster",
  sharedpool: "SharedPool",
  tidbgroup: "TidbGroup",
};

const SECTION_BADGE_COLORS: Record<SectionKey, string> = {
  k8s: "border-blue-500/30 bg-blue-500/10 text-blue-400",
  tidbcluster: "border-purple-500/30 bg-purple-500/10 text-purple-400",
  sharedpool: "border-rose-500/30 bg-rose-500/10 text-rose-400",
  tidbgroup: "border-cyan-500/30 bg-cyan-500/10 text-cyan-400",
};

const ARCH_LABELS: Record<Architecture, string> = {
  dedicated: "Dedicated",
  nextgen: "Nextgen",
  serverless: "Serverless",
  byoc: "BYOC",
};

function SkippedRegionsWarning({ skippedRegions }: { skippedRegions: SkippedRegion[] }) {
  return (
    <div className="bg-warning-muted border border-warning/30 rounded-lg p-4 space-y-2">
      <div className="text-sm font-semibold text-warning">
        Some regions will not be affected
      </div>
      <p className="text-xs text-text-secondary">
        The following regions do not support the requested section(s) for their architecture and will be skipped.
      </p>
      <ul className="space-y-1 mt-2">
        {skippedRegions.map((s) => (
          <li
            key={s.regionId}
            className="text-xs flex flex-wrap items-center gap-2"
          >
            <ArchBadge architecture={s.architecture} />
            <code className="font-mono text-text-secondary">{s.regionId}</code>
            <span className="text-text-muted">
              ({ARCH_LABELS[s.architecture]} does not support{" "}
              {s.skippedSections.map((sec) => SECTION_LABELS[sec]).join(", ")})
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default function PreviewPanel({
  mode = "image",
  sourceRegionId = "",
  prMeta,
  targetTag,
  selectedRegions,
  diffs,
  skippedRegions,
  bumping,
  onBack,
  onConfirm,
  configSyncSummary,
  configSyncDiffs,
  configSyncFailures,
}: {
  mode?: "image" | "file-sync";
  sourceRegionId?: string;
  prMeta: PRMetaPreview;
  targetTag: string;
  selectedRegions: Region[];
  diffs: DiffItem[];
  skippedRegions?: SkippedRegion[];
  bumping: boolean;
  onBack: () => void;
  onConfirm: () => void;
  configSyncSummary?: {
    totalTargets: number;
    applicableTargets: number;
    failedTargets: number;
    totalChanges: number;
  };
  configSyncDiffs?: FileSyncDiffRow[];
  configSyncFailures?: Array<{
    regionId: string;
    error: { code: string; message: string; hint?: string };
  }>;
}) {
  if (mode === "file-sync") {
    const rows = configSyncDiffs || [];
    const failures = configSyncFailures || [];
    const summary = configSyncSummary || {
      totalTargets: 0,
      applicableTargets: 0,
      failedTargets: failures.length,
      totalChanges: rows.length,
    };
    const title = "File Sync Summary";
    const groupedRows = groupRowsByRegion(rows);

    return (
      <div className="space-y-4">
        <div className="bg-surface-0 rounded-lg border border-surface-2 p-5 space-y-2">
          <h3 className="text-sm font-semibold uppercase tracking-wider text-text-secondary">
            Release Title
          </h3>
          <p className="text-sm">{prMeta.prTitle}</p>
          <p className="text-xs text-text-muted">Source region: {sourceRegionId || "-"}</p>
          <p className="text-xs text-text-muted whitespace-pre-wrap">{prMeta.prBody}</p>
        </div>

        <div className="bg-surface-0 rounded-lg border border-surface-2 p-5 space-y-3">
          <h3 className="text-sm font-semibold uppercase tracking-wider text-text-secondary">
            {title}
          </h3>
          <div className="flex flex-wrap gap-2 text-xs">
            <span className="px-2 py-1 rounded-md bg-surface-1 text-text-secondary">
              {summary.totalTargets} targets
            </span>
            <span className="px-2 py-1 rounded-md bg-surface-1 text-success">
              {summary.applicableTargets} applicable
            </span>
            <span className="px-2 py-1 rounded-md bg-surface-1 text-warning">
              {summary.failedTargets} failed
            </span>
            <span className="px-2 py-1 rounded-md bg-surface-1 text-accent">
              {summary.totalChanges} changes
            </span>
          </div>

          {failures.length > 0 && (
            <div className="rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-text-secondary space-y-1">
              {failures.map((failure) => (
                <div key={failure.regionId} className="break-all">
                  <span className="font-mono text-text-primary">{failure.regionId}</span>
                  {": "}
                  <span>[{failure.error.code}] {failure.error.message}</span>
                </div>
              ))}
            </div>
          )}
        </div>

        <div className="space-y-3">
          {groupedRows.length === 0 ? (
            <div className="bg-surface-0 rounded-lg border border-surface-2 p-4 text-sm text-text-secondary">
              No previewable changes.
            </div>
          ) : (
            groupedRows.map((regionGroup) => {
              const files = Array.from(new Set(regionGroup.rows.map((r) => r.filePath)));
              const singleFile = files.length === 1 ? files[0] : null;

              return (
                <div key={regionGroup.regionId} className="bg-surface-0 rounded-lg border border-surface-2 overflow-hidden">
                  <div className="px-4 py-3 border-b border-surface-2 bg-surface-1/40 flex items-center justify-between gap-2">
                    <span className="font-mono text-[13px] text-text-primary">{regionGroup.regionId}</span>
                    <span className="text-xs text-text-muted">{regionGroup.rows.length} changes</span>
                  </div>

                  {singleFile ? (
                    <div className="px-4 py-2 text-xs font-mono text-text-muted border-b border-surface-2/60">
                      {singleFile}
                    </div>
                  ) : null}
                  <div className="px-4 py-3 space-y-3">
                    {regionGroup.rows.map((row, i) => {
                      const previewValues = getPreviewValues(row);
                      const focused = buildFocusedDiffLines(
                        previewValues.oldValue,
                        previewValues.newValue
                      );
                      // remove has no newValue; atomic preview would render
                      // bogus "+ undefined" rows, so bypass it.
                      const atomic =
                        row.op === "remove"
                          ? null
                          : buildAtomicDiffPreview(
                              previewValues.oldValue,
                              previewValues.newValue,
                              row.op === "merge"
                            );
                      return (
                        <div
                          key={`${regionGroup.regionId}:${row.filePath}:${row.path}:${i}`}
                          className="rounded-md border border-surface-2/70 bg-surface-1/20 overflow-hidden"
                        >
                          {!singleFile && (
                            <div className="px-3 py-1.5 border-b border-surface-2/70 font-mono text-[11px] text-text-muted break-all">
                              {row.filePath}
                            </div>
                          )}
                          {atomic ? (
                            <pre className="px-3 py-2 font-mono text-[12px] leading-5 whitespace-pre-wrap break-all">
                              <span className="text-accent">@@ {row.path} ({row.op}) @@{"\n"}</span>
                              {atomic.items.map((item, idx) => (
                                <span key={`${item.subPath}:${idx}`}>
                                  <span className="text-text-muted">~ {item.subPath}{"\n"}</span>
                                  <span className="text-danger/90">
                                    - {renderSimpleValue(item.oldValue)}
                                    {"\n"}
                                  </span>
                                  <span className="text-success">
                                    + {renderSimpleValue(item.newValue)}
                                    {"\n"}
                                  </span>
                                </span>
                              ))}
                              {atomic.hasMore && (
                                <span className="text-text-muted">... more nested changes</span>
                              )}
                            </pre>
                          ) : row.op === "add" ? (
                            <pre className="px-3 py-2 font-mono text-[12px] leading-5 whitespace-pre-wrap break-all">
                              <span className="text-accent">@@ {row.path} ({row.op}) @@{"\n"}</span>
                              <span className="text-success">
                                + {renderSimpleValue(row.newValue)}
                              </span>
                            </pre>
                          ) : row.op === "remove" ? (
                            <pre className="px-3 py-2 font-mono text-[12px] leading-5 whitespace-pre-wrap break-all">
                              <span className="text-accent">@@ {row.path} ({row.op}) @@{"\n"}</span>
                              <span className="text-danger/90">
                                - {renderSimpleValue(row.oldValue)}
                              </span>
                            </pre>
                          ) : (
                            <pre className="px-3 py-2 font-mono text-[12px] leading-5 whitespace-pre-wrap break-all">
                              <span className="text-accent">@@ {row.path} ({row.op}) @@{"\n"}</span>
                              <span className="text-danger/90">
                                {renderHighlightedDiffLine({
                                  sign: "-",
                                  line: focused.oldLine,
                                  tone: "old",
                                })}
                                {"\n"}
                              </span>
                              <span className="text-success">
                                {renderHighlightedDiffLine({
                                  sign: "+",
                                  line: focused.newLine,
                                  tone: "new",
                                })}
                              </span>
                            </pre>
                          )}
                        </div>
                      );
                    })}
                  </div>
                </div>
              );
            })
          )}
        </div>

        <div className="flex gap-3">
          <Button
            onClick={onBack}
            variant="secondary"
          >
            Back
          </Button>
          <Button
            onClick={onConfirm}
            disabled={bumping}
            aria-busy={bumping}
          >
            {bumping ? "Applying..." : "Confirm & Apply"}
          </Button>
        </div>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      <div className="bg-surface-0 rounded-lg border border-surface-2 p-5 space-y-4">
        <h3 className="text-sm font-semibold uppercase tracking-wider text-text-secondary">
          PR Summary
        </h3>
        <div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2.5 text-sm">
          <span className="text-text-muted text-xs uppercase tracking-wider pt-0.5">
            Branch
          </span>
          <code className="font-mono text-[13px] text-accent">
            {prMeta.branchName}
          </code>
          <span className="text-text-muted text-xs uppercase tracking-wider pt-0.5">
            Title
          </span>
          <span>{prMeta.prTitle}</span>
          <span className="text-text-muted text-xs uppercase tracking-wider pt-0.5">
            Scope
          </span>
          <span>{prMeta.scope}</span>
          <span className="text-text-muted text-xs uppercase tracking-wider pt-0.5">
            Target
          </span>
          <code className="font-mono text-[13px] text-accent">{targetTag}</code>
          <span className="text-text-muted text-xs uppercase tracking-wider pt-0.5">
            Regions
          </span>
          <div className="flex flex-wrap gap-1.5">
            {selectedRegions.map((r) => (
              <span
                key={r.id}
                className="inline-flex items-center gap-1.5 px-2 py-1 bg-surface-1 rounded-md text-xs"
              >
                <ArchBadge architecture={r.architecture} />
                <span className="text-text-secondary">{r.environment}</span>
                <span className="font-mono text-text-muted">{r.regionName}</span>
              </span>
            ))}
          </div>
        </div>
      </div>

      {skippedRegions && skippedRegions.length > 0 && (
        <SkippedRegionsWarning skippedRegions={skippedRegions} />
      )}

      <div className="text-sm text-text-secondary">{diffs.length} file(s) will be changed:</div>

      <div className="bg-surface-0 rounded-lg border border-surface-2 overflow-hidden">
        <table className="w-full text-sm">
          <thead>
            <tr className="border-b border-surface-2 bg-surface-1/50">
              <th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-text-muted">
                File
              </th>
              <th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-text-muted">
                Changes
              </th>
            </tr>
          </thead>
          <tbody>
            {diffs.map((d, i) => (
              <tr
                key={i}
                className="border-b border-surface-2/50 last:border-0 hover:bg-surface-1 transition-colors"
              >
                <td className="px-4 py-3 font-mono text-[13px] text-text-secondary max-w-xs truncate">
                  {d.filePath}
                </td>
                <td className="px-4 py-3">
                  <div className="font-mono text-[13px] space-y-0.5">
                    {d.sections && d.sections.length > 0 && (
                      <div className="flex flex-wrap gap-1 mb-1">
                        {d.sections.map((s) => (
                          <span
                            key={s}
                            className={`inline-flex items-center rounded-md border px-1.5 py-px text-[11px] font-medium ${SECTION_BADGE_COLORS[s]}`}
                          >
                            {SECTION_LABELS[s]}
                          </span>
                        ))}
                      </div>
                    )}
                    <div className="flex items-center gap-1.5">
                      <span className="text-danger/60 select-none w-3 text-center">-</span>
                      <span className="text-danger/80 line-through decoration-danger/30">
                        {d.oldTag}
                      </span>
                    </div>
                    <div className="flex items-center gap-1.5">
                      <span className="text-success/60 select-none w-3 text-center">+</span>
                      <span className="text-success">{d.newTag}</span>
                    </div>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <div className="flex gap-3">
        <Button
          onClick={onBack}
          variant="secondary"
        >
          Back
        </Button>
        <Button
          onClick={onConfirm}
          disabled={bumping}
          aria-busy={bumping}
        >
          {bumping ? "Bumping..." : "Confirm & Create PR"}
        </Button>
      </div>
    </div>
  );
}
