const REDIRECT_URI =

  "https://1000antiguedades-oauth.1000antiguedades.workers.dev/callback";


// =====================================================

// FUNCIÓN DE SEGURIDAD HTML

// =====================================================


function escapeHtml(text) {


  return String(text ?? "")

    .replace(/&/g, "&")

    .replace(/</g, "&lt;")

    .replace(/>/g, "&gt;")

    .replace(/"/g, "&quot;")

    .replace(/'/g, "&#039;");


}


export default {


  async fetch(request, env) {


    const url = new URL(request.url);


    // =====================================================

    // PÁGINA PRINCIPAL

    // =====================================================


    if (url.pathname === "/") {


      return new Response(

        "1000 Antigüedades — Conexión con Mercado Libre\n\n" +

        "Visita /login para iniciar la autorización.\n\n" +

        "Catálogo: /catalogo",

        {

          headers: {

            "Content-Type": "text/plain; charset=utf-8"

          }

        }

      );

    }


    // =====================================================

    // INICIAR AUTORIZACIÓN

    // =====================================================


    if (url.pathname === "/login") {


      const codeVerifier =

        crypto.randomUUID() + crypto.randomUUID();


      const data =

        new TextEncoder().encode(codeVerifier);


      const digest =

        await crypto.subtle.digest(

          "SHA-256",

          data

        );


      const codeChallenge =

        btoa(

          String.fromCharCode(

            ...new Uint8Array(digest)

          )

        )

        .replace(/\+/g, "-")

        .replace(/\//g, "_")

        .replace(/=/g, "");


      const authUrl =

        new URL(

          "https://auth.mercadolibre.com.co/authorization"

        );


      authUrl.searchParams.set(

        "response_type",

        "code"

      );


      authUrl.searchParams.set(

        "client_id",

        env.ML_CLIENT_ID

      );


      authUrl.searchParams.set(

        "redirect_uri",

        REDIRECT_URI

      );


      authUrl.searchParams.set(

        "code_challenge",

        codeChallenge

      );


      authUrl.searchParams.set(

        "code_challenge_method",

        "S256"

      );


      return new Response(null, {


        status: 302,


        headers: {


          Location:

            authUrl.toString(),


          "Set-Cookie":

            `ml_code_verifier=${codeVerifier}; ` +

            "Path=/; Secure; HttpOnly; SameSite=Lax"

        }

      });

    }


    // =====================================================

    // CALLBACK DE MERCADO LIBRE

    // =====================================================


    if (url.pathname === "/callback") {


      const code =

        url.searchParams.get("code");


      if (!code) {


        return new Response(

          "Mercado Libre no devolvió un código de autorización.",

          {

            status: 400

          }

        );

      }


      // Recuperar el code_verifier


      const cookie =

        request.headers.get("Cookie") || "";


      const match =

        cookie.match(

          /ml_code_verifier=([^;]+)/

        );


      if (!match) {


        return new Response(

          "No encontramos el code_verifier. " +

          "Inicia nuevamente desde /login.",

          {

            status: 400

          }

        );

      }


      const codeVerifier =

        match[1];


      // ===================================================

      // INTERCAMBIAR CODE POR TOKENS

      // ===================================================


      const body =

        new URLSearchParams();


      body.set(

        "grant_type",

        "authorization_code"

      );


      body.set(

        "client_id",

        env.ML_CLIENT_ID

      );


      body.set(

        "client_secret",

        env.ML_CLIENT_SECRET

      );


      body.set(

        "code",

        code

      );


      body.set(

        "redirect_uri",

        REDIRECT_URI

      );


      body.set(

        "code_verifier",

        codeVerifier

      );


      const response =

        await fetch(

          "https://api.mercadolibre.com/oauth/token",

          {

            method: "POST",


            headers: {

              "Accept":

                "application/json",


              "Content-Type":

                "application/x-www-form-urlencoded"

            },


            body

          }

        );


      const result =

        await response.json();


      // ===================================================

      // COMPROBAR RESPUESTA

      // ===================================================


      if (!response.ok) {


        return new Response(

          JSON.stringify(

            result,

            null,

            2

          ),

          {

            status:

              response.status,


            headers: {

              "Content-Type":

                "application/json"

            }

          }

        );

      }


      // ===================================================

      // GUARDAR TOKENS EN CLOUDFLARE KV

      // ===================================================


      await env.ML_TOKENS.put(

        "oauth_tokens",

        JSON.stringify({


          access_token:

            result.access_token,


          refresh_token:

            result.refresh_token,


          expires_in:

            result.expires_in,


          user_id:

            result.user_id,


          obtained_at:

            Date.now()

        })

      );


      // ===================================================

      // BORRAR COOKIE DEL CODE VERIFIER

      // ===================================================


      return new Response(


        "AUTORIZACIÓN COMPLETADA CORRECTAMENTE.\n\n" +

        "Mercado Libre devolvió los tokens.\n\n" +

        "Los tokens fueron guardados de forma segura en Cloudflare KV.",


        {

          headers: {


            "Content-Type":

              "text/plain; charset=utf-8",


            "Set-Cookie":

              "ml_code_verifier=; " +

              "Path=/; Secure; HttpOnly; " +

              "SameSite=Lax; Max-Age=0"

          }

        }

      );

    }


// =====================================================

// CATÁLOGO — CONSULTA DESDE D1

// =====================================================


// =====================================================

// CATÁLOGO — TIENDA VISUAL

// =====================================================




if (url.pathname === "/catalogo") {


  // ---------------------------------------------------

  // OBTENER PRODUCTOS DESDE D1

  // ---------------------------------------------------


  const q =

  url.searchParams.get("q") || "";


const categoria =

  url.searchParams.get("categoria") || "";


let productosResult;


  if (q.trim()) {


    // ---------------------------------------------------

    // NORMALIZAR TEXTO

    // ---------------------------------------------------


    const normalizarTexto = texto =>

      String(texto || "")

        .toLowerCase()

        .normalize("NFD")

        .replace(/[\u0300-\u036f]/g, "")

        .replace(/[-\s]+/g, "");


    const textoBusqueda =

      normalizarTexto(q.trim());


    // ---------------------------------------------------

    // SINÓNIMOS CONTROLADOS

    // ---------------------------------------------------


    let terminosBusqueda = [

      textoBusqueda

    ];


    if (

      textoBusqueda === "cocacola" ||

      textoBusqueda === "coke" ||

      textoBusqueda === "gaseosa" ||

      textoBusqueda === "postobon"

    ) {


      terminosBusqueda = [

        "cocacola",

        "coke",

        "gaseosa",

        "postobon"

      ];


    }


    // ---------------------------------------------------

    // OBTENER PRODUCTOS DESDE D1

    // ---------------------------------------------------


let consultaCategoria = "";

let parametroCategoria = "";


if (categoria) {


  consultaCategoria = `

    AND EXISTS (

      SELECT 1

      FROM producto_categorias pc

      JOIN categorias c

        ON c.id = pc.categoria_id

      WHERE pc.producto_id = productos.id

        AND c.nombre = ?

    )

  `;


  parametroCategoria = categoria;

}


productosResult =

  await env.DB.prepare(

    `SELECT

      id,

      ml_id,

      slug,

      titulo,

      precio,

      moneda,

      descripcion,

      condicion,

      categoria_tienda,

      estado_tienda

     FROM productos

     WHERE 1 = 1

     ${consultaCategoria}

     ORDER BY id`

  )

  .bind(

    ...(parametroCategoria

      ? [parametroCategoria]

      : [])

  )

  .all();


    // ---------------------------------------------------

    // FILTRAR CON TEXTO NORMALIZADO

    // ---------------------------------------------------


    const productosTodos =

      productosResult.results || [];


    const productosFiltrados =

      productosTodos.filter(

        producto => {


          const titulo =

            normalizarTexto(

              producto.titulo

            );


          const descripcion =

            normalizarTexto(

              producto.descripcion

            );


          const categoria =

            normalizarTexto(

              producto.categoria_tienda

            );


          const textoProducto =

            `${titulo} ${descripcion} ${categoria}`;


          return terminosBusqueda.some(

            termino =>

              textoProducto.includes(termino)

          );


        }

      );


    productosResult = {

      results: productosFiltrados

    };


  } else {


  // ---------------------------------------------------

  // SIN BÚSQUEDA → FILTRAR POR CATEGORÍA SI EXISTE

  // ---------------------------------------------------


  let consultaCategoria = "";

  let parametroCategoria = "";


  if (categoria) {


    consultaCategoria = `

      WHERE EXISTS (

        SELECT 1

        FROM producto_categorias pc

        JOIN categorias c

          ON c.id = pc.categoria_id

        WHERE pc.producto_id = productos.id

          AND c.nombre = ?

      )

    `;


    parametroCategoria = categoria;

  }


  productosResult =

    await env.DB.prepare(

      `SELECT

        id,

        ml_id,

        slug,

        titulo,

        precio,

        moneda,

        descripcion,

        condicion,

        categoria_tienda,

        estado_tienda

       FROM productos

       ${consultaCategoria}

       ORDER BY id`

    )

    .bind(

      ...(parametroCategoria

        ? [parametroCategoria]

        : [])

    )

    .all();


}


  const productos =

    productosResult.results || [];


  // ---------------------------------------------------

  // OBTENER FOTO PRINCIPAL

  // ---------------------------------------------------


  const productosConFotos =

    await Promise.all(


      productos.map(

        async producto => {


          const fotoResult =

            await env.DB.prepare(

              `SELECT url

               FROM producto_fotos

               WHERE producto_id = ?

               ORDER BY

                 es_principal DESC,

                 orden ASC

               LIMIT 1`

            )

              .bind(producto.id)

              .first();


          return {

            ...producto,

            foto_principal:

              fotoResult

                ? fotoResult.url

                : null

          };


        }

      )


    );


  // ---------------------------------------------------

  // CONSTRUIR TARJETAS

  // ---------------------------------------------------


  const tarjetas =

    productosConFotos.map(

      producto => {


        const imagen =

          producto.foto_principal

            ? `/foto?archivo=${encodeURIComponent(

                producto.foto_principal

              )}`

            : "";


        const precio =

          Number(

            producto.precio || 0

          ).toLocaleString("es-CO");


        return `


          <article class="producto-card">


            <a

              class="producto-imagen"

              href="/producto/${producto.slug}"

            >


              ${

                imagen

                  ? `

                    <img

                      src="${imagen}"

                      alt="${escapeHtml(

                        producto.titulo

                      )}"

                      loading="lazy"

                    >

                  `

                  : `

                    <div class="sin-foto">

                      SIN FOTO

                    </div>

                  `

              }


            </a>


            <div class="producto-info">


              <h2>

                ${escapeHtml(

                  producto.titulo

                )}

              </h2>


              <div class="producto-precio">

                $ ${precio}

              </div>


              <a

                class="producto-boton"

                href="/producto/${producto.slug}"

              >

                VER PIEZA

              </a>


            </div>


          </article>


        `;


      }

    ).join("");


  // ---------------------------------------------------

  // PÁGINA VISUAL

  // ---------------------------------------------------


  return new Response(


    `<!DOCTYPE html>


    <html lang="es">


    <head>


      <meta charset="UTF-8">


      <meta

        name="viewport"

        content="width=device-width, initial-scale=1.0"

      >


      <title>

        1000 Antigüedades — Catálogo

      </title>


      <style>


        * {

          box-sizing: border-box;

        }


        body {


          margin: 0;


 background-image: url("https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_5NMDRQvqu-st-f3sRDOjSWvBl89yblLRDUWUIJo4ApZvvWqQ-u1Ln_VqqA4VJZfq0CFOGUC68QowcnU0SPtcYMrZJZbSlRgKW7dPqeSmFkYp_jhoUqJscpDW_Vduvjxgo5DGWyw7BOcn2vcAVe9DhcElaFRE0XYXjF_Zy3PSQfNeRO_4I1U9RMmexk8/s1248/Fondo%20de%20catalogo%20y%20Ficha%20individual.png");


 background-size: 100% 100vh;

background-position: center -5px;

background-repeat: no-repeat;

background-attachment: fixed;


          color: #3b2a23;


          font-family:

  Georgia,

  "Times New Roman",

  serif;

}


.enlace-inicio {

  display: inline-block;

  margin: 23px 0 0 60px;

  color: #7a4b3f;

  text-decoration: none;

  font-size: 15px;

}




.enlace-inicio:hover {

  text-decoration: underline;

}


.enlace-whatsapp {

  color: #7a4b3f;

  text-decoration: none;

  font-size: 15px;

}


.enlace-whatsapp:hover {

  text-decoration: underline;

}




        .catalogo-contenedor {


          width: 94%;


          max-width: 1500px;


          margin: 0 auto;


          padding: 0px 0 60px;


        }


        .catalogo-titulo {


          text-align: center;


          margin-bottom: 35px;


        }


 .catalogo-titulo h1 {


  margin: 0;


  font-size: 42px;


  letter-spacing: 2px;


  font-weight: normal;

  

  font-family: Cambria, serif;


}


.marca-catalogo {


  display: inline-flex;


  align-items: center;


  justify-content: center;


  gap: 14px;


  color: #3b2a23;


  text-decoration: none;


  cursor: pointer;


}


.marca-catalogo:hover {


  color: #7a4b3f;


}


.catalogo-logo {


  width: 62px;


  height: 62px;


  object-fit: contain;


  display: block;


}


        .catalogo-titulo p {

  margin-top: 0px;

  font-size: 18px;

  font-family: inherit;

  font-weight: normal;

  letter-spacing: normal;

  line-height: 0.2;

  opacity: .8;

}



.buscador {

  display: flex;

  max-width: 700px;

  margin: 0 auto 35px;

  gap: 8px;

}


.buscador input {

  flex: 1;

  padding: 13px 15px;

  border: 1px solid #d8c9b2;

  background: #fffdf8;

  color: #3b2a23;

  font-family: inherit;

  font-size: 15px;

}


.buscador button {

  padding: 13px 22px;

  background: #7a4b3f;

  color: #f3ead7;

  border: 1px solid #b58a43;

  cursor: pointer;

  font-family: inherit;

  font-size: 13px;

  letter-spacing: .5px;

}


.buscador button:hover {

  background: #9b9270;

}


.filtros-categorias {


  display: flex;


  justify-content: center;


  flex-wrap: wrap;


  gap: 10px;


  margin: 0 auto 35px;


}


.filtros-categorias a {


  display: inline-block;


  padding: 10px 18px;


  border: 1px solid #b58a43;


  background: #fffdf8;


  color: #7a4b3f;


  text-decoration: none;


  font-size: 13px;


  letter-spacing: .6px;


  transition:

    background .2s ease,

    color .2s ease;


}


.filtros-categorias a:hover {


  background: #9b9270;


  color: #fff;


}


.filtros-categorias a.activo {


  background: #7a4b3f;


  color: #f3ead7;


}


        .productos-grid {


  display: grid;


  grid-template-columns:

    repeat(3, 1fr);


  gap: 28px 20px;


}


        .producto-card {


          background: #F3E8D4;


          border: 1px solid #c3a45b;

box-shadow:

  inset 0 0 0 1px #e0c77b,

  0 2px 3px rgba(80, 60, 25, .22);


          overflow: hidden;


          transition:

            transform .2s ease,

            box-shadow .2s ease;


        }


        .producto-card:hover {


          transform:

            translateY(-4px);


          box-shadow:

            0 8px 20px

            rgba(0,0,0,.12);


        }


        .producto-imagen {


          display: block;


          width: 100%;


          aspect-ratio: 1 / 1;


          background: #ffffff;


          overflow: hidden;


          text-decoration: none;


        }


        .producto-imagen img {


          width: 100%;


          height: 100%;


          object-fit: contain;


          display: block;


        }


        .sin-foto {


          width: 100%;


          height: 100%;


          display: flex;


          align-items: center;


          justify-content: center;


          color: #887766;


          font-size: 14px;


        }


        .producto-info {


  padding: 14px 15px 18px;


  border-top: 1px solid #b9a27a;


}


        .producto-info h2 {


          margin: 0 0 10px;


          font-size: 18px;


          line-height: 1.3;


          font-weight: normal;


          min-height: 47px;


        }


        .producto-precio {


          font-size: 21px;


          font-weight: bold;


          margin-bottom: 15px;


        }


        .producto-boton {


          display: inline-block;


          padding: 9px 16px;


          background: #7a4b3f;


          color: #f3ead7;


          border: 1px solid #b58a43;


          text-decoration: none;


          font-size: 13px;


          letter-spacing: .5px;


          transition:

            background .2s ease;


        }


        .producto-boton:hover {


          background: #9b9270;


          color: #fff;


        }


        @media (max-width: 900px) {


          .productos-grid {


            grid-template-columns:

              repeat(2, 1fr);


            gap: 18px 12px;


          }


          .catalogo-titulo h1 {


            font-size: 30px;


          }


.catalogo-logo {


  width: 48px;


  height: 48px;


}




.marca-catalogo {


  flex-direction: column;


  gap: 4px;


}


.marca-catalogo .catalogo-logo {


  width: 48px;


  height: 48px;


}


.catalogo-titulo p {

  font-size: 14px;

  line-height: 1;

}


          .producto-info h2 {


            font-size: 16px;


          }


          .producto-precio {


            font-size: 18px;


          }


        }


      </style>


    </head>


    <body>


  <div class="zona-navegacion">

 <button class="menu-boton" type="button" aria-label="Abrir menú">

  <span></span>

  <span></span>

 </button>


 <nav class="menu-desplegable">

  <a href="/">INICIO</a>

  <a href="/catalogo">CATÁLOGO</a>

  <a href="/p/archivo-fotografico-de-1000-antiguedades.html">ARCHIVO FOTOGRÁFICO</a>

  <a href="https://wa.me/573183853435" target="_blank">WHATSAPP</a>

 </nav>

</div>


  <main class="catalogo-contenedor">


        <header class="catalogo-titulo">


  <h1>


    <header class="catalogo-titulo">


  <a

  class="marca-catalogo"

  href="/"

  aria-label="1000 Antigüedades — Inicio"

>


    <img

      class="catalogo-logo"

      src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjU6A7bAVT2SfsHJztJSDH6ZpGnW-JbcSlpF8Q02T5NCwf-wJaZ4qovnS4dTRQPwYz639E6jYq3m0TY3GQFVRfQwBDelnmsp2JuOqBx-2kODpLr2YQN9IdrtDV0BzoULUOl8S8SE6U-UHKHWSIypbtbo_eeYxU3HLn4dPTFnHeB5EL5n15RMkC-aic-kPg/s1254/2026-08%20LOGO%20REDONDO%20-%20sin%20fondo.png"

      alt="1000 Antigüedades"

    >


    <span>

      1000 ANTIGÜEDADES

    </span>


  </a>


  <p>

    En cada pieza, una historia

  </p>


</header>


<form

  class="buscador"

  method="GET"

  action="/catalogo"

>


  <input

    type="search"

    name="q"

    placeholder="Buscar una pieza..."

  >


  <button type="submit">

    BUSCAR

  </button>


</form>


<nav class="filtros-categorias">


  <a

    href="/catalogo"

    class="${

      !categoria

        ? "activo"

        : ""

    }"

  >

    TODAS

  </a>


  <a

    href="/catalogo?categoria=Antigüedades"

    class="${

      categoria === "Antigüedades"

        ? "activo"

        : ""

    }"

  >

    ANTIGÜEDADES

  </a>


  <a

    href="/catalogo?categoria=Retro"

    class="${

      categoria === "Retro"

        ? "activo"

        : ""

    }"

  >

    RETRO

  </a>


  <a

    href="/catalogo?categoria=Colecciones"

    class="${

      categoria === "Colecciones"

        ? "activo"

        : ""

    }"

  >

    COLECCIONES

  </a>


  <a

    href="/catalogo?categoria=Otros%20productos"

    class="${

      categoria === "Otros productos"

        ? "activo"

        : ""

    }"

  >

    OTROS PRODUCTOS

  </a>


</nav>


        <section class="productos-grid">


          ${tarjetas}


        </section>


      </main>


<script>

  const menuBoton = document.querySelector('.menu-boton');

  const menuDesplegable = document.querySelector('.menu-desplegable');


  menuBoton.addEventListener('click', function () {

    menuBoton.classList.toggle('activo');

    menuDesplegable.classList.toggle('abierto');

  });

</script>


    </body>


    </html>`,


    {


      headers: {


        "Content-Type":

          "text/html; charset=utf-8"


      }


    }


  );


}


// =====================================================

// FICHA INDIVIDUAL DEL PRODUCTO

// =====================================================


if (url.pathname.startsWith("/producto/")) {


  // ---------------------------------------------------

  // OBTENER SLUG

  // ---------------------------------------------------


  const slug =

    decodeURIComponent(

      url.pathname.substring(

        "/producto/".length

      )

    );


  if (!slug) {


    return new Response(

      "Producto no especificado.",

      {

        status: 400,

        headers: {

          "Content-Type":

            "text/plain; charset=utf-8"

        }

      }

    );


  }


  // ---------------------------------------------------

  // BUSCAR PRODUCTO EN D1

  // ---------------------------------------------------


  const producto =

    await env.DB.prepare(

      `SELECT

        id,

        ml_id,

        slug,

        titulo,

        precio,

        moneda,

        descripcion,

        condicion,

        categoria_tienda,

        estado_tienda

       FROM productos

       WHERE slug = ?

       LIMIT 1`

    )

      .bind(slug)

      .first();


  if (!producto) {


    return new Response(

      "Producto no encontrado.",

      {

        status: 404,

        headers: {

          "Content-Type":

            "text/plain; charset=utf-8"

        }

      }

    );


  }


  // ---------------------------------------------------

  // OBTENER TODAS LAS FOTOGRAFÍAS

  // ---------------------------------------------------


  const fotosResult =

    await env.DB.prepare(

      `SELECT

        url,

        orden,

        es_principal

       FROM producto_fotos

       WHERE producto_id = ?

       ORDER BY

         es_principal DESC,

         orden ASC`

    )

      .bind(producto.id)

      .all();


  const fotos =

    fotosResult.results || [];


  // ---------------------------------------------------

// CONSTRUIR GALERÍA

// ---------------------------------------------------


const galeria =

  fotos.map(

    (foto, indice) => {


      const imagen =

        `/foto?archivo=${encodeURIComponent(

          foto.url

        )}`;


      return `

        <img

          class="foto-producto ${

            indice === 0

              ? "foto-activa"

              : ""

          }"

          src="${imagen}"

          alt="${escapeHtml(

            producto.titulo

          )} - foto ${indice + 1}"

          loading="${

            indice === 0

              ? "eager"

              : "lazy"

          }"

        >

      `;


    }

  ).join("");

  

  // ---------------------------------------------------

  // PRECIO

  // ---------------------------------------------------


  const precio =

    Number(

      producto.precio || 0

    ).toLocaleString("es-CO");


  // ---------------------------------------------------

  // DESCRIPCIÓN

  // ---------------------------------------------------


  const descripcion =

    escapeHtml(

      producto.descripcion || ""

    ).replace(

      /\n/g,

      "<br>"

    );


  // ---------------------------------------------------

  // PÁGINA

  // ---------------------------------------------------


  return new Response(


    `<!DOCTYPE html>


    <html lang="es">


    <head>


      <meta charset="UTF-8">


      <meta

        name="viewport"

        content="width=device-width, initial-scale=1.0"

      >


      <title>

        ${escapeHtml(

          producto.titulo

        )} — 1000 Antigüedades

      </title>


      <style>


        * {

          box-sizing: border-box;

        }


        body {


          margin: 0;


          background-image: url("https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_5NMDRQvqu-st-f3sRDOjSWvBl89yblLRDUWUIJo4ApZvvWqQ-u1Ln_VqqA4VJZfq0CFOGUC68QowcnU0SPtcYMrZJZbSlRgKW7dPqeSmFkYp_jhoUqJscpDW_Vduvjxgo5DGWyw7BOcn2vcAVe9DhcElaFRE0XYXjF_Zy3PSQfNeRO_4I1U9RMmexk8/s1248/Fondo%20de%20catalogo%20y%20Ficha%20individual.png");


background-size: 100% 100vh;

background-position: center -5px;

background-repeat: no-repeat;

background-attachment: fixed;


          color: #3b2a23;


          font-family:

            Georgia,

            "Times New Roman",

            serif;


        }



        .ficha-header {


  text-align: center;


  padding: 0 15px 10px;


}


.zona-inicio {

  margin-left: 60px;

  margin-top: 23px;

}


.zona-navegacion {

  margin-left: 60px;

  margin-top: 23px;

}


.menu-boton {

  position: absolute;

  right: 78px;

  top: 42px;

  width: 34px;

  height: 28px;

  padding: 0;

  border: none;

  background: transparent;

  cursor: pointer;

}


.menu-boton span {

  display: block;

  width: 34px;

  height: 4px;

  background: #7a4b3f;

  margin: 7px 0 7px auto;

  border-radius: 4px;

  box-shadow: 0 2px 2px rgba(0,0,0,.20);

  transition:

    transform 0.45s ease,

    width 0.45s ease;

  transform-origin: center;

}


.menu-boton span:nth-child(2) {

  width: 27px;

}


.menu-boton.activo span:first-child {

  width: 34px;

  transform: translateY(5.5px) rotate(45deg);

}


.menu-boton.activo span:nth-child(2) {

  width: 34px;

  transform: translateY(-5.5px) rotate(-45deg);

}


.menu-desplegable {

  position: absolute;

  right: 78px;

  top: 78px;

  width: 220px;

  background: rgba(243, 232, 212, 0.20);

  border: 1px solid #b9a27a;

  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);

  border-radius: 14px;

  z-index: 20;

  padding: 8px 0;

  opacity: 0;

  visibility: hidden;

  pointer-events: none;

  transform: translateY(-10px);

  transition:

    opacity 0.4s ease,

    transform 0.5s ease,

    visibility 0.5s ease;

}


.menu-desplegable.abierto {

  opacity: 1;

  visibility: visible;

  pointer-events: auto;

  transform: translateY(0);

}


.menu-desplegable a {

  display: block;

}


.zona-navegacion {

  margin-left: 60px;

  margin-top: 23px;

}


.menu-boton {


  position: absolute;


  right: 78px;


  top: 42px;


  width: 34px;


  height: 28px;


  padding: 0;


  border: none;


  background: transparent;


  cursor: pointer;


}


.menu-boton span {


  display: block;


  width: 34px;


  height: 4px;


  background: #7a4b3f;


  margin: 7px 0 7px auto;


  border-radius: 4px;


  box-shadow: 0 2px 2px rgba(0,0,0,.20);


  transition:


    transform 0.45s ease,


    width 0.45s ease;


  transform-origin: center;


}


.menu-boton span:nth-child(2) {


  width: 27px;


}


.menu-boton.activo span:first-child {


  width: 34px;


  transform: translateY(5.5px) rotate(45deg);


}


.menu-boton.activo span:nth-child(2) {


  width: 34px;


  transform: translateY(-5.5px) rotate(-45deg);


}


.menu-desplegable {


  position: absolute;


  right: 78px;


  top: 78px;


  width: 220px;


  background: rgba(243, 232, 212, 0.20);


  border: 1px solid #b9a27a;


  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);


  border-radius: 14px;


  z-index: 20;


  padding: 8px 0;


  opacity: 0;


  visibility: hidden;


  pointer-events: none;


  transform: translateY(-10px);


  transition:


    opacity 0.4s ease,


    transform 0.5s ease,


    visibility 0.5s ease;


}


.menu-desplegable.abierto {


  opacity: 1;


  visibility: visible;


  pointer-events: auto;


  transform: translateY(0);


}


.menu-desplegable a {


  display: block;


  padding: 12px 18px;


  color: #7a4b3f;


  font-family: Georgia, "Times New Roman", serif;


  font-size: 15px;


  text-decoration: none;


  opacity: 0;


  transform: translateY(-8px);


  transition:


    opacity 0.35s ease,


    transform 0.35s ease;


}


.menu-desplegable.abierto a {


  opacity: 1;


  transform: translateY(0);


}


.menu-desplegable.abierto a:nth-child(1) {


  transition-delay: 0.10s;


}


.menu-desplegable.abierto a:nth-child(2) {


  transition-delay: 0.18s;


}


.menu-desplegable.abierto a:nth-child(3) {


  transition-delay: 0.26s;


}


.menu-desplegable.abierto a:nth-child(4) {


  transition-delay: 0.34s;


}


.menu-desplegable a:hover {


  background: #ead9bd;


}


.ficha-marca {


  display: inline-flex;


  align-items: center;


  justify-content: center;


  gap: 12px;


  font-size: 42px;


  letter-spacing: 2px;


  font-weight: normal;


  font-family: Cambria, serif;


  color: #3b2a23;


  text-decoration: none;


}


.ficha-marca:hover {


  color: #7a4b3f;


}


.ficha-logo {


  width: 62px;


  height: 62px;


  object-fit: contain;


  display: block;


}


.ficha-subtitulo {

  font-size: 17.3px;

  font-family: inherit;

  font-weight: normal;

  letter-spacing: normal;

  opacity: .8;

}


        .producto-contenedor {


          width: 94%;


          max-width: 1400px;


          margin: 0 auto;


          padding: 25px 0 60px;


        }


        .volver {


          display: inline-block;


          margin-bottom: 0px;


          color: #7a4b3f;


          text-decoration: none;


          font-size: 15px;


        }


        .volver:hover {


          text-decoration: underline;


        }


        .producto-layout {

  display: flex;

  flex-direction: column;

  align-items: center;

  gap: 35px;

}

        .galeria {


  display: block;

  position: relative;


}


.galeria {

  width: 100%;

  max-width: 370px;

  margin: 0 auto;

}


.carrusel-fotos {

  position: relative;

}


.flecha-carrusel {

  position: absolute;

  top: 50%;

  transform: translateY(-50%);

  z-index: 10;

}


.flecha-anterior {

  left: 10%;

}


.flecha-siguiente {

  right: 10%;

}

  

.galeria {

  position: relative;

  overflow: visible;

}


.indicadores-carrusel {

  display: flex;

  justify-content: center;

  align-items: center;

  gap: 8px;

  margin-top: 12px;

}


.indicador-carrusel {

  width: 11px;

  height: 11px;


  padding: 0;

  margin: 0;


  border-radius: 50%;

  border: 1px solid #8b745c;


  background: radial-gradient(

    circle at 35% 30%,

    #f0dfc5 0%,

    #c6aa83 35%,

    #8b6f50 70%,

    #5f4934 100%

  );


  box-shadow:

    inset 1px 1px 2px rgba(255,255,255,.65),

    inset -1px -1px 2px rgba(60,40,20,.55),

    0 1px 2px rgba(0,0,0,.35);


  cursor: pointer;


  transition:

    transform .15s ease,

    box-shadow .15s ease;

}


.indicador-carrusel:hover {

  transform: scale(1.25);

}


.indicador-carrusel.activo {

  width: 13px;

  height: 13px;


  border-color: #4a3626;


  background: radial-gradient(

    circle at 35% 30%,

    #b99a72 0%,

    #806143 35%,

    #513a27 70%,

    #302116 100%

  );


  box-shadow:

    inset 1px 1px 2px rgba(255,255,255,.45),

    inset -2px -2px 3px rgba(25,15,8,.75),

    0 1px 3px rgba(0,0,0,.5);

}


.carrusel-fotos {

  position: relative;

}


.flecha-carrusel {

  position: absolute;

  top: 73%;

  filter: drop-shadow(2px 3px 3px rgba(60, 60, 60, .45));

  transform: translateY(-50%);

  z-index: 10;


width: 75px;

height: 75px;


  padding: 0;

  margin: 0;


  border: none;

  background-color: transparent;

  background-repeat: no-repeat;

  background-size: 100% 100%;

  background-position: center;


  cursor: pointer;

  font-size: 0;

}


/* FLECHA IZQUIERDA */

.flecha-anterior {

  left: -66px;


  background-image: url("https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhSGC9m2qw4IIBGcPclrcflgOn_iUnUWTHtoJL8RnX3wMTFJkMyj6PS7DZLLsQyuiUA_5G_pG5z9z-YQPB6cpLKLrJU21JH3_kM4EHr6jq8s0R34bBqG4zZCTlHlMZ8CRZ8-8d-Q002Olq3P5123O_GNRhRkYcuta4dIiQ0Y_eE7u-Nc4vHT3pWSYA12CA/s744/flecha%20editada%20izq.png");

}


/* FLECHA DERECHA */

.flecha-siguiente {

  right: -66px;


  background-image: url("https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgcVsazl3idsxnbOmE31fT68PfheJVC4efpnJG4zQV7v4-OA1MeccgpOTXPKt3phjOAhhxu-fattN8BeAbT4OnlM3ofXPCIPkAD9iEcKaI0Pi1cRtMPu-ujhkK4BEskL3PrfNzNGXyd7iM6OhmL8S5GVxgD4Fm8ztFFfuedg0Plb9M4EDkhghonTO_n4wA/s744/flecha%20editada%20der.png");

}


        .foto-producto {


          width: 100%;


          aspect-ratio: 1 / 1;


          object-fit: contain;


          display: block;


          background: #fffdf8;


          border: 1px solid #c3a45b;

box-shadow:

  inset 0 0 0 1px #e0c77b,

  0 7px 16px rgba(70, 70, 70, .60);


        }


        .foto-producto {

  display: none;

  opacity: 0;

}


.foto-producto.foto-activa {

  display: block;

  opacity: 1;

  animation: fundido-foto .25s ease;

}


@keyframes fundido-foto {

  from {

    opacity: 0;

  }


  to {

    opacity: 1;

  }

}


        .informacion {


          background: #F3E8D4;


          border: 1px solid #b9a27a;


          padding: 30px;


          position: sticky;


          top: 20px;

          box-shadow: 0 6px 18px rgba(0, 0, 0, 0.42);


        }


        .informacion h1 {


          margin: 0 0 18px;


          font-size: 32px;


          line-height: 1.2;


          font-weight: normal;


        }


        .precio {


          font-size: 30px;


          font-weight: bold;


          margin: 20px 0;


        }


        .categoria {


          font-size: 14px;


          text-transform: uppercase;


          letter-spacing: 1px;


          color: #806957;


          margin-bottom: 20px;


        }


        .descripcion {


          border-top: 1px solid #d8c9b2;


          padding-top: 20px;


          font-size: 17px;


          line-height: 1.65;


        }


        .comprar {


  display: block;


  margin-top: 25px;


  padding: 14px 20px;


  background: #315442;


  color: #f3ead7;


  border: 1px solid #315442;


  text-decoration: none;


  font-size: 14px;


  letter-spacing: .5px;


  text-align: center;


  transition: background .2s ease;


}



        .comprar:hover {


  background: #25D366;


}


        @media (max-width: 800px) {

 

        


  .producto-layout {

    display: flex;

    flex-direction: column;

    align-items: center;

    gap: 25px;

    width: 100%;

  }



  

  .galeria {

    width: 100%;

    max-width: 100%;

    margin: 0 auto;

  }


  .carrusel-fotos {

    width: 100%;

  }


  .foto-producto {

    width: 100%;

    aspect-ratio: 1 / 1;

    object-fit: contain;

  }


  .flecha-carrusel {

  width: 52px;

  height: 52px;

  top: 88%;

}


.flecha-anterior {

  left: -5px;

}


.flecha-siguiente {

  right: -5px;

}


  .indicadores-carrusel {

    margin-top: 10px;

  }


  .informacion {

    position: static;

    width: 100%;

    box-sizing: border-box;

    padding: 22px;

  }


  .informacion h1 {

    font-size: 26px;

  }


  .precio {

    font-size: 25px;

  }


  .ficha-marca {

    display: flex !important;

    flex-direction: column !important;

    align-items: center !important;

    gap: 4px !important;

    font-size: 30px !important;

  }


  .ficha-logo {

    width: 48px !important;

    height: 48px !important;

  }


  .ficha-subtitulo {

    font-size: 14px !important;

  }


}


.enlace-whatsapp {

  color: #7a4b3f !important;

  font-family: Georgia, "Times New Roman", serif;

  font-size: 15px;

  text-decoration: none;

}


.enlace-whatsapp:hover {

  text-decoration: underline;

}


.ficha-enlace-inicio {

  color: #7a4b3f !important;

  font-family: Georgia, "Times New Roman", serif;

  font-size: 15px;

  text-decoration: none;

}


.ficha-enlace-inicio:hover {

  text-decoration: underline;

}


      </style>


    </head>


    <body>


<div class="zona-navegacion">


  <button class="menu-boton" type="button" aria-label="Abrir menú">

  <span></span>

  <span></span>

</button>


<nav class="menu-desplegable">

  <a href="/">INICIO</a>

  <a href="/catalogo">CATÁLOGO</a>

  <a href="/p/archivo-fotografico-de-1000-antiguedades.html">ARCHIVO FOTOGRÁFICO</a>

  <a href="https://wa.me/573183853435" target="_blank">WHATSAPP</a>

</nav>


</div>


<header class="ficha-header">


<a

  class="ficha-marca"


  href="/"

  aria-label="1000 Antigüedades — Inicio"

>


    <img

      class="ficha-logo"

      src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjU6A7bAVT2SfsHJztJSDH6ZpGnW-JbcSlpF8Q02T5NCwf-wJaZ4qovnS4dTRQPwYz639E6jYq3m0TY3GQFVRfQwBDelnmsp2JuOqBx-2kODpLr2YQN9IdrtDV0BzoULUOl8S8SE6U-UHKHWSIypbtbo_eeYxU3HLn4dPTFnHeB5EL5n15RMkC-aic-kPg/s1254/2026-08%20LOGO%20REDONDO%20-%20sin%20fondo.png"

      alt="1000 Antigüedades"

    >


    <span>

      1000 ANTIGÜEDADES

    </span>


  </a>


  <div class="ficha-subtitulo">

    En cada pieza, una historia

  </div>


</header>


      <main

        class="producto-contenedor"

      >


        


        <div

          class="producto-layout"

        >


          <section class="galeria">


  <button

    class="flecha-carrusel flecha-anterior"

    type="button"

    aria-label="Foto anterior"

  >

    ←

  </button>


  <div class="carrusel-fotos">


  ${galeria}


</div>


<div class="indicadores-carrusel"></div>


  <button

    class="flecha-carrusel flecha-siguiente"

    type="button"

    aria-label="Foto siguiente"

  >

    →

  </button>


</section>


          <aside

            class="informacion"

          >


            <div

              class="categoria"

            >

              ${escapeHtml(

                producto.categoria_tienda

                || "Antigüedades"

              )}

            </div>


            <h1>

              ${escapeHtml(

                producto.titulo

              )}

            </h1>


            <div

              class="precio"

            >

              $ ${precio}

              ${escapeHtml(

                producto.moneda || "COP"

              )}

            </div>


            <div

              class="descripcion"

            >


              ${descripcion}


            </div>


            <a

  class="comprar"

  href="https://wa.me/573183853435?text=${encodeURIComponent(

    "Hola, estoy interesado en la pieza: " +

    producto.titulo +

    "\n" +

    "https://www.1000antiguedades.com/producto/" +

    producto.slug

  )}"

  target="_blank"

>

  CONSULTAR POR WHATSAPP

</a>


          </aside>


        </div>


      </main>

      

<script>

  const fotosCarrusel =

    document.querySelectorAll(".foto-producto");


  const botonAnterior =

    document.querySelector(".flecha-anterior");


  const botonSiguiente =

    document.querySelector(".flecha-siguiente");


  const indicadores =

    document.querySelector(".indicadores-carrusel");


  let indiceActual = 0;


let inicioX = 0;

let inicioY = 0;


const zonaDeslizamiento =

  document.querySelector(".carrusel-fotos");


zonaDeslizamiento.addEventListener(

  "touchstart",

  (evento) => {


    inicioX = evento.touches[0].clientX;

    inicioY = evento.touches[0].clientY;


  },

  { passive: true }

);


zonaDeslizamiento.addEventListener(

  "touchend",

  (evento) => {


    const finalX =

      evento.changedTouches[0].clientX;


    const finalY =

      evento.changedTouches[0].clientY;


    const diferenciaX =

      finalX - inicioX;


    const diferenciaY =

      finalY - inicioY;


    /* SOLO ACTUAR SI EL GESTO ES

       PRINCIPALMENTE HORIZONTAL */


    if (

      Math.abs(diferenciaX) > 50 &&

      Math.abs(diferenciaX) > Math.abs(diferenciaY)

    ) {


      if (diferenciaX < 0) {


        mostrarFoto(indiceActual + 1);


      } else {


        mostrarFoto(indiceActual - 1);


      }


    }


  },

  { passive: true }

);


  /* CREAR UN PUNTO POR CADA FOTO */

  fotosCarrusel.forEach((foto, indice) => {


    const punto =

      document.createElement("button");


    punto.type = "button";


    punto.className =

      "indicador-carrusel";


    punto.setAttribute(

      "aria-label",

      "Ir a foto " + (indice + 1)

    );


    punto.addEventListener(

      "click",

      () => {

        mostrarFoto(indice);

      }

    );


    indicadores.appendChild(punto);

  });


  const puntos =

    document.querySelectorAll(

      ".indicador-carrusel"

    );


  function mostrarFoto(indice) {


    if (!fotosCarrusel.length) return;


    fotosCarrusel[

      indiceActual

    ].classList.remove("foto-activa");


    indiceActual =

      (indice + fotosCarrusel.length)

      % fotosCarrusel.length;


    fotosCarrusel[

      indiceActual

    ].classList.add("foto-activa");


    /* ACTUALIZAR PUNTO ACTIVO */

    puntos.forEach((punto, i) => {


      punto.classList.toggle(

        "activo",

        i === indiceActual

      );


    });

  }


  botonAnterior.addEventListener(

    "click",

    () => {

      mostrarFoto(indiceActual - 1);

    }

  );


  botonSiguiente.addEventListener(

    "click",

    () => {

      mostrarFoto(indiceActual + 1);

    }

  );


  /* MARCAR LA PRIMERA FOTO */

  if (puntos.length) {

    puntos[0].classList.add("activo");

  }


const menuBoton = document.querySelector('.menu-boton');

const menuDesplegable = document.querySelector('.menu-desplegable');


menuBoton.addEventListener('click', function () {

  menuBoton.classList.toggle('activo');

  menuDesplegable.classList.toggle('abierto');

});


</script>

    </body>


    </html>`,


    {


      headers: {


        "Content-Type":

          "text/html; charset=utf-8"


      }


    }


  );


}


// =====================================================

// INVENTARIO COMPLETO DE MERCADO LIBRE

// =====================================================


if (url.pathname === "/inventario") {


  // Leer tokens


  const tokenData =

    await env.ML_TOKENS.get(

      "oauth_tokens",

      "json"

    );


  if (!tokenData) {


    return new Response(

      "No hay tokens guardados. Primero autoriza Mercado Libre.",

      {

        status: 401,

        headers: {

          "Content-Type":

            "text/plain; charset=utf-8"

        }

      }

    );

  }


  let accessToken =

    tokenData.access_token;


  const userId =

    tokenData.user_id;


  // ===================================================

  // RENOVAR TOKEN SI ESTÁ VENCIDO

  // ===================================================


  const obtenidoHace =

    Date.now() - (tokenData.obtained_at || 0);


  const venceEn =

    (tokenData.expires_in || 21600) * 1000;


  if (obtenidoHace >= venceEn) {


    const refreshBody =

      new URLSearchParams();


    refreshBody.set(

      "grant_type",

      "refresh_token"

    );


    refreshBody.set(

      "client_id",

      env.ML_CLIENT_ID

    );


    refreshBody.set(

      "client_secret",

      env.ML_CLIENT_SECRET

    );


    refreshBody.set(

      "refresh_token",

      tokenData.refresh_token

    );


    const refreshResponse =

      await fetch(

        "https://api.mercadolibre.com/oauth/token",

        {

          method: "POST",


          headers: {

            "Accept":

              "application/json",


            "Content-Type":

              "application/x-www-form-urlencoded"

          },


          body: refreshBody

        }

      );


    const refreshData =

      await refreshResponse.json();


    if (!refreshResponse.ok) {


      return new Response(

        JSON.stringify(

          refreshData,

          null,

          2

        ),

        {

          status:

            refreshResponse.status,


          headers: {

            "Content-Type":

              "application/json"

          }

        }

      );

    }


    accessToken =

      refreshData.access_token;


    await env.ML_TOKENS.put(

      "oauth_tokens",

      JSON.stringify({


        access_token:

          refreshData.access_token,


        refresh_token:

          refreshData.refresh_token ||

          tokenData.refresh_token,


        expires_in:

          refreshData.expires_in,


        user_id:

          refreshData.user_id ||

          tokenData.user_id,


        obtained_at:

          Date.now()

      })

    );

  }


  // ===================================================

  // RECORRER TODAS LAS PUBLICACIONES

  // ===================================================


  const publicaciones = [];


  let offset = 0;


  const limit = 50;


  let total = null;


  while (true) {


    const searchUrl =

      new URL(

        `https://api.mercadolibre.com/users/${userId}/items/search`

      );


    searchUrl.searchParams.set(

      "limit",

      String(limit)

    );


    searchUrl.searchParams.set(

      "offset",

      String(offset)

    );


    const searchResponse =

      await fetch(

        searchUrl.toString(),

        {

          headers: {

            "Authorization":

              `Bearer ${accessToken}`

          }

        }

      );


    const searchData =

      await searchResponse.json();


    if (!searchResponse.ok) {


      return new Response(

        JSON.stringify(

          searchData,

          null,

          2

        ),

        {

          status:

            searchResponse.status,


          headers: {

            "Content-Type":

              "application/json"

          }

        }

      );

    }


    // Guardar total informado por Mercado Libre


    if (total === null) {


      total =

        searchData.paging?.total || 0;

    }


    const resultados =

      searchData.results || [];


    publicaciones.push(

      ...resultados

    );


    // =================================================

    // COMPROBAR SI YA TERMINAMOS

    // =================================================


    if (

      resultados.length === 0 ||

      publicaciones.length >= total

    ) {

      break;

    }


    offset += limit;

  }


  // ===================================================

  // DEVOLVER INVENTARIO

  // ===================================================


  return new Response(


    JSON.stringify(

      {

        usuario:

          userId,


        total_reportado_por_mercado_libre:

          total,


        publicaciones_encontradas:

          publicaciones.length,


        ids:

          publicaciones

      },

      null,

      2

    ),


    {

      headers: {

        "Content-Type":

          "application/json; charset=utf-8"

      }

    }

  );

}


// =====================================================

// PRUEBA DE IMPORTACIÓN — 5 PRODUCTOS

// =====================================================


if (url.pathname === "/prueba-importacion") {


  const tokenData =

    await env.ML_TOKENS.get(

      "oauth_tokens",

      "json"

    );


  if (!tokenData) {


    return new Response(

      "No hay tokens guardados.",

      {

        status: 401

      }

    );

  }


  const accessToken =

    tokenData.access_token;


  const userId =

    tokenData.user_id;


  // ===================================================

  // OBTENER LOS PRIMEROS 5 IDS

  // ===================================================


  const searchUrl =

    new URL(

      `https://api.mercadolibre.com/users/${userId}/items/search`

    );


  searchUrl.searchParams.set(

    "limit",

    "5"

  );


  searchUrl.searchParams.set(

    "offset",

    "0"

  );


  const searchResponse =

    await fetch(

      searchUrl.toString(),

      {

        headers: {

          "Authorization":

            `Bearer ${accessToken}`

        }

      }

    );


  const searchData =

    await searchResponse.json();


  if (!searchResponse.ok) {


    return new Response(

      JSON.stringify(

        searchData,

        null,

        2

      ),

      {

        status:

          searchResponse.status,


        headers: {

          "Content-Type":

            "application/json"

        }

      }

    );

  }


  const ids =

    searchData.results || [];


  // ===================================================

  // OBTENER DATOS DE LOS 5 PRODUCTOS

  // ===================================================


  const productos =

    await Promise.all(


      ids.map(async id => {


        // -----------------------------------------------

        // DATOS DEL PRODUCTO

        // -----------------------------------------------


        const itemResponse =

          await fetch(

            `https://api.mercadolibre.com/items/${id}`,

            {

              headers: {

                "Authorization":

                  `Bearer ${accessToken}`

              }

            }

          );


        const item =

          await itemResponse.json();


        // -----------------------------------------------

        // DESCRIPCIÓN

        // -----------------------------------------------


        let descripcion = "";


        const descripcionResponse =

          await fetch(

            `https://api.mercadolibre.com/items/${id}/description`,

            {

              headers: {

                "Authorization":

                  `Bearer ${accessToken}`

              }

            }

          );


        if (descripcionResponse.ok) {


          const descripcionData =

            await descripcionResponse.json();


          descripcion =

            descripcionData.plain_text || "";

        }


        // -----------------------------------------------

        // FOTOGRAFÍAS

        // -----------------------------------------------


        const fotos =

          (item.pictures || [])

            .map(picture =>

              picture.secure_url

            )

            .filter(Boolean);


        // -----------------------------------------------

        // PRODUCTO

        // -----------------------------------------------


        return {


          id:

            item.id,


          titulo:

            item.title,


          precio:

            item.price,


          moneda:

            item.currency_id,


          estado_mercadolibre:

            item.status,


          condicion:

            item.condition,


          categoria_mercadolibre:

            item.category_id,


          fotos:

            fotos,


          cantidad_fotos:

            fotos.length,


          descripcion:

            descripcion,


          origen:

            "mercadolibre"

        };


      })

    );


  // ===================================================

  // RESULTADO

  // ===================================================


  return new Response(


    JSON.stringify(

      {

        total_prueba:

          productos.length,


        productos:

          productos

      },

      null,

      2

    ),


    {

      headers: {

        "Content-Type":

          "application/json; charset=utf-8"

      }

    }

  );

}


// =====================================================

// IMPORTACIÓN CONTROLADA — 5 PRODUCTOS

// REANUDABLE — NO DUPLICA FOTOS

// =====================================================


if (url.pathname === "/importar-5") {


  // ---------------------------------------------------

  // LEER TOKENS

  // ---------------------------------------------------


  const tokenData = await env.ML_TOKENS.get(

    "oauth_tokens",

    "json"

  );


  if (!tokenData) {

    return new Response(

      "No hay tokens guardados. Primero autoriza Mercado Libre.",

      { status: 401 }

    );

  }


  let accessToken = tokenData.access_token;

  const userId = tokenData.user_id;


  // ---------------------------------------------------

  // RENOVAR TOKEN SI ESTÁ VENCIDO

  // ---------------------------------------------------


  const obtenidoHace =

    Date.now() - (tokenData.obtained_at || 0);


  const venceEn =

    (tokenData.expires_in || 21600) * 1000;


  if (obtenidoHace >= venceEn) {


    const refreshBody = new URLSearchParams();


    refreshBody.set(

      "grant_type",

      "refresh_token"

    );


    refreshBody.set(

      "client_id",

      env.ML_CLIENT_ID

    );


    refreshBody.set(

      "client_secret",

      env.ML_CLIENT_SECRET

    );


    refreshBody.set(

      "refresh_token",

      tokenData.refresh_token

    );


    const refreshResponse = await fetch(

      "https://api.mercadolibre.com/oauth/token",

      {

        method: "POST",


        headers: {

          "Accept": "application/json",

          "Content-Type":

            "application/x-www-form-urlencoded"

        },


        body: refreshBody

      }

    );


    const refreshData =

      await refreshResponse.json();


    if (!refreshResponse.ok) {


      return new Response(

        JSON.stringify(

          refreshData,

          null,

          2

        ),

        {

          status: refreshResponse.status,


          headers: {

            "Content-Type":

              "application/json"

          }

        }

      );

    }


    accessToken =

      refreshData.access_token;


    await env.ML_TOKENS.put(

      "oauth_tokens",


      JSON.stringify({


        access_token:

          refreshData.access_token,


        refresh_token:

          refreshData.refresh_token ||

          tokenData.refresh_token,


        expires_in:

          refreshData.expires_in,


        user_id:

          refreshData.user_id ||

          tokenData.user_id,


        obtained_at:

          Date.now()


      })

    );

  }


  // ---------------------------------------------------

  // OBTENER 5 PRODUCTOS DE MERCADO LIBRE

  // ---------------------------------------------------


  const searchUrl = new URL(

    `https://api.mercadolibre.com/users/${userId}/items/search`

  );


  searchUrl.searchParams.set(

    "limit",

    "20"

  );


  searchUrl.searchParams.set(

    "offset",

    "0"

  );


  const searchResponse = await fetch(

    searchUrl.toString(),

    {

      headers: {

        "Authorization":

          `Bearer ${accessToken}`

      }

    }

  );


  const searchData =

    await searchResponse.json();


  if (!searchResponse.ok) {


    return new Response(

      JSON.stringify(

        searchData,

        null,

        2

      ),

      {

        status:

          searchResponse.status,


        headers: {

          "Content-Type":

            "application/json"

        }

      }

    );

  }


  const ids =

    searchData.results || [];


  // ---------------------------------------------------

  // CONTROL DE DESCARGA

  // MÁXIMO 20 FOTOS NUEVAS POR EJECUCIÓN

  // ---------------------------------------------------


  const MAX_FOTOS_POR_EJECUCION = 20;


  let fotosNuevasTotales = 0;

  let bytesNuevosTotales = 0;


  const resultados = [];


  // ---------------------------------------------------

  // PROCESAR PRODUCTOS UNO POR UNO

  // ---------------------------------------------------


  for (const mlId of ids) {


    try {


      // -----------------------------------------------

      // COMPROBAR SI YA EXISTE EN D1

      // -----------------------------------------------


      const existente =

        await env.DB.prepare(

          "SELECT id, titulo FROM productos WHERE ml_id = ?"

        )

          .bind(mlId)

          .first();


      let productoId = null;

      let productoNuevo = false;


      // -----------------------------------------------

      // SI YA EXISTE

      // -----------------------------------------------


      if (existente) {


        productoId =

          existente.id;


      }


      // -----------------------------------------------

      // OBTENER DATOS ACTUALES DE MERCADO LIBRE

      // -----------------------------------------------


      const itemResponse = await fetch(

        `https://api.mercadolibre.com/items/${mlId}`,

        {

          headers: {

            "Authorization":

              `Bearer ${accessToken}`

          }

        }

      );


      const item =

        await itemResponse.json();


      if (!itemResponse.ok) {


        resultados.push({

          ml_id: mlId,

          estado: "error_producto",

          detalle: item

        });


        continue;

      }


      // -----------------------------------------------

      // DESCRIPCIÓN

      // -----------------------------------------------


      let descripcion = "";


      try {


        const descripcionResponse =

          await fetch(

            `https://api.mercadolibre.com/items/${mlId}/description`,

            {

              headers: {

                "Authorization":

                  `Bearer ${accessToken}`

              }

            }

          );


        if (descripcionResponse.ok) {


          const descripcionData =

            await descripcionResponse.json();


          descripcion =

            descripcionData.plain_text || "";

        }


      } catch (error) {


        descripcion = "";

      }


      // -----------------------------------------------

      // SI EL PRODUCTO NO EXISTE, CREARLO

      // -----------------------------------------------


      if (!existente) {


        const slugBase =

          (item.title || mlId)

            .toLowerCase()

            .normalize("NFD")

            .replace(/[\u0300-\u036f]/g, "")

            .replace(/[^a-z0-9]+/g, "-")

            .replace(/^-+|-+$/g, "");


        const slug =

          `${slugBase}-${mlId.toLowerCase()}`;


        const insertProducto =

          await env.DB.prepare(

            `INSERT INTO productos

            (

              ml_id,

              slug,

              titulo,

              precio,

              moneda,

              descripcion,

              condicion,

              categoria_ml,

              categoria_tienda,

              estado_ml,

              estado_tienda,

              origen

            )

            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`

          )

            .bind(


              item.id,


              slug,


              item.title || "",


              item.price || null,


              item.currency_id || "COP",


              descripcion,


              item.condition || null,


              item.category_id || null,


              "Antigüedades",


              item.status || null,


              "disponible",


              "mercadolibre"


            )

            .run();


        productoId =

          insertProducto.meta.last_row_id;


        productoNuevo = true;

      }


      // -----------------------------------------------

      // FOTOGRAFÍAS DE MERCADO LIBRE

      // SOLO pictures[]

      // NO VIDEOS

      // -----------------------------------------------


      const fotos =

        (item.pictures || [])

          .map(

            picture =>

              picture.secure_url

          )

          .filter(Boolean);


      // -----------------------------------------------

      // CONSULTAR FOTOS YA REGISTRADAS EN D1

      // -----------------------------------------------


      const fotosExistentesResult =

        await env.DB.prepare(

          `SELECT orden, url

           FROM producto_fotos

           WHERE producto_id = ?

           ORDER BY orden`

        )

          .bind(productoId)

          .all();


      const fotosExistentes =

        fotosExistentesResult.results || [];


      const ordenesExistentes =

        new Set(

          fotosExistentes.map(

            foto =>

              Number(foto.orden)

          )

        );


      const cantidadAntes =

        fotosExistentes.length;


      let fotosGuardadasEnEstaEjecucion = 0;

      let bytesProducto = 0;


      // -----------------------------------------------

      // DESCARGAR SOLO LAS FOTOS QUE FALTAN

      // -----------------------------------------------


      for (

        let i = 0;

        i < fotos.length;

        i++

      ) {


        // Si ya tenemos demasiadas fotos en esta ejecución,

        // dejamos que la siguiente ejecución continúe.


        if (

          fotosNuevasTotales >=

          MAX_FOTOS_POR_EJECUCION

        ) {

          break;

        }


        // ---------------------------------------------

        // SI ESTA FOTO YA EXISTE, NO LA DESCARGAMOS

        // ---------------------------------------------


        if (

          ordenesExistentes.has(i)

        ) {

          continue;

        }


        const fotoUrl =

          fotos[i];


        try {


          const fotoResponse =

            await fetch(fotoUrl);


          if (!fotoResponse.ok) {

            continue;

          }


          const contenido =

            await fotoResponse.arrayBuffer();


          const contentType =

            fotoResponse.headers.get(

              "content-type"

            ) || "image/jpeg";


          const extension =

            contentType.includes("png")

              ? "png"

              : contentType.includes("webp")

                ? "webp"

                : "jpg";


          const nombreObjeto =

            `productos/${mlId}/foto-${i + 1}.${extension}`;


          // -------------------------------------------

          // GUARDAR EN R2

          // -------------------------------------------


          await env.FOTOS.put(

            nombreObjeto,

            contenido,

            {

              httpMetadata: {

                contentType:

                  contentType

              }

            }

          );


          // -------------------------------------------

          // REGISTRAR EN D1

          // -------------------------------------------


          await env.DB.prepare(

            `INSERT INTO producto_fotos

            (

              producto_id,

              url,

              orden,

              es_principal

            )

            VALUES (?, ?, ?, ?)`

          )

            .bind(


              productoId,


              nombreObjeto,


              i,


              i === 0 ? 1 : 0


            )

            .run();


          // -------------------------------------------

          // CONTADORES

          // -------------------------------------------


          fotosGuardadasEnEstaEjecucion++;


          fotosNuevasTotales++;


          bytesProducto +=

            contenido.byteLength;


          bytesNuevosTotales +=

            contenido.byteLength;


        } catch (error) {


          // Si una foto falla,

          // continuamos con las demás.


        }

      }


      // -----------------------------------------------

      // CANTIDAD FINAL DE FOTOS

      // -----------------------------------------------


      const fotosFinales =

        cantidadAntes +

        fotosGuardadasEnEstaEjecucion;


      // -----------------------------------------------

      // DETERMINAR ESTADO

      // -----------------------------------------------


      let estado;


      if (productoNuevo) {


        estado =

          fotosFinales >= fotos.length

            ? "importado_completo"

            : "importado_parcial";


      } else {


        if (

          fotosFinales >= fotos.length

        ) {


          estado =

            fotosGuardadasEnEstaEjecucion > 0

              ? "fotos_completadas"

              : "ya_existia_completo";


        } else {


          estado =

            fotosGuardadasEnEstaEjecucion > 0

              ? "fotos_completadas_parcial"

              : "ya_existia_pendiente";


        }

      }


      // -----------------------------------------------

      // RESULTADO DEL PRODUCTO

      // -----------------------------------------------


      resultados.push({


        ml_id: mlId,


        estado: estado,


        producto_id: productoId,


        titulo: item.title,


        fotos_encontradas:

          fotos.length,


        fotos_que_ya_existian:

          cantidadAntes,


        fotos_nuevas:

          fotosGuardadasEnEstaEjecucion,


        fotos_totales:

          fotosFinales,


        fotos_pendientes:

          Math.max(

            fotos.length -

            fotosFinales,

            0

          ),


        espacio_bytes:

          bytesProducto,


        espacio_MB:

          (

            bytesProducto /

            1024 /

            1024

          ).toFixed(2)


      });


    } catch (error) {


      resultados.push({


        ml_id: mlId,


        estado: "error",


        mensaje:

          error.message


      });


    }

  }


  // ---------------------------------------------------

  // RESUMEN

  // ---------------------------------------------------


  const espacioTotal =

    bytesNuevosTotales;


  const pendientes =

    resultados.filter(

      r =>

        (r.fotos_pendientes || 0) > 0

    ).length;


  // ---------------------------------------------------

  // RESPUESTA

  // ---------------------------------------------------


  return new Response(


    JSON.stringify(


      {


        limite_prueba:

          5,


        maximo_fotos_por_ejecucion:

          MAX_FOTOS_POR_EJECUCION,


        encontrados:

          ids.length,


        fotos_nuevas_en_esta_ejecucion:

          fotosNuevasTotales,


        productos_con_fotos_pendientes:

          pendientes,


        espacio_total_bytes:

          espacioTotal,


        espacio_total_MB:

          (

            espacioTotal /

            1024 /

            1024

          ).toFixed(2),


        resultados:

          resultados


      },


      null,


      2


    ),


    {


      headers: {


        "Content-Type":

          "application/json; charset=utf-8"


      }


    }


  );

}


// =====================================================

// IMPORTACIÓN MASIVA DE PRODUCTOS — REANUDABLE

// NO DESCARGA FOTOS

// =====================================================


if (url.pathname === "/importar-productos") {


  const tokenData = await env.ML_TOKENS.get(

    "oauth_tokens",

    "json"

  );


  if (!tokenData) {

    return new Response(

      "No hay tokens guardados.",

      { status: 401 }

    );

  }


  let accessToken = tokenData.access_token;

  const userId = tokenData.user_id;


  // ---------------------------------------------------

  // RENOVAR TOKEN SI ESTÁ VENCIDO

  // ---------------------------------------------------


  const obtenidoHace =

    Date.now() - (tokenData.obtained_at || 0);


  const venceEn =

    (tokenData.expires_in || 21600) * 1000;


  if (obtenidoHace >= venceEn) {


    const refreshBody =

      new URLSearchParams();


    refreshBody.set(

      "grant_type",

      "refresh_token"

    );


    refreshBody.set(

      "client_id",

      env.ML_CLIENT_ID

    );


    refreshBody.set(

      "client_secret",

      env.ML_CLIENT_SECRET

    );


    refreshBody.set(

      "refresh_token",

      tokenData.refresh_token

    );


    const refreshResponse =

      await fetch(

        "https://api.mercadolibre.com/oauth/token",

        {

          method: "POST",

          headers: {

            "Accept":

              "application/json",

            "Content-Type":

              "application/x-www-form-urlencoded"

          },

          body: refreshBody

        }

      );


    const refreshData =

      await refreshResponse.json();


    if (!refreshResponse.ok) {


      return new Response(

        JSON.stringify(

          refreshData,

          null,

          2

        ),

        {

          status:

            refreshResponse.status,

          headers: {

            "Content-Type":

              "application/json"

          }

        }

      );

    }


    accessToken =

      refreshData.access_token;


    await env.ML_TOKENS.put(

      "oauth_tokens",

      JSON.stringify({

        access_token:

          refreshData.access_token,


        refresh_token:

          refreshData.refresh_token ||

          tokenData.refresh_token,


        expires_in:

          refreshData.expires_in,


        user_id:

          refreshData.user_id ||

          tokenData.user_id,


        obtained_at:

          Date.now()

      })

    );

  }


  // ---------------------------------------------------

  // LOTE

  // ---------------------------------------------------


  const urlInterna =

    new URL(request.url);


  const offset =

    parseInt(

      urlInterna.searchParams.get(

        "offset"

      ) || "0",

      10

    );


  const limite =

    parseInt(

      urlInterna.searchParams.get(

        "limite"

      ) || "20",

      10

    );


  // ---------------------------------------------------

  // OBTENER PRODUCTOS DE MERCADO LIBRE

  // ---------------------------------------------------


  const searchUrl =

    new URL(

      `https://api.mercadolibre.com/users/${userId}/items/search`

    );


  searchUrl.searchParams.set(

    "limit",

    String(limite)

  );


  searchUrl.searchParams.set(

    "offset",

    String(offset)

  );


  const searchResponse =

    await fetch(

      searchUrl.toString(),

      {

        headers: {

          "Authorization":

            `Bearer ${accessToken}`

        }

      }

    );


  const searchData =

    await searchResponse.json();


  if (!searchResponse.ok) {


    return new Response(

      JSON.stringify(

        searchData,

        null,

        2

      ),

      {

        status:

          searchResponse.status,

        headers: {

          "Content-Type":

            "application/json"

        }

      }

    );

  }


  const ids =

    searchData.results || [];


  // ---------------------------------------------------

  // PROCESAR PRODUCTOS

  // ---------------------------------------------------


  const resultados = [];


  for (const mlId of ids) {


    try {


      // -----------------------------------------------

      // COMPROBAR D1

      // -----------------------------------------------


      const existente =

        await env.DB.prepare(

          "SELECT id, titulo FROM productos WHERE ml_id = ?"

        )

          .bind(mlId)

          .first();


      if (existente) {


        resultados.push({

          ml_id:

            mlId,


          estado:

            "ya_existia",


          producto_id:

            existente.id,


          titulo:

            existente.titulo

        });


        continue;

      }


      // -----------------------------------------------

      // OBTENER PRODUCTO

      // -----------------------------------------------


      const itemResponse =

        await fetch(

          `https://api.mercadolibre.com/items/${mlId}`,

          {

            headers: {

              "Authorization":

                `Bearer ${accessToken}`

            }

          }

        );


      const item =

        await itemResponse.json();


      if (!itemResponse.ok) {


        resultados.push({

          ml_id:

            mlId,


          estado:

            "error_producto",


          detalle:

            item

        });


        continue;

      }


      // -----------------------------------------------

      // DESCRIPCIÓN

      // -----------------------------------------------


      let descripcion = "";


      try {


        const descripcionResponse =

          await fetch(

            `https://api.mercadolibre.com/items/${mlId}/description`,

            {

              headers: {

                "Authorization":

                  `Bearer ${accessToken}`

              }

            }

          );


        if (descripcionResponse.ok) {


          const descripcionData =

            await descripcionResponse.json();


          descripcion =

            descripcionData.plain_text || "";

        }


      } catch (error) {


        descripcion = "";

      }


      // -----------------------------------------------

      // SLUG

      // -----------------------------------------------


      const slugBase =

        (item.title || mlId)

          .toLowerCase()

          .normalize("NFD")

          .replace(

            /[\u0300-\u036f]/g,

            ""

          )

          .replace(

            /[^a-z0-9]+/g,

            "-"

          )

          .replace(

            /^-+|-+$/g,

            "");


      const slug =

        `${slugBase}-${mlId.toLowerCase()}`;


      // -----------------------------------------------

      // INSERTAR EN D1

      // -----------------------------------------------


      const insertProducto =

        await env.DB.prepare(

          `INSERT INTO productos

          (

            ml_id,

            slug,

            titulo,

            precio,

            moneda,

            descripcion,

            condicion,

            categoria_ml,

            categoria_tienda,

            estado_ml,

            estado_tienda,

            origen

          )

          VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`

        )

          .bind(

            item.id,

            slug,

            item.title || "",

            item.price || null,

            item.currency_id || "COP",

            descripcion,

            item.condition || null,

            item.category_id || null,

            "Antigüedades",

            item.status || null,

            "disponible",

            "mercadolibre"

          )

          .run();


      const productoId =

        insertProducto.meta.last_row_id;


      // -----------------------------------------------

      // RESULTADO

      // -----------------------------------------------


      resultados.push({

        ml_id:

          mlId,


        estado:

          "importado",


        producto_id:

          productoId,


        titulo:

          item.title

      });


    } catch (error) {


      resultados.push({

        ml_id:

          mlId,


        estado:

          "error",


        mensaje:

          error.message

      });

    }

  }


  // ---------------------------------------------------

  // RESPUESTA

  // ---------------------------------------------------


  return new Response(

    JSON.stringify(

      {

        offset:

          offset,


        limite:

          limite,


        encontrados:

          ids.length,


        siguiente_offset:

          offset + ids.length,


        total_mercado_libre:

          searchData.paging

            ? searchData.paging.total

            : null,


        resultados:

          resultados

      },

      null,

      2

    ),

    {

      headers: {

        "Content-Type":

          "application/json; charset=utf-8"

      }

    }

  );

}


// =====================================================

// DIAGNÓSTICO — ESTADO DE FOTOGRAFÍAS

// =====================================================


if (url.pathname === "/diagnostico-fotos") {


  // ---------------------------------------------------

  // TOTAL DE PRODUCTOS

  // ---------------------------------------------------


  const totalProductosResult =

    await env.DB.prepare(

      "SELECT COUNT(*) AS total FROM productos"

    ).first();


  const totalProductos =

    Number(totalProductosResult?.total || 0);


  // ---------------------------------------------------

  // TOTAL DE FOTOGRAFÍAS REGISTRADAS EN D1

  // ---------------------------------------------------


  const totalFotosResult =

    await env.DB.prepare(

      "SELECT COUNT(*) AS total FROM producto_fotos"

    ).first();


  const totalFotos =

    Number(totalFotosResult?.total || 0);


  // ---------------------------------------------------

  // PRODUCTOS CON AL MENOS UNA FOTO

  // ---------------------------------------------------


  const productosConFotosResult =

    await env.DB.prepare(

      `SELECT COUNT(DISTINCT producto_id) AS total

       FROM producto_fotos`

    ).first();


  const productosConFotos =

    Number(productosConFotosResult?.total || 0);


  // ---------------------------------------------------

  // PRODUCTOS SIN NINGUNA FOTO

  // ---------------------------------------------------


  const productosSinFotos =

    Math.max(

      totalProductos - productosConFotos,

      0

    );


  // ---------------------------------------------------

  // PROMEDIO DE FOTOS POR PRODUCTO CON FOTOS

  // ---------------------------------------------------


  const promedioFotos =

    productosConFotos > 0

      ? (

          totalFotos /

          productosConFotos

        ).toFixed(2)

      : "0.00";


  // ---------------------------------------------------

  // RESPUESTA

  // ---------------------------------------------------


  return new Response(


    JSON.stringify(


      {

        total_productos:

          totalProductos,


        total_fotos_en_D1:

          totalFotos,


        productos_con_fotos:

          productosConFotos,


        productos_sin_fotos:

          productosSinFotos,


        promedio_fotos_por_producto:

          promedioFotos

      },


      null,


      2


    ),


    {


      headers: {


        "Content-Type":

          "application/json; charset=utf-8"


      }


    }


  );

}


// =====================================================

// IMPORTACIÓN DE FOTOS — 10 PRODUCTOS POR LOTE

// REANUDABLE — NO DUPLICA FOTOS

// =====================================================


if (url.pathname === "/importar-fotos") {


  // ---------------------------------------------------

  // LEER TOKENS

  // ---------------------------------------------------


  const tokenData =

    await env.ML_TOKENS.get(

      "oauth_tokens",

      "json"

    );


  if (!tokenData) {


    return new Response(

      "No hay tokens guardados.",

      { status: 401 }

    );


  }


  let accessToken =

    tokenData.access_token;


  const userId =

    tokenData.user_id;


  // ---------------------------------------------------

  // RENOVAR TOKEN SI ESTÁ VENCIDO

  // ---------------------------------------------------


  const obtenidoHace =

    Date.now() -

    (tokenData.obtained_at || 0);


  const venceEn =

    (tokenData.expires_in || 21600) *

    1000;


  if (obtenidoHace >= venceEn) {


    const refreshBody =

      new URLSearchParams();


    refreshBody.set(

      "grant_type",

      "refresh_token"

    );


    refreshBody.set(

      "client_id",

      env.ML_CLIENT_ID

    );


    refreshBody.set(

      "client_secret",

      env.ML_CLIENT_SECRET

    );


    refreshBody.set(

      "refresh_token",

      tokenData.refresh_token

    );


    const refreshResponse =

      await fetch(

        "https://api.mercadolibre.com/oauth/token",

        {

          method: "POST",


          headers: {

            "Accept":

              "application/json",


            "Content-Type":

              "application/x-www-form-urlencoded"

          },


          body:

            refreshBody

        }

      );


    const refreshData =

      await refreshResponse.json();


    if (!refreshResponse.ok) {


      return new Response(

        JSON.stringify(

          refreshData,

          null,

          2

        ),

        {

          status:

            refreshResponse.status,


          headers: {

            "Content-Type":

              "application/json"

          }

        }

      );


    }


    accessToken =

      refreshData.access_token;


    await env.ML_TOKENS.put(

      "oauth_tokens",

      JSON.stringify({


        access_token:

          refreshData.access_token,


        refresh_token:

          refreshData.refresh_token ||

          tokenData.refresh_token,


        expires_in:

          refreshData.expires_in,


        user_id:

          refreshData.user_id ||

          tokenData.user_id,


        obtained_at:

          Date.now()


      })

    );


  }


  // ---------------------------------------------------

  // LOTE

  // ---------------------------------------------------


  const urlInterna =

    new URL(request.url);


  const offset =

    parseInt(

      urlInterna.searchParams.get(

        "offset"

      ) || "0",

      10

    );


  const limite =

    parseInt(

      urlInterna.searchParams.get(

        "limite"

      ) || "10",

      10

    );


  // ---------------------------------------------------

  // LÍMITE DE SEGURIDAD

  // ---------------------------------------------------


  const MAX_FOTOS =

    20;


  let fotosNuevasTotales =

    0;


  let bytesNuevosTotales =

    0;


  const resultados = [];


  // ---------------------------------------------------

  // OBTENER PRODUCTOS DE D1

  // ---------------------------------------------------


  const productosResult =

    await env.DB.prepare(

      `SELECT

         id,

         ml_id,

         titulo

       FROM productos

       ORDER BY id

       LIMIT ? OFFSET ?`

    )

      .bind(

        limite,

        offset

      )

      .all();


  const productos =

    productosResult.results || [];


  // ---------------------------------------------------

  // TOTAL DE PRODUCTOS

  // ---------------------------------------------------


  const totalResult =

    await env.DB.prepare(

      "SELECT COUNT(*) AS total FROM productos"

    ).first();


  const totalProductos =

    Number(

      totalResult?.total || 0

    );


  // ---------------------------------------------------

  // PROCESAR PRODUCTOS

  // ---------------------------------------------------


  for (const producto of productos) {


    try {


      // -----------------------------------------------

      // OBTENER PRODUCTO DE MERCADO LIBRE

      // -----------------------------------------------


      const itemResponse =

        await fetch(

          `https://api.mercadolibre.com/items/${producto.ml_id}`,

          {

            headers: {

              "Authorization":

                `Bearer ${accessToken}`

            }

          }

        );


      const item =

        await itemResponse.json();


      if (!itemResponse.ok) {


        resultados.push({


          ml_id:

            producto.ml_id,


          producto_id:

            producto.id,


          estado:

            "error_producto",


          detalle:

            item


        });


        continue;


      }


      // -----------------------------------------------

      // FOTOGRAFÍAS

      // -----------------------------------------------


      const fotos =

        (item.pictures || [])

          .map(

            picture =>

              picture.secure_url

          )

          .filter(Boolean);


      // -----------------------------------------------

      // FOTOS YA REGISTRADAS

      // -----------------------------------------------


      const fotosExistentesResult =

        await env.DB.prepare(

          `SELECT orden, url

           FROM producto_fotos

           WHERE producto_id = ?

           ORDER BY orden`

        )

          .bind(

            producto.id

          )

          .all();


      const fotosExistentes =

        fotosExistentesResult.results || [];


      const ordenesExistentes =

        new Set(

          fotosExistentes.map(

            foto =>

              Number(foto.orden)

          )

        );


      const cantidadAntes =

        fotosExistentes.length;


      let fotosNuevasProducto =

        0;


      let bytesProducto =

        0;


      // -----------------------------------------------

      // DESCARGAR FOTOS FALTANTES

      // -----------------------------------------------


      for (

        let i = 0;

        i < fotos.length;

        i++

      ) {


        if (

          fotosNuevasTotales >=

          MAX_FOTOS

        ) {

          break;

        }


        // ---------------------------------------------

        // YA EXISTE

        // ---------------------------------------------


        if (

          ordenesExistentes.has(i)

        ) {

          continue;

        }


        const fotoUrl =

          fotos[i];


        try {


          const fotoResponse =

            await fetch(

              fotoUrl

            );


          if (!fotoResponse.ok) {

            continue;

          }


          const contenido =

            await fotoResponse.arrayBuffer();


          const contentType =

            fotoResponse.headers.get(

              "content-type"

            ) ||

            "image/jpeg";


          const extension =

            contentType.includes("png")

              ? "png"

              : contentType.includes("webp")

                ? "webp"

                : "jpg";


          const nombreObjeto =

            `productos/${producto.ml_id}/foto-${i + 1}.${extension}`;


          // -----------------------------------------

          // GUARDAR EN R2

          // -----------------------------------------


          await env.FOTOS.put(

            nombreObjeto,

            contenido,

            {

              httpMetadata: {

                contentType:

                  contentType

              }

            }

          );


          // -----------------------------------------

          // REGISTRAR EN D1

          // -----------------------------------------


          await env.DB.prepare(

            `INSERT INTO producto_fotos

             (

               producto_id,

               url,

               orden,

               es_principal

             )

             VALUES (?, ?, ?, ?)`

          )

            .bind(

              producto.id,

              nombreObjeto,

              i,

              i === 0 ? 1 : 0

            )

            .run();


          // -----------------------------------------

          // CONTADORES

          // -----------------------------------------


          fotosNuevasProducto++;


          fotosNuevasTotales++;


          bytesProducto +=

            contenido.byteLength;


          bytesNuevosTotales +=

            contenido.byteLength;


        } catch (error) {


          // Si una foto falla,

          // continuamos con las demás.


        }


      }


      // -----------------------------------------------

      // TOTAL FINAL

      // -----------------------------------------------


      const fotosFinales =

        cantidadAntes +

        fotosNuevasProducto;


      // -----------------------------------------------

      // ESTADO

      // -----------------------------------------------


      let estado;


      if (

        fotosFinales >=

        fotos.length

      ) {


        estado =

          fotosNuevasProducto > 0

            ? "fotos_completadas"

            : "ya_existia_completo";


      } else {


        estado =

          fotosNuevasProducto > 0

            ? "fotos_parcial"

            : "sin_fotos";


      }


      // -----------------------------------------------

      // RESULTADO

      // -----------------------------------------------


      resultados.push({


        ml_id:

          producto.ml_id,


        producto_id:

          producto.id,


        estado:

          estado,


        titulo:

          producto.titulo,


        fotos_encontradas:

          fotos.length,


        fotos_que_ya_existian:

          cantidadAntes,


        fotos_nuevas:

          fotosNuevasProducto,


        fotos_totales:

          fotosFinales,


        fotos_pendientes:

          Math.max(

            fotos.length -

            fotosFinales,

            0

          ),


        espacio_bytes:

          bytesProducto,


        espacio_MB:

          (

            bytesProducto /

            1024 /

            1024

          ).toFixed(2)


      });


    } catch (error) {


      resultados.push({


        ml_id:

          producto.ml_id,


        producto_id:

          producto.id,


        estado:

          "error",


        mensaje:

          error.message


      });


    }


  }


  // ---------------------------------------------------

  // SIGUIENTE OFFSET

  // ---------------------------------------------------


// ---------------------------------------------------

// SIGUIENTE OFFSET

// ---------------------------------------------------


const siguienteOffset =

  offset + productos.length;


  // ---------------------------------------------------

  // RESPUESTA

  // ---------------------------------------------------


  return new Response(


    JSON.stringify(


      {


        offset:

          offset,


        limite:

          limite,


        encontrados:

          productos.length,


        siguiente_offset:

          siguienteOffset,


        total_productos:

          totalProductos,


        maximo_fotos_por_ejecucion:

          MAX_FOTOS,


        fotos_nuevas_en_esta_ejecucion:

          fotosNuevasTotales,


        espacio_total_bytes:

          bytesNuevosTotales,


        espacio_total_MB:

          (

            bytesNuevosTotales /

            1024 /

            1024

          ).toFixed(2),


        resultados:

          resultados


      },


      null,


      2


    ),


    {


      headers: {


        "Content-Type":

          "application/json; charset=utf-8"


      }


    }


  );


}


// =====================================================

// SERVIR FOTOGRAFÍAS DESDE R2

// =====================================================


if (url.pathname === "/foto") {


  const archivo =

    url.searchParams.get("archivo");


  if (!archivo) {


    return new Response(

      "Falta el parámetro archivo.",

      {

        status: 400

      }

    );


  }


  const objeto =

    await env.FOTOS.get(archivo);


  if (!objeto) {


    return new Response(

      "Fotografía no encontrada.",

      {

        status: 404

      }

    );


  }


  return new Response(

    objeto.body,

    {

      headers: {

        "Content-Type":

          objeto.httpMetadata?.contentType ||

          "image/jpeg",


        "Cache-Control":

          "public, max-age=31536000"

      }

    }

  );

}


    return new Response(

      "Ruta no encontrada.",

      {

        status: 404

      }

    );

  }

};