/**
 * v1 → v2 템플릿 저장 포맷 마이그레이션
 * ============================================================================
 *
 * DB 의 TEMPLATES.VIEWS jsonb 를 v2 스키마 (% 좌표 + role + responsivePolicy)
 * 로 변환한다. 런타임에 로드 시 version 필드 확인 후 v1 이면 즉석 변환.
 *
 * role / responsivePolicy 추론은 마이그레이션 1회 규칙이다. 일단 현재 렌더
 * 결과와 비슷하게 나오도록 좌표 기반 추론을 사용하되, v2 포맷으로 저장된
 * 이후엔 디자이너가 명시 값으로 덮어쓰는 것을 전제로 한다.
 *
 * 설계 문서: notes/gbpark/2026-04-20-template-storage-model-v2.md
 * ============================================================================
 */

import type {
  BlockV2,
  CanvasV2,
  ResponsivePolicy,
  TemplateViewsV2,
  ViewV2,
} from '@/types/invyone-component';

// ─────────────────────────────────────────────────────────────────────────────
// 1. v1 파싱 헬퍼
// ─────────────────────────────────────────────────────────────────────────────

/** v1 저장본의 다양한 legacy componentId 를 통합 ID 로 정규화. */
const LEGACY_TO_UNIFIED: Record<string, string> = {
  'v2-divider-line': 'divider',
  'divider-line': 'divider',
  'v2-split-line': 'divider',
  'v2-text-display': 'title',
  'text-display': 'title',
  'v2-button-primary': 'button',
  'button-primary': 'button',
  'v2-table-search-widget': 'search',
  'table-search-widget': 'search',
  // V2 입력/선택 (Phase D.2) · 기본 입력 6종 (Phase E) · radio-basic/toggle-switch
  // (Phase F.1) 폐기 — runtime alias 모두 제거.
  'v2-aggregation-widget': 'stats',
  'aggregation-widget': 'stats',
  'v2-status-count': 'stats',
  'v2-table-list': 'table',
  'table-list': 'table',
  'v2-tabs-widget': 'container',
  'tabs-widget': 'container',
  'tabs': 'container',
  'v2-tabs': 'container',
  'v2-section-card': 'container',
  'v2-section-paper': 'container',
  'v2-repeat-container': 'container',
  'v2-repeater': 'container',
  'section-card': 'container',
  'section-paper': 'container',
  'accordion-basic': 'container',
};

const FULL_WIDTH_IDS = new Set([
  'table',
  'container',
  'search',
  'accordion',
  'tabs',
  'split-panel',
  // ★ 2026-05-18 canonical data-view 추가
  'grouped-table',
]);

const MAIN_CONTENT_IDS = new Set([
  ...FULL_WIDTH_IDS,
  'stats',
  'form',
  'title',
  // ★ 2026-05-18 canonical data-view 추가
  'chart',
  'card-list',
]);

function extractIdFromUrl(url?: unknown): string {
  if (typeof url !== 'string' || !url) return '';
  const parts = url.split('/').filter(Boolean);
  return parts[parts.length - 1] ?? '';
}

function extractComponents(viewData: unknown): any[] {
  if (!viewData) return [];
  if (Array.isArray(viewData)) return viewData;
  if (typeof viewData === 'object' && viewData !== null) {
    const v = viewData as any;
    if (Array.isArray(v.components)) return v.components;
    if (Array.isArray(v.blocks)) return v.blocks;
  }
  return [];
}

function unifiedComponentId(c: any): string {
  const raw = String(
    c?.componentId ??
      c?.componentType ??
      c?.type ??
      extractIdFromUrl(c?.url) ??
      c?.overrides?.type ??
      '',
  );
  return LEGACY_TO_UNIFIED[raw] ?? raw;
}

function readPos(c: any): {
  left: number;
  top: number;
  width: number;
  height: number;
} {
  const p = c?.position ?? {};
  const s = c?.size ?? {};
  return {
    left: Number(p.left ?? p.x ?? 0) || 0,
    top: Number(p.top ?? p.y ?? 0) || 0,
    width: Number(p.width ?? s.width ?? 200) || 200,
    height: Number(p.height ?? s.height ?? 100) || 100,
  };
}

