import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import * as fs from "fs";
import * as path from "path";
import axios from "axios";
import { PredictionCardDto, TopPick } from "./dto/prediction-card.dto";

let createCanvas: any;
let loadImage: any;
try {
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  const canvas = require("canvas");
  createCanvas = canvas.createCanvas;
  loadImage = canvas.loadImage;
} catch {
  // Canvas unavailable: render calls will fail with a clear error.
}

type Theme = {
  accent: string;
  accent2: string;
  mutedAccent: string;
  glow: string;
  title: string;
  halfLabel: string;
  backdrop: "pitch" | "court";
};

@Injectable()
export class ImageRendererService implements OnModuleInit {
  private readonly logger = new Logger(ImageRendererService.name);
  private readonly outputDir = path.join(
    process.cwd(),
    "public",
    "predictions",
  );

  onModuleInit() {
    if (!fs.existsSync(this.outputDir)) {
      fs.mkdirSync(this.outputDir, { recursive: true });
    }
  }

  async renderCard(card: PredictionCardDto): Promise<string> {
    if (!createCanvas || !loadImage) {
      throw new Error("canvas native module is not available");
    }

    // SEO-friendly filename: "<league>-<home>-vs-<away>-<yyyymmdd>.jpg"
    // Search engines index image filenames; matchId is opaque, this isn't.
    const fileName = this.buildSeoFilename(card);
    const filePath = path.join(this.outputDir, fileName);

    this.logger.log(
      `Rendering ${card.sport} social card for ${card.homeTeam} vs ${card.awayTeam}`,
    );
    await this.drawCanvas(card, filePath);

    // Sidecar metadata for OpenGraph / SEO consumers (same basename + .json).
    try {
      this.writeMetadataSidecar(card, filePath);
    } catch (e) {
      this.logger.warn(`metadata sidecar failed: ${(e as Error).message}`);
    }

    return filePath;
  }

