import "@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { normalizeInstagramHandle } from "../_shared/normalizeHandles.ts";
import { PERPLEXITY_MODEL } from "../_shared/config.ts";
import { isMissingValue, validateReportQuality } from "../_shared/dataQuality.ts";
import { extractFoundingYear } from "../_shared/foundingYearExtractor.ts";
import { fetchWithRetry } from "../_shared/retry.ts";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type",
};

interface ReportRequest {
  // V2: reportId-based (Phase 3 — uses data already in DB)
  reportId?: string;
  // V3: clinic + run IDs for new schema
  clinicId?: string;
  runId?: string;
  // V1 compat: url-based (legacy single-call flow)
  url?: string;
  clinicName?: string;
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

  try {
    const body = (await req.json()) as ReportRequest;

    const PERPLEXITY_API_KEY = Deno.env.get("PERPLEXITY_API_KEY");
    if (!PERPLEXITY_API_KEY) throw new Error("PERPLEXITY_API_KEY not configured");

    const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
    const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
    const supabase = createClient(supabaseUrl, supabaseKey);

    // ─── V2 Pipeline: reportId provided (Phase 1 & 2 already ran) ───
    if (body.reportId) {
      const { data: row, error: fetchErr } = await supabase
        .from("marketing_reports")
        .select("*")
        .eq("id", body.reportId)
        .single();
      if (fetchErr || !row) throw new Error(`Report not found: ${fetchErr?.message}`);

      await supabase.from("marketing_reports").update({ status: "generating" }).eq("id", body.reportId);

      const channelData = row.channel_data || {};
      const analysisData = row.analysis_data || {};
      const scrapeData = row.scrape_data || {};
      const clinic = scrapeData.clinic || {};
      const verified = row.verified_channels || {};

      // Build real data summary for AI prompt
      const channelSummary = buildChannelSummary(channelData, verified);
      const marketSummary = JSON.stringify(analysisData.analysis || {}, null, 2).slice(0, 8000);

      const reportPrompt = `
당신은 프리미엄 의료 마케팅 전문 분석가입니다. 아래 **실제 수집된 데이터**를 기반으로 종합 마케팅 리포트를 생성해주세요.

⚠️ 중요: 아래 데이터에 없는 수치는 절대 추측하지 마세요. 데이터가 없으면 "데이터 없음"으로 표시하세요.

## 병원 기본 정보
- 병원명: ${clinic.clinicName || row.clinic_name}
- 주소: ${clinic.address || ""}
- 전화: ${clinic.phone || ""}
- 시술: ${(clinic.services || []).join(", ")}
- 의료진: ${JSON.stringify(clinic.doctors || []).slice(0, 500)}
- 슬로건: ${clinic.slogan || ""}

## 실제 채널 데이터 (수집 완료)
${channelSummary}

## 시장 분석 데이터
${marketSummary}

## 웹사이트 브랜딩
${JSON.stringify(scrapeData.branding || {}, null, 2).slice(0, 1000)}

## 리포트 형식 (반드시 아래 JSON 구조로 응답)
{
  "clinicInfo": {
    "name": "병원명 (한국어)",
    "nameEn": "영문 병원명",
    "established": "개원년도",
    "address": "주소",
    "phone": "전화번호",
    "services": ["시술1", "시술2"],
    "doctors": [{"name": "의사명", "specialty": "전문분야"}],
    "leadDoctor": {"name": "대표원장 이름", "specialty": "전문분야/학력", "rating": 0, "reviewCount": 0},
    "staffCount": 0
  },
  "executiveSummary": "경영진 요약 (3-5문장)",
  "overallScore": 0-100,
  "channelAnalysis": {
    "naverBlog": { "score": 0-100, "status": "active|inactive|not_found", "posts": 0, "recommendation": "추천사항", "diagnosis": [{"issue": "문제", "severity": "critical|warning|good", "recommendation": "개선안"}] },
    "instagram": { "score": 0-100, "status": "active|inactive|not_found", "followers": 실제수치, "posts": 실제수치, "recommendation": "추천사항", "diagnosis": [{"issue": "문제", "severity": "critical|warning|good", "recommendation": "개선안"}] },
    "youtube": { "score": 0-100, "status": "active|inactive|not_found", "subscribers": 실제수치, "recommendation": "추천사항", "diagnosis": [{"issue": "문제", "severity": "critical|warning|good", "recommendation": "개선안"}] },
    "naverPlace": { "score": 0-100, "rating": 실제수치, "reviews": 실제수치, "recommendation": "추천사항" },
    "gangnamUnni": { "score": 0-100, "rating": 실제수치, "ratingScale": 10, "reviews": 실제수치, "status": "active|not_found", "recommendation": "추천사항" },
    "website": { "score": 0-100, "issues": [], "recommendation": "추천사항", "trackingPixels": [{"name": "이름", "installed": true}], "snsLinksOnSite": true, "additionalDomains": [], "mainCTA": "주요 CTA" }
  },
  "newChannelProposals": [{ "channel": "채널명", "priority": "P0|P1|P2", "rationale": "근거" }],
  "competitors": [{ "name": "경쟁병원", "strengths": [], "weaknesses": [], "marketingChannels": [] }],
  "keywords": { "primary": [{"keyword": "키워드", "monthlySearches": 0, "competition": "high|medium|low"}], "longTail": [{"keyword": "키워드"}] },
  "targetAudience": { "primary": { "ageRange": "", "gender": "", "interests": [], "channels": [] } },
  "brandIdentity": [{ "area": "영역", "asIs": "현재", "toBe": "개선" }],
  "kpiTargets": [{ "metric": "지표명 (실제 수집된 수치 기반으로 현실적 목표 설정)", "current": "현재 실제 수치", "target3Month": "3개월 목표", "target12Month": "12개월 목표" }],
  "recommendations": [{ "priority": "high|medium|low", "category": "카테고리", "title": "제목", "description": "설명", "expectedImpact": "효과" }],
  "marketTrends": ["트렌드1"]
}
`;

      // Use fetchWithRetry to handle transient Perplexity failures (429, 502, 503).
      // 2 retries with 5s/15s backoff — total budget ~3min before giving up.
      const aiRes = await fetchWithRetry("https://api.perplexity.ai/chat/completions", {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
        body: JSON.stringify({
          model: PERPLEXITY_MODEL,
          messages: [
            { role: "system", content: "You are a Korean medical marketing analyst. Respond ONLY with valid JSON, no markdown code blocks. Use Korean for text fields. 강남언니 rating is 10-point scale. Use ONLY the provided real data — never invent metrics." },
            { role: "user", content: reportPrompt },
          ],
          temperature: 0.3,
        }),
      }, { maxRetries: 2, backoffMs: [5000, 15000], timeoutMs: 90000, label: "perplexity-report" });

      const aiData = await aiRes.json();
      let reportText = aiData.choices?.[0]?.message?.content || "";
      const jsonMatch = reportText.match(/```(?:json)?\n?([\s\S]*?)```/);
      if (jsonMatch) reportText = jsonMatch[1];

      let report;
      try { report = JSON.parse(reportText); } catch { report = { raw: reportText, parseError: true }; }

      // ─── Post-processing: Inject Vision Analysis data directly ───
      // Perplexity may ignore Vision data in prompt, so we force-inject critical fields
      const vision = channelData.visionAnalysis as Record<string, unknown> | undefined;
      // Use Harness 3's isMissingValue() — covers all known LLM "no data" variants
      if (vision) {
        // Force-inject foundingYear if Vision found it but Perplexity didn't
        if (vision.foundingYear && isMissingValue(report.clinicInfo?.established)) {
          report.clinicInfo = report.clinicInfo || {};
          report.clinicInfo.established = String(vision.foundingYear);
          console.log(`[report] Injected foundingYear from Vision: ${vision.foundingYear}`);
        }
        if (vision.operationYears && isMissingValue(report.clinicInfo?.established)) {
          const year = new Date().getFullYear() - Number(vision.operationYears);
          report.clinicInfo = report.clinicInfo || {};
          report.clinicInfo.established = String(year);
          console.log(`[report] Calculated foundingYear from operationYears: ${vision.operationYears} → ${year}`);
        }

        // Force-inject doctors from Vision if report has none
        const visionDoctors = vision.doctors as { name: string; specialty: string; position?: string }[] | undefined;
        if (visionDoctors?.length && (!report.clinicInfo?.doctors?.length)) {
          report.clinicInfo = report.clinicInfo || {};
          report.clinicInfo.doctors = visionDoctors;
          console.log(`[report] Injected ${visionDoctors.length} doctors from Vision`);
        }

        // Force-inject 강남언니 data from Vision if channelAnalysis has none/zero
        const guVision = vision.gangnamUnniStats as Record<string, unknown> | undefined;
        if (guVision) {
          report.channelAnalysis = report.channelAnalysis || {};
          const existing = report.channelAnalysis.gangnamUnni;
          if (!existing || existing.score === 0 || !existing.rating) {
            const rating = parseFloat(String(guVision.rating || 0));
            const reviewStr = String(guVision.reviews || "0").replace(/[^0-9]/g, "");
            const reviews = parseInt(reviewStr) || 0;
            // Score: rating 10점 만점 기준 + 리뷰 수 보정
            const ratingScore = Math.min(rating * 10, 100);
            const reviewBonus = Math.min(reviews / 1000, 10); // 1000리뷰당 +1, max +10
            const score = Math.round(Math.min(ratingScore + reviewBonus, 100));
            report.channelAnalysis.gangnamUnni = {
              ...(existing || {}),
              score,
              rating,
              ratingScale: 10,
              reviews,
              doctors: Number(guVision.doctors) || 0,
              status: "active",
              recommendation: `평점 ${guVision.rating}/10, 리뷰 ${guVision.reviews}건 — 강남언니에서 활발한 활동`,
            };
            console.log(`[report] Injected gangnamUnni from Vision: score=${score}, rating=${rating}, reviews=${reviews}`);
          }
        }

        // Store Vision analysis separately for frontend
        report.visionAnalysis = vision;
      }

      // ─── Harness 4: GroundTruth channel injection ───
      // Perplexity occasionally drops channel metrics even though they exist in channelData.
      // For each channel we force-inject the numeric fields from the actually-collected data.
      // Same pattern as the Vision injection above (foundingYear / gangnamUnni), extended
      // to instagram / youtube / facebook / naverBlog / naverPlace.
      report.channelAnalysis = report.channelAnalysis || {};
      const injectedChannels: string[] = [];

      // Instagram — prefer instagramAccounts[0], fall back to channelData.instagram
      const igAccountsList = (channelData.instagramAccounts as Record<string, unknown>[] | undefined) || [];
      const igPrimary = (igAccountsList[0] || channelData.instagram) as Record<string, unknown> | undefined;
      const igPostsSummary = channelData.instagramPosts as Record<string, unknown> | undefined;
      if (igPrimary) {
        const target = (report.channelAnalysis.instagram ||= {} as Record<string, unknown>);
        if (isMissingValue(target.followers) && igPrimary.followers) {
          target.followers = igPrimary.followers;
        }
        // Prefer profile-level post count; fall back to scraped posts summary
        if (isMissingValue(target.posts)) {
          if (igPrimary.posts) target.posts = igPrimary.posts;
          else if (igPostsSummary?.totalPosts) target.posts = igPostsSummary.totalPosts;
        }
        if (isMissingValue(target.status)) target.status = "active";
        injectedChannels.push(`instagram(f=${target.followers},p=${target.posts})`);
      }

      // YouTube
      const ytGT = channelData.youtube as Record<string, unknown> | undefined;
      if (ytGT) {
        const target = (report.channelAnalysis.youtube ||= {} as Record<string, unknown>);
        if (isMissingValue(target.subscribers) && ytGT.subscribers) {
          target.subscribers = ytGT.subscribers;
        }
        if (isMissingValue((target as Record<string, unknown>).videos) && ytGT.totalVideos) {
          (target as Record<string, unknown>).videos = ytGT.totalVideos;
        }
        if (isMissingValue(target.status)) target.status = "active";
        injectedChannels.push(`youtube(s=${target.subscribers})`);
      }

      // Facebook
      const fbGT = channelData.facebook as Record<string, unknown> | undefined;
      if (fbGT) {
        const target = (report.channelAnalysis.facebook ||= {} as Record<string, unknown>);
        if (isMissingValue(target.followers) && fbGT.followers) {
          target.followers = fbGT.followers;
        }
        if (isMissingValue((target as Record<string, unknown>).likes) && fbGT.likes) {
          (target as Record<string, unknown>).likes = fbGT.likes;
        }
        if (isMissingValue(target.status)) target.status = "active";
        injectedChannels.push(`facebook(f=${target.followers})`);
      }

      // Naver Blog — RSS provides totalResults + posts
      const nbGT = channelData.naverBlog as Record<string, unknown> | undefined;
      if (nbGT && !nbGT.skipped) {
        const target = (report.channelAnalysis.naverBlog ||= {} as Record<string, unknown>);
        if (isMissingValue(target.posts) && nbGT.totalResults) {
          target.posts = nbGT.totalResults;
        }
        if (isMissingValue(target.status)) {
          target.status = (nbGT.totalResults as number) > 0 ? "active" : "inactive";
        }
        injectedChannels.push(`naverBlog(p=${target.posts})`);
      }

      // Naver Place — Naver Local Search doesn't include rating/reviews, but stores the place metadata.
      // If the place was found we at least set status=active; rating/reviews often come via Vision.
      const npGT = channelData.naverPlace as Record<string, unknown> | undefined;
      if (npGT && !npGT.skipped && npGT.name) {
        const target = (report.channelAnalysis.naverPlace ||= {} as Record<string, unknown>);
        if (isMissingValue(target.status)) target.status = "active";
        if (isMissingValue((target as Record<string, unknown>).name) && npGT.name) {
          (target as Record<string, unknown>).name = npGT.name;
        }
        // Rating/reviews may be injected by Vision block above; don't overwrite
        injectedChannels.push(`naverPlace(${npGT.name})`);
      }

      // GangnamUnni — Vision block handled the rating/reviews path already.
      // Here we only fill status/rating when they come directly from the scraper (D).
      const guGT = channelData.gangnamUnni as Record<string, unknown> | undefined;
      if (guGT && !guGT.skipped) {
        const target = (report.channelAnalysis.gangnamUnni ||= {} as Record<string, unknown>);
        if (isMissingValue(target.rating) && guGT.rating) {
          target.rating = guGT.rating;
          (target as Record<string, unknown>).ratingScale = 10;
        }
        if (isMissingValue(target.reviews) && (guGT.totalReviews || guGT.reviews)) {
          target.reviews = guGT.totalReviews || guGT.reviews;
        }
        if (isMissingValue(target.status)) target.status = "active";
        injectedChannels.push(`gangnamUnni(r=${target.rating})`);
      }

      if (injectedChannels.length > 0) {
        console.log(`[report] GroundTruth injected: ${injectedChannels.join(", ")}`);
      }

      // ─── Harness 4b: Diagnosis fallback ───
      // ProblemDiagnosis.tsx returns null when `problemDiagnosis` is empty, hiding the whole
      // section. Make sure each weak channel contributes at least one diagnosis item so the
      // frontend renders. Only runs when AI didn't fill diagnosis itself.
      const DEFAULT_DIAGNOSIS: Record<string, { issue: string; recommendation: string }> = {
        instagram: {
          issue: "Instagram 업로드 빈도가 부족하거나 게시물 참여율이 낮습니다",
          recommendation: "주 3회 이상 주제별 콘텐츠 발행 + 릴스 비중 40% 이상 확보",
        },
        youtube: {
          issue: "YouTube 채널 활동이 저조하여 브랜드 신뢰도 확보가 어렵습니다",
          recommendation: "시술 Before/After Shorts 주 2회, 롱폼 월 2회 업로드 루틴 수립",
        },
        facebook: {
          issue: "Facebook 페이지 활동이 낮아 유입 기여가 제한적입니다",
          recommendation: "광고 랜딩 채널로 재정의하고 월 4회 프로모션 포스트 자동화",
        },
        naverBlog: {
          issue: "네이버 블로그 최신 포스팅 양이 부족하여 로컬 SEO 기회를 놓치고 있습니다",
          recommendation: "주요 시술 키워드별 주 2건, 월 8건 이상 SEO 최적화 포스팅",
        },
        naverPlace: {
          issue: "네이버 플레이스 리뷰/콘텐츠 관리가 체계화되어 있지 않습니다",
          recommendation: "방문 리뷰 유도 프로세스 구축 + 키워드·사진 리뷰 월 20건 확보",
        },
        gangnamUnni: {
          issue: "강남언니 프로필 정보와 답변률이 미흡합니다",
          recommendation: "의사별 프로필 완성도 100% + 문의 24시간 내 응답 체계",
        },
      };
      const weakStatuses = new Set(["inactive", "weak", "not_found", undefined]);
      for (const ch of Object.keys(DEFAULT_DIAGNOSIS)) {
        const node = (report.channelAnalysis as Record<string, unknown>)[ch] as
          | Record<string, unknown>
          | undefined;
        if (!node) continue;
        const existing = node.diagnosis as unknown[] | undefined;
        if (Array.isArray(existing) && existing.length > 0) continue;
        const score = typeof node.score === "number" ? node.score : null;
        const status = node.status as string | undefined;
        const needsFallback = (score !== null && score < 60) || weakStatuses.has(status);
        if (!needsFallback) continue;
        const def = DEFAULT_DIAGNOSIS[ch];
        node.diagnosis = [{
          issue: def.issue,
          severity: score !== null && score < 40 ? "critical" : "warning",
          recommendation: def.recommendation,
        }];
        console.log(`[report] Diagnosis fallback injected for ${ch} (score=${score}, status=${status})`);
      }

      // Embed channel enrichment data for frontend mergeEnrichment()
      report.channelEnrichment = channelData;
      report.enrichedAt = new Date().toISOString();

      // Embed screenshots as evidence for frontend EvidenceGallery
      const screenshots = (channelData.screenshots || []) as Record<string, unknown>[];
      if (screenshots.length > 0) {
        report.screenshots = screenshots.map((ss: Record<string, unknown>) => ({
          id: ss.id,
          url: ss.url, // data URI or Storage URL
          channel: ss.channel,
          capturedAt: ss.capturedAt,
          caption: ss.caption,
          sourceUrl: ss.sourceUrl,
        }));

        // 채널별 스크린샷 그룹핑 (프론트엔드에서 각 채널 섹션에 증거 이미지 표시)
        report.channelScreenshots = screenshots.reduce((acc: Record<string, unknown[]>, ss: Record<string, unknown>) => {
          const channel = ss.channel as string || 'etc';
          if (!acc[channel]) acc[channel] = [];
          acc[channel].push({ url: ss.url, caption: ss.caption, id: ss.id, sourceUrl: ss.sourceUrl });
          return acc;
        }, {} as Record<string, unknown[]>);
      }

      // Instagram Posts/Reels 분석 (engagement rate, 콘텐츠 성과)
      const igProfile = channelData.instagram as Record<string, unknown> | undefined;
      const igPosts = channelData.instagramPosts as Record<string, unknown> | undefined;
      const igReels = channelData.instagramReels as Record<string, unknown> | undefined;
      if (igProfile && (igPosts || igReels)) {
        const followers = (igProfile.followers as number) || 0;
        const avgLikes = (igPosts?.avgLikes as number) || 0;
        const avgComments = (igPosts?.avgComments as number) || 0;

        // 해시태그 빈도 분석
        const hashtagCounts: Record<string, number> = {};
        const allPosts = [...((igPosts?.posts as Record<string, unknown>[]) || []), ...((igReels?.reels as Record<string, unknown>[]) || [])];
        for (const post of allPosts) {
          for (const tag of (post.hashtags as string[]) || []) {
            const lower = tag.toLowerCase();
            hashtagCounts[lower] = (hashtagCounts[lower] || 0) + 1;
          }
        }
        const topHashtags = Object.entries(hashtagCounts)
          .sort((a, b) => b[1] - a[1])
          .slice(0, 10)
          .map(([tag, count]) => ({ tag, count }));

        // 게시 빈도 (최근 포스트 간 평균 간격)
        const postTimestamps = ((igPosts?.posts as Record<string, unknown>[]) || [])
          .map(p => new Date(p.timestamp as string).getTime())
          .filter(t => !isNaN(t))
          .sort((a, b) => b - a);
        let postFrequency = 'N/A';
        if (postTimestamps.length >= 2) {
          const spanDays = (postTimestamps[0] - postTimestamps[postTimestamps.length - 1]) / (1000 * 60 * 60 * 24);
          const postsPerWeek = spanDays > 0 ? (postTimestamps.length / spanDays * 7).toFixed(1) : 'N/A';
          postFrequency = `${postsPerWeek}회/주`;
        }

        // 최고 성과 포스트
        const bestPost = ((igPosts?.posts as Record<string, unknown>[]) || [])
          .sort((a, b) => ((b.likesCount as number) || 0) - ((a.likesCount as number) || 0))[0] || null;

        report.instagramAnalysis = {
          engagementRate: followers > 0 ? ((avgLikes + avgComments) / followers * 100).toFixed(2) + '%' : 'N/A',
          avgLikes,
          avgComments,
          topHashtags,
          postFrequency,
          bestPerformingPost: bestPost ? {
            url: bestPost.url,
            likes: bestPost.likesCount,
            comments: bestPost.commentsCount,
            caption: ((bestPost.caption as string) || '').slice(0, 200),
          } : null,
          reelsPerformance: igReels ? {
            totalReels: igReels.totalReels,
            avgViews: igReels.avgViews,
            avgPlays: igReels.avgPlays,
            viewToFollowerRatio: followers > 0
              ? (((igReels.avgViews as number) || 0) / followers * 100).toFixed(1) + '%'
              : 'N/A',
          } : null,
          totalPostsAnalyzed: (igPosts?.totalPosts as number) || 0,
          totalReelsAnalyzed: (igReels?.totalReels as number) || 0,
        };
      }

      // Embed verified handles
      const igHandles = (verified.instagram || []).filter((v: { verified: boolean }) => v.verified).map((v: { handle: string }) => v.handle);
      report.socialHandles = {
        instagram: igHandles.length > 0 ? igHandles : null,
        youtube: verified.youtube?.verified ? verified.youtube.handle : null,
        facebook: verified.facebook?.verified ? verified.facebook.handle : null,
      };

      // ─── Harness 2 (tertiary): Last-resort founding year from all channel text ───
      if (isMissingValue(report.clinicInfo?.established)) {
        const allTexts = [
          channelData.scrapeMarkdown,
          channelData.naverBlog?.description,
          channelData.gangnamUnni?.description,
          JSON.stringify(channelData.visionPerPage || {}),
        ].filter(Boolean).join(" ");
        const lastResortYear = extractFoundingYear(allTexts);
        if (lastResortYear) {
          report.clinicInfo = report.clinicInfo || {};
          report.clinicInfo.established = String(lastResortYear);
          console.log(`[harness] Founding year from tertiary text scan: ${lastResortYear}`);
        }
      }

      // ─── Harness 3: Report quality validation ───
      const qualityReport = validateReportQuality(report);
      report.dataQualityScore = qualityReport.score;
      report.dataQualityDetails = {
        missingCritical: qualityReport.missingCritical,
        missingImportant: qualityReport.missingImportant,
        warnings: qualityReport.warnings,
      };
      if (qualityReport.score < 60) {
        console.warn(`[harness] Low report quality (${qualityReport.score}/100):`, qualityReport.warnings);
      }

      // Persist quality report into analysis_data.qualityReport so the frontend (and
      // future debugging queries) can inspect why specific sections rendered empty.
      // Downgrade status to 'partial' when too many important/critical fields are missing.
      const totalMissing = qualityReport.missingCritical.length + qualityReport.missingImportant.length;
      const reportStatus = (qualityReport.missingCritical.length > 0 || totalMissing >= 5)
        ? "partial"
        : "complete";
      const enrichedAnalysisData = {
        ...analysisData,
        qualityReport: {
          score: qualityReport.score,
          missingCritical: qualityReport.missingCritical,
          missingImportant: qualityReport.missingImportant,
          missingOptional: qualityReport.missingOptional,
          warnings: qualityReport.warnings,
          injectedChannels,
          generatedAt: new Date().toISOString(),
        },
      };

      // Legacy: marketing_reports
      await supabase.from("marketing_reports").update({
        report,
        analysis_data: enrichedAnalysisData,
        status: reportStatus,
        data_quality_score: qualityReport.score,
        pipeline_completed_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      }).eq("id", body.reportId);

      // V3: analysis_runs + clinics
      const v3RunId = body.runId || null;
      const v3ClinicId = body.clinicId || null;
      if (v3RunId) {
        try {
          await supabase.from("analysis_runs").update({
            report,
            status: "complete",
            pipeline_completed_at: new Date().toISOString(),
          }).eq("id", v3RunId);
        } catch (e) {
          const errMsg = e instanceof Error ? e.message : String(e);
          console.error("V3 run update error:", errMsg);
          try {
            await supabase.from("analysis_runs").update({
              error_message: `V3 report update failed: ${errMsg}`,
              status: "report_error",
            }).eq("id", v3RunId);
          } catch { /* ignore secondary failure */ }
        }
      }
      if (v3ClinicId) {
        try {
          await supabase.from("clinics").update({
            last_analyzed_at: new Date().toISOString(),
            established_year: report.clinicInfo?.established ? parseInt(report.clinicInfo.established) || null : null,
            updated_at: new Date().toISOString(),
          }).eq("id", v3ClinicId);
        } catch (e) { console.error("V3 clinic update error:", e); }
      }

      // ─── Storage: save report.json to clinics/{domain}/{reportId}/ ───
      try {
        const domain = new URL(row.url || "").hostname.replace('www.', '');
        const jsonBytes = new TextEncoder().encode(JSON.stringify(report, null, 2));
        await supabase.storage
          .from('clinic-data')
          .upload(`clinics/${domain}/${body.reportId}/report.json`, jsonBytes, {
            contentType: 'application/json',
            upsert: true,
          });
        console.log(`[storage] report.json → clinics/${domain}/${body.reportId}/`);
      } catch (e) {
        console.warn('[storage] report.json upload failed:', e instanceof Error ? e.message : e);
      }

      return new Response(
        JSON.stringify({
          success: true,
          reportId: body.reportId,
          report,
          metadata: {
            url: row.url,
            clinicName: row.clinic_name,
            generatedAt: new Date().toISOString(),
            dataSources: { scraping: true, marketAnalysis: true, aiGeneration: !report.parseError },
            socialHandles: report.socialHandles,
            address: clinic.address || "",
            services: clinic.services || [],
            // Registry metadata — available when discovered via clinic_registry DB
            source: scrapeData.source || "scrape",
            registryData: scrapeData.registryData || null,
          },
        }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" } },
      );
    }

    // ─── V1 Legacy: url-based single-call flow (backwards compat) ───
    const { url, clinicName } = body;
    if (!url) {
      return new Response(JSON.stringify({ error: "URL or reportId is required" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } });
    }

    // Call scrape-website
    const scrapeRes = await fetch(`${supabaseUrl}/functions/v1/scrape-website`, {
      method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${supabaseKey}` },
      body: JSON.stringify({ url, clinicName }),
    });
    const scrapeResult = await scrapeRes.json();
    if (!scrapeResult.success) throw new Error(`Scraping failed: ${scrapeResult.error}`);

    const clinic = scrapeResult.data.clinic;
    const resolvedName = clinicName || clinic.clinicName || url;
    const services = clinic.services || [];
    const address = clinic.address || "";

    // Call analyze-market
    const analyzeRes = await fetch(`${supabaseUrl}/functions/v1/analyze-market`, {
      method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${supabaseKey}` },
      body: JSON.stringify({ clinicName: resolvedName, services, address, scrapeData: scrapeResult.data }),
    });
    const analyzeResult = await analyzeRes.json();

    // Generate report with Perplexity (legacy prompt)
    const reportPrompt = `당신은 프리미엄 의료 마케팅 전문 분석가입니다. 아래 데이터를 기반으로 종합 마케팅 인텔리전스 리포트를 생성해주세요.

## 수집된 데이터
### 병원 정보
${JSON.stringify(scrapeResult.data, null, 2).slice(0, 6000)}

### 시장 분석
${JSON.stringify(analyzeResult.data?.analysis || {}, null, 2).slice(0, 4000)}

## 리포트 형식 (반드시 아래 JSON 구조로 응답)
{
  "clinicInfo": { "name": "병원명", "nameEn": "영문명", "established": "개원년도", "address": "주소", "phone": "전화", "services": [], "doctors": [], "socialMedia": { "instagramAccounts": [], "youtube": "", "facebook": "", "naverBlog": "" } },
  "executiveSummary": "요약", "overallScore": 0-100,
  "channelAnalysis": { "naverBlog": { "score": 0-100, "status": "active|inactive", "recommendation": "" }, "instagram": { "score": 0-100, "followers": 0, "recommendation": "" }, "youtube": { "score": 0-100, "subscribers": 0, "recommendation": "" }, "naverPlace": { "score": 0-100, "rating": 0, "reviews": 0 }, "gangnamUnni": { "score": 0-100, "rating": 0, "ratingScale": 10, "reviews": 0, "status": "active|not_found" }, "website": { "score": 0-100, "issues": [], "trackingPixels": [], "snsLinksOnSite": false, "mainCTA": "" } },
  "brandIdentity": [{ "area": "", "asIs": "", "toBe": "" }],
  "kpiTargets": [{ "metric": "", "current": "", "target3Month": "", "target12Month": "" }],
  "recommendations": [{ "priority": "high|medium|low", "category": "", "title": "", "description": "", "expectedImpact": "" }],
  "newChannelProposals": [{ "channel": "", "priority": "P0|P1|P2", "rationale": "" }],
  "marketTrends": []
}`;

    const aiRes = await fetch("https://api.perplexity.ai/chat/completions", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${PERPLEXITY_API_KEY}` },
      body: JSON.stringify({
        model: PERPLEXITY_MODEL,
        messages: [
          { role: "system", content: "You are a Korean medical marketing analyst. Respond ONLY with valid JSON, no markdown code blocks. Korean for text fields. 강남언니 rating uses 10-point scale." },
          { role: "user", content: reportPrompt },
        ],
        temperature: 0.3,
      }),
    });

    const aiData = await aiRes.json();
    let reportText = aiData.choices?.[0]?.message?.content || "";
    const jsonMatch = reportText.match(/```(?:json)?\n?([\s\S]*?)```/);
    if (jsonMatch) reportText = jsonMatch[1];

    let report;
    try { report = JSON.parse(reportText); } catch { report = { raw: reportText, parseError: true }; }

    // Merge social handles
    const scrapeSocial = clinic.socialMedia || {};
    const aiSocial = report?.clinicInfo?.socialMedia || {};
    const aiIgAccounts: string[] = Array.isArray(aiSocial.instagramAccounts) ? aiSocial.instagramAccounts : aiSocial.instagram ? [aiSocial.instagram] : [];
    const scrapeIg = scrapeSocial.instagram ? [scrapeSocial.instagram] : [];
    const igHandles = [...new Set([...aiIgAccounts, ...scrapeIg].map((h: string) => normalizeInstagramHandle(h)).filter((h): h is string => h !== null))];

    const pickNonEmpty = (...vals: (string | null | undefined)[]): string | null => vals.find(v => v && v.trim().length > 0) || null;
    const normalizedHandles = {
      instagram: igHandles.length > 0 ? igHandles : null,
      youtube: pickNonEmpty(aiSocial.youtube, scrapeSocial.youtube),
      facebook: pickNonEmpty(aiSocial.facebook, scrapeSocial.facebook),
      blog: pickNonEmpty(aiSocial.naverBlog, scrapeSocial.blog),
    };
    report.socialHandles = normalizedHandles;

    // Save
    const { data: saved, error: saveError } = await supabase.from("marketing_reports").insert({
      url, clinic_name: resolvedName, report, scrape_data: scrapeResult.data, analysis_data: analyzeResult.data, status: "complete",
    }).select("id").single();

    if (saveError) console.error("DB save error:", saveError);

    return new Response(
      JSON.stringify({
        success: true, reportId: saved?.id || null, report,
        metadata: { url, clinicName: resolvedName, generatedAt: new Date().toISOString(), dataSources: { scraping: scrapeResult.success, marketAnalysis: analyzeResult.success, aiGeneration: !report.parseError }, socialHandles: normalizedHandles, saveError: saveError?.message || null, address, services },
      }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" } },
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ success: false, error: error.message }),
      { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
    );
  }
});

// ─── Helper: Build channel summary from collected data ───

function buildChannelSummary(channelData: Record<string, unknown>, verified: Record<string, unknown>): string {
  const parts: string[] = [];

  // Instagram
  const igAccounts = channelData.instagramAccounts as Record<string, unknown>[] | undefined;
  if (igAccounts?.length) {
    for (const ig of igAccounts) {
      parts.push(`### Instagram @${ig.username}`);
      parts.push(`- 팔로워: ${(ig.followers as number || 0).toLocaleString()}명, 게시물: ${ig.posts}개`);
      parts.push(`- 비즈니스 계정: ${ig.isBusinessAccount ? 'O' : 'X'}`);
      parts.push(`- Bio: ${(ig.bio as string || '').slice(0, 200)}`);
    }
  } else {
    parts.push("### Instagram: 데이터 없음");
  }