function readConfig(c: any): Record<string, any> {
  const a = c?.config;
  if (a && typeof a === 'object' && !Array.isArray(a)) return a;
  const b = c?.componentConfig;
  if (b && typeof b === 'object' && !Array.isArray(b)) return b;
  const d = c?.overrides;
  if (d && typeof d === 'object' && !Array.isArray(d)) return d;
  return {};
}

// ─────────────────────────────────────────────────────────────────────────────
// 2. role / policy 추론 (마이그레이션 1회용)
// ─────────────────────────────────────────────────────────────────────────────

interface MainBbox {
  top: number;
  bottom: number;
  left: number;
  right: number;
}

/**
 * 레거시 컴포넌트의 role 을 추론.
 * - button 은 항상 action: 원본 좌표가 main band 내부에 걸쳐있어도
 *   "action bar" 의미로 취급한다. 이렇게 해야 TemplateRenderer 의
 *   content-size 정책이 버튼에 일관되게 적용된다.
 * - input 은 좌표 기반 추정: main band 내부면 companion (필터 인풋 등),
 *   바깥이면 action.
 * - divider 는 companion.
 */
function inferRole(cid: string, pos: {
  top: number;
  height: number;
}, mains: MainBbox[]): BlockRole {
  if (MAIN_CONTENT_IDS.has(cid)) return 'main';
  if (cid === 'divider') return 'companion';
  if (cid === 'button') return 'action';

  if (cid === 'input') {
    const centerY = pos.top + pos.height / 2;
    for (const m of mains) {
      if (centerY >= m.top && centerY <= m.bottom) return 'companion';
    }
    return 'action';
  }

  return 'main';
}

function inferPolicy(cid: string): ResponsivePolicy {
  if (FULL_WIDTH_IDS.has(cid)) return 'scroll';
  // ★ 2026-05-18 chart / card-list 는 reflow (작은 카드/그리드 모드도 자연스러움)
  if (cid === 'stats' || cid === 'form' || cid === 'chart' || cid === 'card-list') return 'reflow';
  if (
    cid === 'button' ||
    cid === 'input' ||
    cid === 'title' ||
    cid === 'divider'
  ) {
    return 'fixed';
  }
  return 'fixed';
}

// ─────────────────────────────────────────────────────────────────────────────
// 3. 뷰 변환
// ─────────────────────────────────────────────────────────────────────────────

interface BandBbox {
  id: string;
  top: number;
  bottom: number;
}