  private buildSeoFilename(card: PredictionCardDto): string {
    const slug = (s: string) =>
      (s || "")
        .toLowerCase()
        .normalize("NFD")
        .replace(/[̀-ͯ]/g, "")
        .replace(/[ıİ]/g, "i")
        .replace(/[şŞ]/g, "s")
        .replace(/[çÇ]/g, "c")
        .replace(/[ğĞ]/g, "g")
        .replace(/[öÖ]/g, "o")
        .replace(/[üÜ]/g, "u")
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "")
        .slice(0, 40);
    const league = slug(card.leagueName) || "match";
    const home = slug(card.homeTeam) || "home";
    const away = slug(card.awayTeam) || "away";
    const d = new Date();
    const stamp = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
    return `${league}-${home}-vs-${away}-${stamp}.jpg`;
  }

  private writeMetadataSidecar(card: PredictionCardDto, imagePath: string): void {
    const meta = {
      title: `${card.homeTeam} - ${card.awayTeam} Maç Tahmini | ${card.leagueName}`,
      description:
        `${card.leagueName} maçında ${card.homeTeam} - ${card.awayTeam} ` +
        `karşılaşması için AI destekli maç tahmini. Tahmini skor ${card.ftScore}, ` +
        `ilk yarı ${card.htScore}. En iyi 3 bahis önerisi.`,
      og: {
        type: "article",
        title: `${card.homeTeam} vs ${card.awayTeam} — ${card.leagueName}`,
        description: `Skor tahmini: ${card.ftScore} (İlk yarı: ${card.htScore})`,
        image_alt: `${card.homeTeam} - ${card.awayTeam} maç tahmin kartı`,
      },
      structured_data: {
        "@context": "https://schema.org",
        "@type": "SportsEvent",
        name: `${card.homeTeam} vs ${card.awayTeam}`,
        sport: card.sport === "basketball" ? "Basketball" : "Football",
        homeTeam: { "@type": "SportsTeam", name: card.homeTeam },
        awayTeam: { "@type": "SportsTeam", name: card.awayTeam },
        location: card.countryName || undefined,
        organizer: card.leagueName,
        startDate: card.matchDate,
      },
      picks: card.topPicks.map((p) => ({
        market: p.market,
        pick: p.pick,
        confidence: p.confidence,
        odds: p.odds,
      })),
      generated_at: new Date().toISOString(),
    };
    const sidecar = imagePath.replace(/\.jpg$/i, ".json");
    fs.writeFileSync(sidecar, JSON.stringify(meta, null, 2), "utf-8");
  }

  private async drawCanvas(
    data: PredictionCardDto,
    outPath: string,
  ): Promise<void> {
    const width = 1080;
    const height = 1080;
    const canvas = createCanvas(width, height);
    const ctx = canvas.getContext("2d");
    const theme = this.getTheme(data.sport);

    this.drawBackground(ctx, width, height, theme);

    const [homeImg, awayImg, countryFlag, leagueLogo] = await Promise.all([
      this.loadImageSafe(data.homeLogo),
      this.loadImageSafe(data.awayLogo),
      this.loadImageSafe(data.countryFlag || ""),
      this.loadImageSafe(data.leagueLogo || ""),
    ]);

    this.drawHeader(ctx, data, theme, countryFlag, leagueLogo);
    this.drawMatchBlock(ctx, data, theme, homeImg, awayImg);
    this.drawScoreBlock(ctx, data, theme);
    this.drawPicks(ctx, data.topPicks, theme);
    this.drawFooter(ctx, data);

    const buffer = canvas.toBuffer("image/jpeg", { quality: 0.94 });
    fs.writeFileSync(outPath, buffer);
  }

  private getTheme(sport: "football" | "basketball"): Theme {
    if (sport === "basketball") {
      return {
        accent: "#ff7a1a",
        accent2: "#20e48b",
        mutedAccent: "rgba(255, 122, 26, 0.18)",
        glow: "rgba(255, 122, 26, 0.35)",
        title: "BASKETBOL TAHMİNİ",
        halfLabel: "İlk Devre",
        backdrop: "court",
      };
    }

    return {
      accent: "#21e88a",
      accent2: "#65d7ff",
      mutedAccent: "rgba(33, 232, 138, 0.15)",
      glow: "rgba(33, 232, 138, 0.28)",
      title: "MAÇ TAHMİNİ",
      halfLabel: "İlk Yarı",
      backdrop: "pitch",
    };
  }

  private drawBackground(
    ctx: any,
    width: number,
    height: number,
    theme: Theme,
  ) {
    const bg = ctx.createRadialGradient(540, 300, 120, 540, 540, 820);
    bg.addColorStop(0, "#1a2328");
    bg.addColorStop(0.45, "#071015");
    bg.addColorStop(1, "#020407");
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, width, height);

    const floor = ctx.createLinearGradient(0, 570, 0, 1080);
    floor.addColorStop(0, "rgba(0,0,0,0)");
    floor.addColorStop(
      0.58,
      theme.backdrop === "court"
        ? "rgba(120,55,12,0.58)"
        : "rgba(10,70,36,0.48)",
    );
    floor.addColorStop(1, "#030405");
    ctx.fillStyle = floor;
    ctx.fillRect(0, 520, width, 560);

    ctx.save();
    ctx.globalAlpha = theme.backdrop === "court" ? 0.28 : 0.22;
    ctx.strokeStyle = "rgba(255,255,255,0.18)";
    ctx.lineWidth = 2;

    if (theme.backdrop === "court") {
      for (let y = 560; y < height; y += 36) {
        ctx.beginPath();
        ctx.moveTo(0, y + (y - 560) * 0.18);
        ctx.lineTo(width, y + (y - 560) * 0.18);
        ctx.stroke();
      }
      ctx.beginPath();
      ctx.arc(width / 2, 710, 240, Math.PI, Math.PI * 2);
      ctx.stroke();
    } else {
      ctx.beginPath();
      ctx.moveTo(0, 630);
      ctx.lineTo(width, 630);
      ctx.moveTo(0, 840);
      ctx.lineTo(width, 790);
      ctx.moveTo(130, 1080);
      ctx.lineTo(392, 640);
      ctx.moveTo(950, 1080);
      ctx.lineTo(688, 640);
      ctx.stroke();
    }
    ctx.restore();

    this.radialGlow(ctx, 120, 135, 260, "rgba(255,255,255,0.23)");
    this.radialGlow(ctx, 960, 135, 260, "rgba(255,255,255,0.23)");
    this.radialGlow(ctx, 205, 210, 360, theme.glow);
    this.radialGlow(ctx, 875, 210, 360, theme.glow);
    this.radialGlow(ctx, 540, 640, 400, theme.mutedAccent);

    ctx.save();
    ctx.strokeStyle = theme.accent;
    ctx.globalAlpha = 0.32;
    ctx.lineWidth = 2;
    for (let i = 0; i < 6; i++) {
      const offset = i * 14;
      ctx.beginPath();
      ctx.moveTo(70 + offset, 420);
      ctx.lineTo(238 + offset, 360);
      ctx.lineTo(402 + offset, 420);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(1010 - offset, 420);
      ctx.lineTo(842 - offset, 360);
      ctx.lineTo(678 - offset, 420);
      ctx.stroke();
    }
    ctx.restore();

    ctx.save();
    ctx.strokeStyle = "rgba(255,255,255,0.18)";
    ctx.lineWidth = 1;
    for (let y = 120; y < 520; y += 52) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(width, y - 28);
      ctx.stroke();
    }
    ctx.restore();
  }

  private drawHeader(
    ctx: any,
    data: PredictionCardDto,
    theme: Theme,
    countryFlag: any,
    leagueLogo: any,
  ) {
    ctx.save();
    ctx.shadowColor = theme.glow;
    ctx.shadowBlur = 20;
    ctx.fillStyle = "rgba(0,0,0,0.78)";
    this.angularRect(ctx, 118, 26, 844, 70, 34, true);
    ctx.restore();
    ctx.strokeStyle = theme.accent;
    ctx.lineWidth = 2;
    this.angularRect(ctx, 118, 26, 844, 70, 34, false, true);

    const iconY = 62;
    this.drawHeaderItem(
      ctx,
      data.sport === "basketball" ? "◉" : "●",
      data.leagueName,
      204,
      iconY,
      theme,
      leagueLogo,
    );
    this.drawHeaderItem(
      ctx,
      "",
      data.countryName || "",
      520,
      iconY,
      theme,
      countryFlag,
    );
    this.drawHeaderItem(ctx, "◷", data.matchDate, 790, iconY, theme);

    ctx.fillStyle = theme.accent;
    ctx.fillRect(388, 44, 2, 36);
    ctx.fillRect(634, 44, 2, 36);
  }

  private drawMatchBlock(
    ctx: any,
    data: PredictionCardDto,
    theme: Theme,
    homeImg: any,
    awayImg: any,
  ) {
    this.drawTeam(ctx, homeImg, data.homeTeam, 260, 214, theme);
    this.drawTeam(ctx, awayImg, data.awayTeam, 820, 214, theme);

    ctx.save();
    ctx.shadowColor = "rgba(255,255,255,0.55)";
    ctx.shadowBlur = 12;
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 70px Arial";
    ctx.textAlign = "center";
    ctx.fillText("VS", 540, 265);
    ctx.restore();

    ctx.save();
    ctx.shadowColor = theme.glow;
    ctx.shadowBlur = 18;
    ctx.fillStyle = "rgba(0,0,0,0.82)";
    this.angularRect(ctx, 230, 408, 620, 72, 42, true);
    ctx.restore();
    ctx.strokeStyle = theme.accent;
    ctx.lineWidth = 2;
    this.angularRect(ctx, 230, 408, 620, 72, 42, false, true);

    ctx.fillStyle = "#ffffff";
    ctx.font = "900 50px Arial";
    ctx.textAlign = "center";
    ctx.fillText(theme.title, 540, 462);
  }

  private drawTeam(
    ctx: any,
    img: any,
    name: string,
    centerX: number,
    centerY: number,
    theme: Theme,
  ) {
    this.drawShield(ctx, centerX, centerY - 15, 172, 204, theme);

    if (img) {
      this.drawImageContain(ctx, img, centerX - 62, centerY - 82, 124, 124);
    } else {
      ctx.fillStyle = theme.accent;
      ctx.font = "900 72px Arial";
      ctx.textAlign = "center";
      ctx.fillText(this.initials(name), centerX, centerY - 8);
    }

    ctx.save();
    ctx.shadowColor = "rgba(255,255,255,0.28)";
    ctx.shadowBlur = 8;
    ctx.fillStyle = "#ffffff";
    ctx.textAlign = "center";
    this.fitCenteredText(
      ctx,
      name.toLocaleUpperCase("tr-TR"),
      centerX,
      362,
      452,
      54,
    );
    ctx.restore();

    const underline = ctx.createLinearGradient(
      centerX - 190,
      0,
      centerX + 190,
      0,
    );
    underline.addColorStop(0, theme.accent);
    underline.addColorStop(0.5, "#ffffff");
    underline.addColorStop(1, theme.accent2);
    ctx.fillStyle = underline;
    ctx.fillRect(centerX - 190, 382, 380, 3);
  }

  private drawScoreBlock(ctx: any, data: PredictionCardDto, theme: Theme) {
    this.drawScoreCell(
      ctx,
      105,
      512,
      370,
      150,
      theme.halfLabel,
      data.htScore,
      theme,
      false,
    );
    this.drawScoreCell(
      ctx,
      605,
      512,
      370,
      150,
      "Maç Sonu",
      data.ftScore,
      theme,
      true,
    );

    this.drawSportBall(ctx, 540, 608, data.sport, theme);

    const badgeText = `Güven %${data.scoreConfidence}`;
    ctx.font = "900 32px Arial";
    const badgeW = Math.ceil(ctx.measureText(badgeText).width + 64);
    const badgeX = Math.round(540 - badgeW / 2);
    ctx.fillStyle = "rgba(0,0,0,0.88)";
    this.angularRect(ctx, badgeX, 658, badgeW, 54, 28, true);
    ctx.strokeStyle = theme.accent;
    ctx.lineWidth = 2;
    this.angularRect(ctx, badgeX, 658, badgeW, 54, 28, false, true);
    ctx.fillStyle = theme.accent2;
    ctx.font = "900 32px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(badgeText, 540, 685);
    ctx.textBaseline = "alphabetic";
  }

  private drawScoreCell(
    ctx: any,
    x: number,
    y: number,
    w: number,
    h: number,
    label: string,
    score: string,
    theme: Theme,
    highlighted: boolean,
  ) {
    const grad = ctx.createLinearGradient(x, y, x + w, y + h);
    grad.addColorStop(0, highlighted ? theme.mutedAccent : "rgba(0,0,0,0.86)");
    grad.addColorStop(1, "rgba(0,0,0,0.92)");
    ctx.fillStyle = grad;
    this.angularRect(ctx, x, y + 32, w, h - 32, 32, true);
    ctx.strokeStyle = theme.accent;
    ctx.lineWidth = 2;
    this.angularRect(ctx, x, y + 32, w, h - 32, 32, false, true);

    const labelGrad = ctx.createLinearGradient(x, y, x + w, y);
    labelGrad.addColorStop(0, theme.mutedAccent);
    labelGrad.addColorStop(1, "rgba(0,0,0,0.88)");
    ctx.fillStyle = labelGrad;
    this.angularRect(ctx, x + 55, y, w - 110, 48, 18, true);
    ctx.strokeStyle = theme.accent;
    this.angularRect(ctx, x + 55, y, w - 110, 48, 18, false, true);

    ctx.fillStyle = "#ffffff";
    ctx.font = "900 34px Arial";
    ctx.textAlign = "center";
    ctx.fillText(label, x + w / 2, y + 36);

    ctx.save();
    ctx.shadowColor = "rgba(255,255,255,0.35)";
    ctx.shadowBlur = 10;
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 92px Arial";
    ctx.fillText(score.replace("-", " - "), x + w / 2, y + 122);
    ctx.restore();
  }

  private drawPicks(ctx: any, picks: TopPick[], theme: Theme) {
    ctx.fillStyle = "rgba(0,0,0,0.88)";
    this.angularRect(ctx, 326, 704, 428, 58, 32, true);
    ctx.strokeStyle = theme.accent;
    ctx.lineWidth = 2;
    this.angularRect(ctx, 326, 704, 428, 58, 32, false, true);
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 38px Arial";
    ctx.textAlign = "center";
    ctx.fillText("EN İYİ 3 TAHMİN", 540, 746);

    picks.slice(0, 3).forEach((pick, index) => {
      const y = 772 + index * 58;
      ctx.fillStyle = "rgba(0,0,0,0.88)";
      this.angularRect(ctx, 66, y, 948, 50, 26, true);
      ctx.strokeStyle = theme.accent;
      ctx.lineWidth = 1.5;
      this.angularRect(ctx, 66, y, 948, 50, 26, false, true);

      const rankGrad = ctx.createLinearGradient(76, y, 178, y + 54);
      rankGrad.addColorStop(0, theme.accent);
      rankGrad.addColorStop(1, "rgba(0,0,0,0.75)");
      ctx.fillStyle = rankGrad;
      this.angularRect(ctx, 76, y, 88, 50, 18, true);
      ctx.fillStyle = "#ffffff";
      ctx.font = "900 34px Arial";
      ctx.textAlign = "center";
      ctx.fillText(String(index + 1), 120, y + 37);

      ctx.fillStyle = "#ffffff";
      this.fitText(ctx, this.pickLabel(pick), 192, y + 34, 330, 29, "900");

      const barX = 520;
      const barY = y + 20;
      const barW = 330;
      ctx.fillStyle = "rgba(255,255,255,0.22)";
      this.roundRect(ctx, barX, barY, barW, 10, 5, true);
      const fill = Math.max(0, Math.min(1, pick.confidence / 100));
      const grad = ctx.createLinearGradient(barX, 0, barX + barW, 0);
      grad.addColorStop(0, "#8df05d");
      grad.addColorStop(1, theme.accent2);
      ctx.fillStyle = grad;
      this.roundRect(ctx, barX, barY, barW * fill, 10, 5, true);

      ctx.fillStyle = "#89f45d";
      ctx.font = "900 34px Arial";
      ctx.textAlign = "right";
      ctx.fillText(`%${pick.confidence}`, 962, y + 37);
    });
  }

  private drawFooter(ctx: any, data: PredictionCardDto) {
    const riskText = `Risk: ${this.translateRisk(data.riskLevel)}`;
    ctx.font = "900 34px Arial";
    const riskW = Math.ceil(ctx.measureText(riskText).width + 72);
    const riskX = Math.round(540 - riskW / 2);
    ctx.fillStyle = "rgba(0,0,0,0.86)";
    this.roundRect(ctx, riskX, 956, riskW, 54, 24, true);
    ctx.strokeStyle = "#ffbe32";
    ctx.lineWidth = 2;
    this.roundRect(ctx, riskX, 956, riskW, 54, 24, false, true);
    ctx.fillStyle = this.riskColor(data.riskLevel);
    ctx.font = "900 34px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(riskText, 540, 983);
    ctx.textBaseline = "alphabetic";

    ctx.fillStyle = "#ffffff";
    ctx.font = "900 42px Arial";
    ctx.textAlign = "center";
    ctx.fillText("iddaai.com", 540, 1065);
  }

  private drawHeaderItem(
    ctx: any,
    icon: string,
    text: string,
    centerX: number,
    centerY: number,
    theme: Theme,
    image?: any,
  ) {
    if (image) {
      this.drawImageCover(ctx, image, centerX - 92, centerY - 16, 34, 28, 8);
    } else {
      ctx.fillStyle = theme.accent;
      ctx.font = "900 28px Arial";
      ctx.textAlign = "center";
      ctx.fillText(icon, centerX - 75, centerY + 10);
    }

    ctx.fillStyle = "#ffffff";
    ctx.font = "900 30px Arial";
    ctx.textAlign = "left";
    ctx.fillText(text, centerX - 42, centerY + 10);
  }

  private drawShield(
    ctx: any,
    centerX: number,
    centerY: number,
    width: number,
    height: number,
    theme: Theme,
  ) {
    const x = centerX - width / 2;
    const y = centerY - height / 2;

    ctx.save();
    ctx.shadowColor = theme.glow;
    ctx.shadowBlur = 26;
    const grad = ctx.createLinearGradient(x, y, x + width, y + height);
    grad.addColorStop(0, theme.mutedAccent);
    grad.addColorStop(0.55, "rgba(2,8,12,0.95)");
    grad.addColorStop(1, "rgba(0,0,0,0.98)");
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.moveTo(centerX, y);
    ctx.lineTo(x + width - 10, y + 34);
    ctx.lineTo(x + width - 20, y + height - 56);
    ctx.quadraticCurveTo(centerX, y + height + 8, x + 20, y + height - 56);
    ctx.lineTo(x + 10, y + 34);
    ctx.closePath();
    ctx.fill();
    ctx.restore();

    ctx.strokeStyle = "#ffd34d";
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.moveTo(centerX, y);
    ctx.lineTo(x + width - 10, y + 34);
    ctx.lineTo(x + width - 20, y + height - 56);
    ctx.quadraticCurveTo(centerX, y + height + 8, x + 20, y + height - 56);
    ctx.lineTo(x + 10, y + 34);
    ctx.closePath();
    ctx.stroke();
  }

  private drawSportBall(
    ctx: any,
    x: number,
    y: number,
    sport: PredictionCardDto["sport"],
    theme: Theme,
  ) {
    ctx.save();
    ctx.shadowColor = theme.glow;
    ctx.shadowBlur = 18;
    ctx.fillStyle = "rgba(0,0,0,0.85)";
    ctx.beginPath();
    ctx.arc(x, y, 55, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = theme.accent;
    ctx.lineWidth = 3;
    ctx.stroke();

    ctx.fillStyle = sport === "basketball" ? "#d96a19" : "#f4f4f4";
    ctx.beginPath();
    ctx.arc(x, y, 36, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = sport === "basketball" ? "#1b0f08" : "#111";
    ctx.lineWidth = 3;
    ctx.stroke();

    ctx.beginPath();
    if (sport === "basketball") {
      ctx.moveTo(x - 36, y);
      ctx.lineTo(x + 36, y);
      ctx.moveTo(x, y - 36);
      ctx.lineTo(x, y + 36);
      ctx.arc(x - 36, y, 36, -Math.PI / 2, Math.PI / 2);
      ctx.arc(x + 36, y, 36, Math.PI / 2, -Math.PI / 2);
    } else {
      ctx.moveTo(x - 28, y);
      ctx.lineTo(x + 28, y);
      ctx.moveTo(x, y - 28);
      ctx.lineTo(x, y + 28);
      ctx.arc(x, y, 18, 0, Math.PI * 2);
    }
    ctx.stroke();
    ctx.restore();
  }

  private async loadImageSafe(url: string) {
    if (!url) return null;

    try {
      if (url.startsWith("/")) {
        const localPath = path.join(process.cwd(), "public", url);
        if (fs.existsSync(localPath)) {
          return await loadImage(localPath);
        }
      }

      if (url.startsWith("http")) {
        const response = await axios.get(url, {
          responseType: "arraybuffer",
          timeout: 7000,
        });
        return await loadImage(response.data);
      }
    } catch (error) {
      this.logger.warn(
        `Could not load social poster image ${url}: ${error.message}`,
      );
    }

    return null;
  }

  private roundRect(
    ctx: any,
    x: number,
    y: number,
    width: number,
    height: number,
    radius: number,
    fill = false,
    stroke = false,
  ) {
    const r = Math.min(radius, width / 2, height / 2);
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + width, y, x + width, y + height, r);
    ctx.arcTo(x + width, y + height, x, y + height, r);
    ctx.arcTo(x, y + height, x, y, r);
    ctx.arcTo(x, y, x + width, y, r);
    ctx.closePath();
    if (fill) ctx.fill();
    if (stroke) ctx.stroke();
  }

  private angularRect(
    ctx: any,
    x: number,
    y: number,
    width: number,
    height: number,
    cut: number,
    fill = false,
    stroke = false,
  ) {
    const c = Math.min(cut, width / 2, height / 2);
    ctx.beginPath();
    ctx.moveTo(x + c, y);
    ctx.lineTo(x + width - c, y);
    ctx.lineTo(x + width, y + height / 2);
    ctx.lineTo(x + width - c, y + height);
    ctx.lineTo(x + c, y + height);
    ctx.lineTo(x, y + height / 2);
    ctx.closePath();
    if (fill) ctx.fill();
    if (stroke) ctx.stroke();
  }

  private radialGlow(
    ctx: any,
    x: number,
    y: number,
    radius: number,
    color: string,
  ) {
    const grad = ctx.createRadialGradient(x, y, 0, x, y, radius);
    grad.addColorStop(0, color);
    grad.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = grad;
    ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
  }

  private drawImageContain(
    ctx: any,
    img: any,
    x: number,
    y: number,
    w: number,
    h: number,
  ) {
    const ratio = Math.min(w / img.width, h / img.height);
    const drawW = img.width * ratio;
    const drawH = img.height * ratio;
    ctx.drawImage(img, x + (w - drawW) / 2, y + (h - drawH) / 2, drawW, drawH);
  }

  private drawImageCover(
    ctx: any,
    img: any,
    x: number,
    y: number,
    w: number,
    h: number,
    radius: number,
  ) {
    ctx.save();
    this.roundRect(ctx, x, y, w, h, radius, false);
    ctx.clip();
    const ratio = Math.max(w / img.width, h / img.height);
    const drawW = img.width * ratio;
    const drawH = img.height * ratio;
    ctx.drawImage(img, x + (w - drawW) / 2, y + (h - drawH) / 2, drawW, drawH);
    ctx.restore();
  }

  private fitText(
    ctx: any,
    text: string,
    x: number,
    y: number,
    maxWidth: number,
    fontSize: number,
    weight = "700",
  ) {
    let size = fontSize;
    do {
      ctx.font = `${weight} ${size}px Arial`;
      if (ctx.measureText(text).width <= maxWidth) break;
      size -= 1;
    } while (size >= 16);
    ctx.textAlign = x < 540 ? "left" : "center";
    ctx.fillText(text, x < 540 ? x : x + maxWidth / 2, y);
  }

  private fitCenteredText(
    ctx: any,
    text: string,
    centerX: number,
    y: number,
    maxWidth: number,
    fontSize: number,
  ) {
    let size = fontSize;
    do {
      ctx.font = `900 ${size}px Arial`;
      if (ctx.measureText(text).width <= maxWidth) break;
      size -= 1;
    } while (size >= 22);
    ctx.textAlign = "center";
    ctx.fillText(text, centerX, y);
  }

  private pickLabel(pick: TopPick): string {
    return pick.market
      .replace(/\s*:\s*/g, " ")
      .replace(/(Üst\s+\d+(?:\.\d+)?\s+Gol)\s+Üst$/i, "$1")
      .trim();
  }

  private initials(name: string): string {
    return name
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 2)
      .map((part) => part[0])
      .join("")
      .toLocaleUpperCase("tr-TR");
  }

  private translateRisk(risk: PredictionCardDto["riskLevel"]): string {
    switch (risk) {
      case "LOW":
        return "Düşük";
      case "HIGH":
        return "Yüksek";
      case "EXTREME":
        return "Çok Yüksek";
      case "MEDIUM":
      default:
        return "Orta";
    }
  }

  private riskBg(risk: PredictionCardDto["riskLevel"]): string {
    if (risk === "LOW") return "rgba(32, 228, 139, 0.16)";
    if (risk === "HIGH") return "rgba(255, 122, 26, 0.16)";
    if (risk === "EXTREME") return "rgba(255, 64, 80, 0.16)";
    return "rgba(255, 196, 52, 0.16)";
  }

  private riskColor(risk: PredictionCardDto["riskLevel"]): string {
    if (risk === "LOW") return "#20e48b";
    if (risk === "HIGH") return "#ff7a1a";
    if (risk === "EXTREME") return "#ff4050";
    return "#ffc434";
  }

  getImageUrl(filePath: string): string {
    const relativePath = path.relative(
      path.join(process.cwd(), "public"),
      filePath,
    );
    return `/${relativePath.replace(/\\/g, "/")}`;
  }
}