  // YouTube
  const yt = channelData.youtube as Record<string, unknown> | undefined;
  if (yt) {
    parts.push(`### YouTube ${yt.handle || yt.channelName}`);
    parts.push(`- 구독자: ${(yt.subscribers as number || 0).toLocaleString()}명, 영상: ${yt.totalVideos}개, 총 조회수: ${(yt.totalViews as number || 0).toLocaleString()}`);
    parts.push(`- 채널 설명: ${(yt.description as string || '').slice(0, 300)}`);
    const videos = yt.videos as Record<string, unknown>[] | undefined;
    if (videos?.length) {
      parts.push(`- 인기 영상 TOP ${videos.length}:`);
      for (const v of videos.slice(0, 5)) {
        parts.push(`  - "${v.title}" (조회수: ${(v.views as number || 0).toLocaleString()}, 좋아요: ${v.likes})`);
      }
    }
  } else {
    parts.push("### YouTube: 데이터 없음");
  }

  // Facebook
  const fb = channelData.facebook as Record<string, unknown> | undefined;
  if (fb) {
    parts.push(`### Facebook ${fb.pageName}`);
    parts.push(`- 팔로워: ${(fb.followers as number || 0).toLocaleString()}, 좋아요: ${fb.likes}`);
    parts.push(`- 소개: ${(fb.intro as string || '').slice(0, 200)}`);
  }