function migrateView(
  raw: unknown,
  canvas: { width: number; height: number },
): ViewV2 {
  const rawBlocks = (extractComponents(raw) as any[]).filter(
    (c) => c && typeof c === 'object',
  );

  // 1차: 정규화 정보 수집
  interface Prelim {
    raw: any;
    i: number;
    cid: string;
    pos: ReturnType<typeof readPos>;
    config: Record<string, any>;
    role: BlockRole;
  }

  const mains: MainBbox[] = [];
  const prelim: Prelim[] = rawBlocks.map((c, i) => {
    const cid = unifiedComponentId(c);
    const pos = readPos(c);
    const config = readConfig(c);
    // role 은 main 수집 후에 확정해야 companion 판정 가능
    return { raw: c, i, cid, pos, config, role: 'main' as BlockRole };
  });

  // main bbox 수집 (role 추론 재료)
  for (const p of prelim) {
    if (MAIN_CONTENT_IDS.has(p.cid)) {
      mains.push({
        top: p.pos.top,
        bottom: p.pos.top + p.pos.height,
        left: p.pos.left,
        right: p.pos.left + p.pos.width,
      });
    }
  }

  // role 확정
  for (const p of prelim) {
    p.role = inferRole(p.cid, p.pos, mains);
  }

  // 2차: main 블록 y-overlap clustering → bandId 생성
  // (normalize 단계의 1회 추론. 런타임에서는 이 bandId 를 그대로 사용)
  const mainPrelims = prelim
    .filter((p) => p.role === 'main')
    .sort((a, b) => a.pos.top - b.pos.top);

  const bandIdOf = new Map<Prelim, string>();
  const bandBboxes: BandBbox[] = [];
  let bandIdx = 0;

  for (const m of mainPrelims) {
    const myTop = m.pos.top;
    const myBot = m.pos.top + m.pos.height;
    // 기존 band 중 세로 overlap 이 충분한 것 찾기 (작은 쪽 h 의 30% 이상)
    let matched: BandBbox | null = null;
    for (const bb of bandBboxes) {
      const overlap =
        Math.min(bb.bottom, myBot) - Math.max(bb.top, myTop);
      const smallerH = Math.min(bb.bottom - bb.top, myBot - myTop);
      if (smallerH > 0 && overlap / smallerH > 0.3) {
        matched = bb;
        break;
      }
    }
    if (!matched) {
      matched = { id: `band-${bandIdx++}`, top: myTop, bottom: myBot };
      bandBboxes.push(matched);
    } else {
      matched.top = Math.min(matched.top, myTop);
      matched.bottom = Math.max(matched.bottom, myBot);
    }
    bandIdOf.set(m, matched.id);
  }

  // 3차: companion/action/overlay 는 가장 가까운 main band 에 합류
  //   - centerY 가 band 내부면 그 band
  //   - 아니면 가장 가까운 (top/bottom 거리 최소) band
  //   - main band 가 전혀 없으면 bandId 비워둠 (런타임 fallback 에 위임)
  for (const p of prelim) {
    if (bandIdOf.has(p)) continue;
    if (bandBboxes.length === 0) continue;
    const centerY = p.pos.top + p.pos.height / 2;
    let target: BandBbox | null = null;
    let minDist = Infinity;
    for (const bb of bandBboxes) {
      if (centerY >= bb.top && centerY <= bb.bottom) {
        target = bb;
        break;
      }
      const dist = Math.min(
        Math.abs(centerY - bb.top),
        Math.abs(centerY - bb.bottom),
      );
      if (dist < minDist) {
        minDist = dist;
        target = bb;
      }
    }
    if (target) bandIdOf.set(p, target.id);
  }

  // 4차: BlockV2 생성
  const blocks: BlockV2[] = prelim.map((p) => {
    const bandId = bandIdOf.get(p);
    return {
      id: String(p.raw.id ?? `blk_${p.i}`),
      componentId: p.cid,
      xPct: canvas.width > 0 ? p.pos.left / canvas.width : 0,
      yPct: canvas.height > 0 ? p.pos.top / canvas.height : 0,
      wPct: canvas.width > 0 ? p.pos.width / canvas.width : 0,
      hPct: canvas.height > 0 ? p.pos.height / canvas.height : 0,
      role: p.role,
      responsivePolicy: inferPolicy(p.cid),
      ...(bandId ? { bandId } : {}),
      config: p.config,
    };
  });

  return { blocks };
}

// ─────────────────────────────────────────────────────────────────────────────
// 4. Public API
// ─────────────────────────────────────────────────────────────────────────────

export function isV2Views(views: unknown): views is TemplateViewsV2 {
  return (
    !!views &&
    typeof views === 'object' &&
    (views as any).version === 2
  );
}

/**
 * 런타임 정규화.
 * 이미 v2 포맷으로 저장된 stale 데이터가 최신 규칙을 반영하지 못해 화면이
 * 틀어지는 문제 방지. 현재는 한 단계로 동작:
 *
 *   [Stage 1] componentId / responsivePolicy 보정
 *     - LEGACY_TO_UNIFIED 로 componentId 를 통합 ID 로 교체
 *     - unified ID 기준으로 inferPolicy 재적용
 *     - role / bandId 는 사용자가 저장한 값을 그대로 보존
 */
function normalizeRoles(v: TemplateViewsV2): TemplateViewsV2 {
  const patchView = (view: ViewV2 | undefined): ViewV2 | undefined => {
    if (!view) return view;

    // Stage 1: componentId / policy 보정. role / bandId 는 그대로 보존.
    const stage1: BlockV2[] = view.blocks.map((b) => {
      const unified = LEGACY_TO_UNIFIED[b.componentId] ?? b.componentId;
      const idChanged = unified !== b.componentId;
      const nextPolicy = inferPolicy(unified);
      const policyChanged = b.responsivePolicy !== nextPolicy;

      if (!idChanged && !policyChanged) return b;

      const { bandId: _drop, ...rest } = b;
      return {
        ...rest,
        componentId: unified,
        responsivePolicy: nextPolicy,
      };
    });
    return { ...view, blocks: stage1 };
  };
  return {
    ...v,
    list: patchView(v.list)!,
    ...(v.create ? { create: patchView(v.create)! } : {}),
    ...(v.edit ? { edit: patchView(v.edit)! } : {}),
  };
}

export function migrateV1toV2(v1Views: unknown): TemplateViewsV2 {
  const root = (v1Views ?? {}) as any;
  const sr = root.screenResolution ?? root.designSize ?? {};
  const defaultCanvas: CanvasV2 = {
    baseWidth: Number(sr.width) || 1920,
    baseHeight: Number(sr.height) || 1080,
    aspectPolicy: 'preserve',
  };

  // 뷰별 해상도 파싱 — 빌더(ScreenDesigner)가 저장한 viewScreenResolutions
  // (JSON 키는 screenResolutions). 없는 뷰는 defaultCanvas 로 폴백.
  const srs = root.screenResolutions ?? root.screen_resolutions;
  const makeCanvas = (v: any): CanvasV2 | undefined => {
    if (!v) return undefined;
    const w = Number(v.width);
    const h = Number(v.height);
    if (!isFinite(w) || !isFinite(h) || w <= 0 || h <= 0) return undefined;
    return { baseWidth: w, baseHeight: h, aspectPolicy: 'preserve' };
  };
  const listCanvas = makeCanvas(srs?.list) ?? defaultCanvas;
  const createCanvas = makeCanvas(srs?.create) ?? defaultCanvas;
  const editCanvas = makeCanvas(srs?.edit) ?? defaultCanvas;

  // 각 뷰의 컴포넌트 좌표는 자기 canvas 기준으로 정규화.
  // (빌더가 activeView 별 screenResolution 으로 px 를 저장하므로, 그 기준으로 나눠야 정확.)
  const list = migrateView(root.list, {
    width: listCanvas.baseWidth,
    height: listCanvas.baseHeight,
  });
  const create = root.create
    ? migrateView(root.create, {
        width: createCanvas.baseWidth,
        height: createCanvas.baseHeight,
      })
    : undefined;
  const edit = root.edit
    ? migrateView(root.edit, {
        width: editCanvas.baseWidth,
        height: editCanvas.baseHeight,
      })
    : undefined;

  const viewCanvases: TemplateViewsV2['viewCanvases'] = {
    ...(makeCanvas(srs?.list) ? { list: listCanvas } : {}),
    ...(makeCanvas(srs?.create) ? { create: createCanvas } : {}),
    ...(makeCanvas(srs?.edit) ? { edit: editCanvas } : {}),
  };

  return {
    version: 2,
    canvas: defaultCanvas,
    list,
    ...(create ? { create } : {}),
    ...(edit ? { edit } : {}),
    ...(viewCanvases && Object.keys(viewCanvases).length > 0
      ? { viewCanvases }
      : {}),
  };
}

/**
 * 로드 시점 정규화 헬퍼 — v1 이든 v2 이든 받아서 항상 v2 로 반환.
 * 기존 v2 저장본이라도 현재 role 규칙(button→action 등)에 맞춰 재정규화한다.
 */
export function ensureV2Views(views: unknown): TemplateViewsV2 {
  const v2 = isV2Views(views) ? views : migrateV1toV2(views);
  return normalizeRoles(v2);
}

/**
 * template 객체의 views 필드를 v2 로 정규화한 사본을 반환.
 * template 자체는 변형하지 않는다.
 */
export function ensureTemplateV2(template: any): any {
  if (!template) return template;
  const views = template.views ?? template.VIEWS;
  if (!views) return template;
  const v2 = ensureV2Views(views);
  return { ...template, views: v2 };
}