  // 강남언니
  const gu = channelData.gangnamUnni as Record<string, unknown> | undefined;
  if (gu) {
    parts.push(`### 강남언니 ${gu.name}`);
    parts.push(`- 평점: ${gu.rating}/10, 리뷰: ${(gu.totalReviews as number || 0).toLocaleString()}건`);
    const doctors = gu.doctors as Record<string, unknown>[] | undefined;
    if (doctors?.length) {
      parts.push(`- 등록 의사: ${doctors.map(d => `${d.name}(${d.specialty})`).join(', ')}`);
    }
  }

  // Google Maps
  const gm = channelData.googleMaps as Record<string, unknown> | undefined;
  if (gm) {
    parts.push(`### Google Maps ${gm.name}`);
    parts.push(`- 평점: ${gm.rating}/5, 리뷰: ${gm.reviewCount}건`);
  }

  // Naver
  const nb = channelData.naverBlog as Record<string, unknown> | undefined;
  if (nb) {
    parts.push(`### 네이버 블로그: 검색결과 ${nb.totalResults}건`);
  }
  const np = channelData.naverPlace as Record<string, unknown> | undefined;
  if (np) {
    parts.push(`### 네이버 플레이스: ${np.name} (${np.category})`);
  }

  // Vision Analysis (from Gemini Vision on screenshots)
  const vision = channelData.visionAnalysis as Record<string, unknown> | undefined;
  if (vision) {
    parts.push("\n### Vision Analysis (스크린샷 기반 추출 데이터)");
    if (vision.foundingYear) parts.push(`- 개원 연도: ${vision.foundingYear}`);
    if (vision.operationYears) parts.push(`- 운영 기간: ${vision.operationYears}년`);
    const doctors = vision.doctors as { name: string; specialty: string; position?: string }[] | undefined;
    if (doctors?.length) {
      parts.push(`- 의료진 (스크린샷 확인): ${doctors.map(d => `${d.name}(${d.specialty}${d.position ? ', ' + d.position : ''})`).join(', ')}`);
    }
    const certs = vision.certifications as string[] | undefined;
    if (certs?.length) parts.push(`- 인증: ${certs.join(', ')}`);
    const services = vision.serviceCategories as string[] | undefined;
    if (services?.length) parts.push(`- 시술 카테고리: ${services.join(', ')}`);
    const slogans = vision.slogans as string[] | undefined;
    if (slogans?.length) parts.push(`- 슬로건: ${slogans.join(' / ')}`);
    const ytStats = vision.youtubeStats as Record<string, string> | undefined;
    if (ytStats) parts.push(`- YouTube (스크린샷): 구독자 ${ytStats.subscribers || '?'}, 영상 ${ytStats.videos || '?'}`);
    const igStats = vision.instagramStats as Record<string, string> | undefined;
    if (igStats) parts.push(`- Instagram (스크린샷): 팔로워 ${igStats.followers || '?'}, 게시물 ${igStats.posts || '?'}`);
    const guStats = vision.gangnamUnniStats as Record<string, unknown> | undefined;
    if (guStats) parts.push(`- 강남언니 (스크린샷): 평점 ${guStats.rating || '?'}/10, 리뷰 ${guStats.reviews || '?'}건, 의사 ${guStats.doctors || '?'}명`);
  }

  return parts.join("\n");
}
