#!/usr/bin/env python3
import http.server
import json
import re
import os
import socketserver
import subprocess
import mimetypes
import urllib.request
from pathlib import Path
from urllib.parse import parse_qs, urlparse, quote

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
REMOTE_BASE_FRESQ = "https://fresq.ru"
REMOTE_BASE_APPLICO = "https://applico.ru"
# Proxy these prefixes to the chosen remote base (fresq/applico depending on cookie)
PROXY_PREFIXES = (
    "/app/ajax",
    "/upload/",
    "/ajax/",       # Applico constructor endpoints live here
    "/local/",      # Applico assets (js/css)
    "/bitrix/",     # Both Fresq/Applico use Bitrix assets
)
APPLICO_EXPORT_DIR = Path(
    os.environ.get(
        "APPLICO_EXPORT_DIR",
        str(Path(BASE_DIR) / "applico_export" / "output"),
    )
)
MANIFEST_PATH = APPLICO_EXPORT_DIR / "manifest.json"
APPLICO_COLLECTIONS_PATH = APPLICO_EXPORT_DIR / "collections.json"
APPLICO_ITEMS_PATH = APPLICO_EXPORT_DIR / "items.json"
APPLICO_ITEMS_DETAILED_PATH = APPLICO_EXPORT_DIR / "items_detailed.ndjson"

CACHE_DIR = Path(
    os.environ.get(
        "FRESQ_CACHE_DIR",
        str(Path(BASE_DIR) / ".cache_img"),
    )
)
IMGPROXY_ORIGIN = os.environ.get("IMGPROXY_ORIGIN", "").rstrip("/")
COLLECTIONS_CACHE_TTL = int(os.environ.get("COLLECTIONS_CACHE_TTL", "120"))

DEMO_SECTION_ID = "9001"
DEMO_ITEM_ID = "19277"
DEMO_COLLECTION_NAME = "Applico Demo"
DEMO_CATALOG_NAME = "APPlico"
APPLICO_MAX_WIDTH = 950
APPLICO_MAX_HEIGHT = 317
FAST_WIDTH_IMAGE = 600  # For thumbnails only
FAST_WIDTH_BACKGROUND = 400
OPTIMAL_WIDTH_IMAGE = 1000  # Balanced quality/speed for main images
HQ_WIDTH_IMAGE = 1600
HQ_WIDTH_BACKGROUND = 800

# In-memory cache for Applico data (exported by applico_export scripts)
_APPLICO_CACHE = {
    "loaded": False,
    "collections": None,      # Fresq-like collections payload
    "items_by_id": {},        # element_id -> detailed row
    "basic_by_id": {},        # element_id -> basic row (thumb/url/title)
}
_LAST_APPLICO_ITEM_ID = None


def _load_manifest():
    if not MANIFEST_PATH.exists():
        return None
    try:
        return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
    except Exception:
        return None


def _slug_from_url(url):
    try:
        path = urlparse(url).path.rstrip("/")
        if not path:
            return "applico-item"
        return path.split("/")[-1]
    except Exception:
        return "applico-item"


def _unwrap_imgproxy_url(url: str) -> str:
    """Some exports contain links like https://applico.ru/imgproxy?url=<real>&w=400.
    Applico doesn't always serve /imgproxy, so unwrap to the real image URL."""
    try:
        parsed = urlparse(url)
        if parsed.path.rstrip("/") != "/imgproxy":
            return url
        qs = parse_qs(parsed.query)
        inner = (qs.get("url") or [""])[0]
        return inner or url
    except Exception:
        return url


def _normalize_img(url_or_path):
    if not url_or_path:
        return ""
    url = url_or_path
    if not (url.startswith("http://") or url.startswith("https://")):
        url = "https://applico.ru" + url
    return _unwrap_imgproxy_url(url)


def _normalize_fresq_img(url_or_path):
    if not url_or_path:
        return ""
    if url_or_path.startswith("http://") or url_or_path.startswith("https://"):
        return url_or_path
    if url_or_path.startswith("//"):
        return "https:" + url_or_path
    if url_or_path.startswith("/"):
        return REMOTE_BASE_FRESQ + url_or_path
    return REMOTE_BASE_FRESQ + "/" + url_or_path


def _imgproxy_url(url, width=1600, *, bw=False):
    path = f"/imgproxy?url={quote(url)}&w={width}"
    if bw:
        path += "&bw=1"
    if IMGPROXY_ORIGIN:
        return IMGPROXY_ORIGIN + path
    return path

def _imgproxy_url_progressive(url, *, bw=False):
    """Return both fast and HQ URLs for progressive loading."""
    fast_url = _imgproxy_url(url, width=FAST_WIDTH_IMAGE, bw=bw)
    hq_url = _imgproxy_url(url, width=HQ_WIDTH_IMAGE, bw=bw)
    return {
        "fast": fast_url,
        "hq": hq_url
    }


def _rewrite_image_urls(data, base_url, *, width_image=1600, width_background=800, bw=False, progressive=False):
    def _wrap(url, width):
        return _imgproxy_url(url, width, bw=bw)

    def _wrap_progressive(url):
        return _imgproxy_url_progressive(url, bw=bw)

    # 1) Image endpoint payload: { items: [...] }
    if isinstance(data, dict) and "items" in data:
        for item in data.get("items", []):
            if not isinstance(item, dict):
                continue
            img = item.get("img") or ""
            if base_url == "applico":
                img = _normalize_img(img)
            else:
                img = _normalize_fresq_img(img)
            if img:
                if progressive:
                    # Return both fast and HQ URLs
                    urls = _wrap_progressive(img)
                    item["img"] = urls["fast"]
                    item["imgHQ"] = urls["hq"]
                else:
                    item["img"] = _wrap(img, width_image)
        return data

    # 2) Collections payload: [ { img, items:[{img},...] }, ... ]
    if isinstance(data, list) and data and isinstance(data[0], dict) and "items" in data[0]:
        for collection in data:
            if not isinstance(collection, dict):
                continue
            cimg = collection.get("img") or ""
            if cimg:
                cimg = _normalize_img(cimg) if base_url == "applico" else _normalize_fresq_img(cimg)
                collection["img"] = _wrap(cimg, width_background)
            for item in collection.get("items", []) or []:
                if not isinstance(item, dict):
                    continue
                img = item.get("img") or ""
                if not img:
                    continue
                img = _normalize_img(img) if base_url == "applico" else _normalize_fresq_img(img)
                item["img"] = _wrap(img, width_image)
        return data

    # 3) Background payload: [ { colors:[{img}], premColors:[{img}] }, ... ]
    if isinstance(data, list):
        for group in data:
            if not isinstance(group, dict):
                continue
            for key in ("colors", "premColors"):
                for color in group.get(key, []) or []:
                    if not isinstance(color, dict):
                        continue
                    img = color.get("img") or ""
                    if base_url == "applico":
                        img = _normalize_img(img)
                    else:
                        img = _normalize_fresq_img(img)
                    if img:
                        color["img"] = _wrap(img, width_background)
        return data

    return data


def _rewrite_fresq_collections(data):
    if not isinstance(data, list):
        return data
    for collection in data:
        if isinstance(collection, dict):
            if "img" in collection:
                collection["img"] = _normalize_fresq_img(collection.get("img"))
            items = collection.get("items")
            if isinstance(items, list):
                for item in items:
                    if isinstance(item, dict) and "img" in item:
                        item["img"] = _normalize_fresq_img(item.get("img"))
    return data


def _rewrite_fresq_background(data):
    if not isinstance(data, list):
        return data
    for group in data:
        if not isinstance(group, dict):
            continue
        for key in ("colors", "premColors"):
            for color in group.get(key, []) or []:
                if not isinstance(color, dict):
                    continue
                img = color.get("img") or ""
                color["img"] = _normalize_fresq_img(img)
    return data


def _rewrite_fresq_image_payload(data):
    if not isinstance(data, dict):
        return data
    items = data.get("items")
    if isinstance(items, list):
        for item in items:
            if isinstance(item, dict) and "img" in item:
                item["img"] = _normalize_fresq_img(item.get("img") or "")
    return data


def _unhide_fresq_options(data):
    """Ensure Standard/Nonstandard size options are visible in Fresq mode."""
    # Handle both array response and object with "data" key
    if isinstance(data, list):
        items = data
    elif isinstance(data, dict):
        items = data.get("data")
        if not isinstance(items, list):
            return data
    else:
        return data

    target_names = {
        "стандартные панели",
        "нестандартные размеры",
        "стандартные размеры",
    }
    for opt in items:
        if not isinstance(opt, dict):
            continue
        opt_id = opt.get("id")
        name = str(opt.get("name") or "").strip().lower()
        if opt_id in (1107, 1108, 1520) or name in target_names:
            opt["isHidden"] = False

    return data


def _build_demo_data():
    manifest = _load_manifest() or {}
    source_url = manifest.get("source_url") or "https://applico.ru/"
    raw_item_id = manifest.get("item_id") or DEMO_ITEM_ID
    try:
        item_id = int(raw_item_id)
    except Exception:
        item_id = int(DEMO_ITEM_ID)
    item_name = _slug_from_url(source_url).replace("-", " ").upper()
    item_img = _normalize_img(manifest.get("original_img"))
    factures = manifest.get("factures") or []

    if not item_img:
        item_img = "https://applico.ru/upload/iblock/cee/zmujnpch3j8gezjiqzgjnbbnaa8kcu7o.jpg"

    collection = [
        {
            "id": int(DEMO_SECTION_ID),
            "img": item_img,
            "name": DEMO_COLLECTION_NAME,
            "items": [
                {
                    "id": int(item_id),
                    "img": item_img,
                    "name": item_name,
                }
            ],
        }
    ]

    background_items = []
    for f in factures:
        background_items.append(
            {
                "price": int(f.get("price") or 0),
                "premPrice": 0,
                "excludedOptions": False,
                "installPrice": 0,
                "minInstallPrice": 0,
                "examplePrice": 0,
                "exampleExtraPrice": 0,
                "id": int(f.get("id") or 0),
                "groupName": f.get("name") or "Base",
                "selectedImg": "",
                "isPrem": False,
                "colors": [
                    {
                        "img": _normalize_img(f.get("img")),
                        "name": f.get("name") or "Base",
                    }
                ],
                "premColors": [],
                "isSeamless": False,
                "isSeamlessMode": False,
            }
        )

    if not background_items:
        background_items = [
            {
                "price": 0,
                "premPrice": 0,
                "excludedOptions": False,
                "installPrice": 0,
                "minInstallPrice": 0,
                "examplePrice": 0,
                "exampleExtraPrice": 0,
                "id": 1,
                "groupName": "Base",
                "selectedImg": "",
                "isPrem": False,
                "colors": [{"img": item_img, "name": "Base"}],
                "premColors": [],
                "isSeamless": False,
                "isSeamlessMode": False,
            }
        ]

    image = {
        "items": [
            {
                "id": int(item_id),
                "name": item_name,
                "img": item_img,
                "price": int((factures[0].get("price") if factures else 0) or 0),
            }
        ],
        "catalog": DEMO_CATALOG_NAME,
        "remove_background": [],
    }

    return {
        "collections": collection,
        "background": background_items,
        "image": image,
    }


def _parse_cookies(cookie_header):
    cookies = {}
    if not cookie_header:
        return cookies
    parts = cookie_header.split(";")
    for part in parts:
        if "=" in part:
            k, v = part.split("=", 1)
            cookies[k.strip()] = v.strip()
    return cookies


def _should_use_applico(cookie_header):
    cookies = _parse_cookies(cookie_header)
    return cookies.get("data_source") == "applico"


def _should_use_fast_images(cookie_header):
    cookies = _parse_cookies(cookie_header)
    return cookies.get("img_quality", "fast") == "fast"

def _should_use_bw(cookie_header):
    cookies = _parse_cookies(cookie_header)
    return cookies.get("applico_bw") == "1"

def _get_applico_cookie_item_id(cookie_header):
    cookies = _parse_cookies(cookie_header)
    return cookies.get("applico_item_id") or ""

def _get_applico_cookie_section_id(cookie_header):
    cookies = _parse_cookies(cookie_header)
    return cookies.get("applico_section_id") or ""

def _get_applico_cookie_size(cookie_header):
    cookies = _parse_cookies(cookie_header)
    w = cookies.get("applico_w") or ""
    h = cookies.get("applico_h") or ""
    try:
        w = int(w)
    except Exception:
        w = 0
    try:
        h = int(h)
    except Exception:
        h = 0
    if w <= 0 or w > APPLICO_MAX_WIDTH:
        w = 0
    if h <= 0 or h > APPLICO_MAX_HEIGHT:
        h = 0
    return w, h


def _stable_int_id(value: str, base: int = 900000) -> int:
    # Deterministic id for non-numeric tab ids like "one", "№1".
    import zlib
    try:
        raw = value.encode("utf-8")
    except Exception:
        raw = str(value).encode("utf-8", errors="ignore")
    return base + (zlib.crc32(raw) % 100000)


def _load_applico_data():
    """Load exported Applico dataset (Stage 1/2) and map it to Fresq-like /app/ajax payloads."""
    if _APPLICO_CACHE.get("loaded"):
        return

    collections_raw = None
    items_by_id = {}
    basic_by_id = {}

    # Load basic items (Stage 1)
    if APPLICO_ITEMS_PATH.exists():
        try:
            basic_items = json.loads(APPLICO_ITEMS_PATH.read_text(encoding="utf-8"))
            for row in basic_items or []:
                item_id = row.get("item_id")
                if item_id is None:
                    continue
                basic_by_id[str(item_id)] = row
        except Exception:
            basic_by_id = {}

    # Load detailed items (Stage 2, preferred)
    if APPLICO_ITEMS_DETAILED_PATH.exists():
        try:
            for line in APPLICO_ITEMS_DETAILED_PATH.read_text(encoding="utf-8", errors="ignore").splitlines():
                line = line.strip()
                if not line:
                    continue
                row = json.loads(line)
                item_id = row.get("item_id")
                if item_id is None:
                    continue
                items_by_id[str(item_id)] = row
        except Exception:
            items_by_id = {}

    # Load collections (Stage 1)
    if APPLICO_COLLECTIONS_PATH.exists():
        try:
            collections_raw = json.loads(APPLICO_COLLECTIONS_PATH.read_text(encoding="utf-8"))
        except Exception:
            collections_raw = None

    if not collections_raw:
        # fallback to demo-only mode
        _APPLICO_CACHE["collections"] = None
        _APPLICO_CACHE["items_by_id"] = items_by_id
        _APPLICO_CACHE["basic_by_id"] = basic_by_id
        _APPLICO_CACHE["loaded"] = True
        return

    # Map Applico exported structure -> Fresq-like collections
    mapped = []
    for group in collections_raw:
        group_title = (group.get("title") or "Applico").strip()
        for tab in group.get("tabs") or []:
            tab_id = str(tab.get("id") or "")
            tab_label = (tab.get("label") or tab_id or "tab").strip()
            coll_id = int(tab_id) if tab_id.isdigit() else _stable_int_id(tab_id)
            name = f"{group_title} / {tab_label}" if tab_label and tab_label != group_title else group_title

            items = []
            for it in tab.get("items") or []:
                iid = it.get("item_id")
                if iid is None:
                    continue
                iid_s = str(iid)
                detail = items_by_id.get(iid_s) or {}
                img = detail.get("original_img") or detail.get("thumb") or it.get("thumb") or ""
                title = detail.get("title") or it.get("title") or it.get("alt") or str(iid)
                items.append({
                    "id": int(iid) if str(iid).isdigit() else iid,
                    "img": img,
                    "name": title,
                })

            # pick collection image
            coll_img = items[0]["img"] if items else ""
            mapped.append({
                "id": coll_id,
                "img": coll_img,
                "name": name,
                "items": items,
            })

    _APPLICO_CACHE["collections"] = mapped
    _APPLICO_CACHE["items_by_id"] = items_by_id
    _APPLICO_CACHE["basic_by_id"] = basic_by_id
    _APPLICO_CACHE["loaded"] = True


def _get_applico_collections_payload():
    _load_applico_data()
    return _APPLICO_CACHE.get("collections")

def _find_applico_collection_by_id(section_id: str):
    _load_applico_data()
    for coll in _APPLICO_CACHE.get("collections") or []:
        if str(coll.get("id")) == str(section_id):
            return coll
    return None

def _find_applico_item_in_collections(element_id: str):
    _load_applico_data()
    for coll in _APPLICO_CACHE.get("collections") or []:
        for item in coll.get("items", []) or []:
            if str(item.get("id")) == str(element_id):
                return item
    return None


def _get_applico_image_payload(element_id: str):
    """Return Fresq-like image payload for a given element_id (Applico item_id)."""
    global _LAST_APPLICO_ITEM_ID
    _load_applico_data()
    if element_id:
        _LAST_APPLICO_ITEM_ID = str(element_id)
    row = _APPLICO_CACHE.get("items_by_id", {}).get(str(element_id))
    basic = _APPLICO_CACHE.get("basic_by_id", {}).get(str(element_id))
    coll_item = _find_applico_item_in_collections(element_id)

    if row or basic:
        title = (
            (row or {}).get("title")
            or (basic or {}).get("title")
            or (basic or {}).get("alt")
            or (coll_item or {}).get("name")
            or str(element_id)
        )
        img = (
            (row or {}).get("original_img")
            or (row or {}).get("thumb")
            or (basic or {}).get("thumb")
            or (coll_item or {}).get("img")
        )
        # Applico pricing is per m2 via factures; keep texture price at 0.
        price = 0
        return {
            "items": [{"id": int(element_id) if str(element_id).isdigit() else element_id, "name": title, "img": img, "price": price}],
            "catalog": "APPlico",
            "remove_background": [],
        }

    if coll_item:
        title = coll_item.get("name") or str(element_id)
        img = coll_item.get("img") or ""
        return {
            "items": [{"id": int(element_id) if str(element_id).isdigit() else element_id, "name": title, "img": img, "price": 0}],
            "catalog": "APPlico",
            "remove_background": [],
        }

    # Sometimes UI passes collection id instead of item id; fallback to first item.
    coll = _find_applico_collection_by_id(str(element_id))
    if coll and (coll.get("items") or []):
        first = coll["items"][0]
        fid = first.get("id")
        title = first.get("name") or str(fid or element_id)
        img = first.get("img") or ""
        return {
            "items": [{"id": int(fid) if str(fid).isdigit() else fid, "name": title, "img": img, "price": 0}],
            "catalog": "APPlico",
            "remove_background": [],
        }

    return None


def _get_applico_background_payload():
    """Return Fresq-like background payload. We use factures of the last requested item if available."""
    _load_applico_data()
    element_id = _LAST_APPLICO_ITEM_ID or DEMO_ITEM_ID
    row = _APPLICO_CACHE.get("items_by_id", {}).get(str(element_id))
    facts = (row.get("factures") if isinstance(row, dict) else None) or []
    base_price = None
    if isinstance(row, dict):
        base_price = row.get("base_price") or row.get("price")
    if not facts:
        # fallback: use base price if available, otherwise demo manifest
        if base_price is not None:
            try:
                price = int(base_price)
            except Exception:
                price = 0
            return [{
                "price": price,
                "premPrice": 0,
                "excludedOptions": False,
                "installPrice": 0,
                "minInstallPrice": 0,
                "examplePrice": 0,
                "exampleExtraPrice": 0,
                "id": 1,
                "groupName": "Base",
                "selectedImg": "",
                "isPrem": False,
                "colors": [{"img": (row or {}).get("original_img") or "", "name": "Base"}],
                "premColors": [],
                "isSeamless": True,
                "isSeamlessMode": False,
            }]
        return _build_demo_data().get("background")

    out = []
    for f in facts:
        try:
            fid = int(f.get("id") or 0)
        except Exception:
            fid = 0
        name = f.get("name") or "Base"
        img = f.get("img") or ""
        try:
            price = int(f.get("price") or 0)
        except Exception:
            price = 0
        out.append({
            "price": price,
            "premPrice": 0,
            "excludedOptions": False,
            "installPrice": 0,
            "minInstallPrice": 0,
            "examplePrice": 0,
            "exampleExtraPrice": 0,
            "id": fid,
            "groupName": name,
            "selectedImg": "",
            "isPrem": False,
            "colors": [{"img": img, "name": name}],
            "premColors": [],
            "isSeamless": True,
            "isSeamlessMode": False,
        })

    return out


def _build_applico_options():
    # Applico options: keep full Fresq-like schema to satisfy client expectations,
    # but hide the irrelevant ones. Only Mirror + BW are visible.
    hidden = True
    base = [
        {"id": 1107, "name": "Бесшовное полотно", "isHidden": hidden},
        {"id": 1108, "name": "Нестандартные размеры", "isHidden": hidden},
        {"id": 1109, "name": "Подпись художника", "isHidden": hidden},
        {"id": 1110, "name": "Покрытие лаком", "isHidden": hidden},
        {"id": 1111, "name": "Монтаж", "isHidden": hidden},
        {"id": 1112, "name": "Образец", "isHidden": hidden},
        {"id": 1400, "name": "Срочность", "isHidden": hidden},
        {"id": 1520, "name": "Стандартные панели", "isHidden": hidden},
        {"id": 1604, "name": "Объемный рисунок", "isHidden": hidden},
    ]

    visible = [
        {
            "id": 1401,
            "name": "Отзеркалить",
            "isHidden": False,
            "dataText": "Зеркальное отражение рисунка.",
        },
        {
            "id": 90001,
            "name": "Ч/Б",
            "isHidden": False,
            "dataText": "Черно-белый вариант изображения.",
        },
    ]

    def normalize(opt):
        return {
            "id": opt["id"],
            "name": opt["name"],
            "price": 0,
            "priceExtra": 0,
            "isPricePerMeter": False,
            "isIndividualPrice": False,
            "isPercentToFullPrice": False,
            "isSet": False,
            "isHidden": opt.get("isHidden", False),
            "isExtraPrice": False,
            "dataText": opt.get("dataText", ""),
        }

    return [normalize(o) for o in (base + visible)]


_FRESQ_COLLECTIONS_CACHE = {
    "ts": 0,
    "data": None,
}


def _get_fresq_collections():
    import time
    now = time.time()
    if _FRESQ_COLLECTIONS_CACHE["data"] and now - _FRESQ_COLLECTIONS_CACHE["ts"] < COLLECTIONS_CACHE_TTL:
        return _FRESQ_COLLECTIONS_CACHE["data"]
    try:
        url = f"{REMOTE_BASE_FRESQ}/app/ajax/?type=collections&"
        with urllib.request.urlopen(url, timeout=10) as resp:
            raw = resp.read()
            data = json.loads(raw.decode("utf-8"))
            _FRESQ_COLLECTIONS_CACHE["data"] = data
            _FRESQ_COLLECTIONS_CACHE["ts"] = now
            return data
    except Exception:
        return None


def _build_fresq_image_stub(element_id):
    collections = _get_fresq_collections() or []
    for collection in collections:
        for item in collection.get("items", []) or []:
            if str(item.get("id")) == str(element_id):
                img = _normalize_fresq_img(item.get("img") or "")
                return {
                    "items": [
                        {
                            "id": int(element_id),
                            "name": item.get("name") or "",
                            "img": img,
                            "price": 0,
                        }
                    ],
                    "catalog": collection.get("name") or "",
                    "remove_background": [],
                }
    return None

class ProxyHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=BASE_DIR, **kwargs)

    def _should_disable_cache(self):
        try:
            path = self.path.split("?", 1)[0]
        except Exception:
            path = self.path
        ext = os.path.splitext(path)[1].lower()
        if ext in {".html", ".js", ".css", ".json", ".map"}:
            return True
        if path.startswith("/constructor"):
            return True
        if path.startswith("/app/static/js") or path.startswith("/app/static/css"):
            return True
        if path.startswith("/app/ajax") or path.startswith("/ajax"):
            return True
        return False

    def end_headers(self):
        if self._should_disable_cache():
            self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
            self.send_header("Pragma", "no-cache")
            self.send_header("Expires", "0")
        super().end_headers()

    def do_GET(self):
        if self.path.startswith("/imgproxy"):
            return self._handle_imgproxy()
        if self.path.startswith("/app/ajax"):
            return self._handle_ajax()
        if self.path.startswith(PROXY_PREFIXES):
            return self._proxy_request()
        return super().do_GET()

    def do_HEAD(self):
        if self.path.startswith("/imgproxy"):
            return self._handle_imgproxy(head_only=True)
        if self.path.startswith("/app/ajax"):
            return self._handle_ajax(head_only=True)
        if self.path.startswith(PROXY_PREFIXES):
            return self._proxy_request(head_only=True)
        return super().do_HEAD()

    def _handle_ajax(self, head_only=False):
        parsed = urlparse(self.path)
        qs = parse_qs(parsed.query)
        req_type = (qs.get("type") or [""])[0]

        if _should_use_applico(self.headers.get("Cookie", "")):
            # Applico mode:
            # - /app/ajax is served locally in Fresq-like schema, but backed by exported Applico dataset.
            # - /ajax/, /local/, /bitrix/, /upload/ are proxied to applico.ru.

            # Always rewrite Applico image URLs to same-origin /imgproxy.
            # This prevents canvas tainting (https://applico.ru images) and keeps behavior isolated to Applico mode.
            cookie = self.headers.get("Cookie", "")
            is_fast = _should_use_fast_images(cookie)
            is_hq = not is_fast
            bw = _should_use_bw(cookie)
            # HQ quality should affect only the main image. Thumbnails/background stay fast.
            w_img_main = HQ_WIDTH_IMAGE if is_hq else FAST_WIDTH_IMAGE
            w_bg = FAST_WIDTH_BACKGROUND
            w_coll_img = FAST_WIDTH_IMAGE

            if req_type == "collections":
                payload = _get_applico_collections_payload() or _build_demo_data().get("collections", [])
                payload = _rewrite_image_urls(payload, "applico", width_image=w_coll_img, width_background=w_bg, bw=bw)
            elif req_type == "background":
                # Background should follow the last selected item; allow cookie override.
                cookie_item = _get_applico_cookie_item_id(self.headers.get("Cookie", ""))
                if cookie_item:
                    _LAST_APPLICO_ITEM_ID = str(cookie_item)
                payload = _get_applico_background_payload() or _build_demo_data().get("background", [])
                payload = _rewrite_image_urls(payload, "applico", width_image=w_coll_img, width_background=w_bg, bw=bw)
            elif req_type == "image":
                element_id = (qs.get("element_id") or [""])[0]
                cookie_item = _get_applico_cookie_item_id(self.headers.get("Cookie", ""))
                if cookie_item:
                    element_id = cookie_item
                payload = _get_applico_image_payload(element_id)
                if not payload:
                    # Fallback: try to read itemId from Referer if element_id is stale.
                    ref = self.headers.get("Referer", "")
                    try:
                        ref_qs = parse_qs(urlparse(ref).query)
                        ref_item = (ref_qs.get("itemId") or [""])[0]
                        if ref_item:
                            payload = _get_applico_image_payload(ref_item)
                    except Exception:
                        pass
                payload = payload or _build_demo_data().get("image", {})
                cookie_w, _cookie_h = _get_applico_cookie_size(self.headers.get("Cookie", ""))
                if cookie_w:
                    width_img = cookie_w if is_fast else max(cookie_w, HQ_WIDTH_IMAGE)
                else:
                    width_img = w_img_main
                payload = _rewrite_image_urls(payload, "applico", width_image=width_img, width_background=w_bg, bw=bw)
            elif req_type == "options":
                payload = _build_applico_options()
                # Keep UI in sync with current BW cookie.
                if bw:
                    for opt in payload:
                        if opt.get("id") == 90001 or opt.get("name") in ("Ч/Б", "ЧБ"):
                            opt["isSet"] = True
            else:
                return self._proxy_request(head_only=head_only)
        else:
            cookie = self.headers.get("Cookie", "")
            is_fast = _should_use_fast_images(cookie)
            w_img_main = FAST_WIDTH_IMAGE if is_fast else HQ_WIDTH_IMAGE
            w_bg = FAST_WIDTH_BACKGROUND
            if req_type == "options":
                target_url = REMOTE_BASE_FRESQ + self.path
                try:
                    with urllib.request.urlopen(target_url, timeout=20) as resp:
                        raw = resp.read()
                        data = json.loads(raw.decode("utf-8"))
                        payload = _unhide_fresq_options(data)
                        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
                        self.send_response(200)
                        self.send_header("Content-Type", "application/json; charset=utf-8")
                        self.send_header("Content-Length", str(len(body)))
                        self.end_headers()
                        if not head_only:
                            self.wfile.write(body)
                        return None
                except Exception:
                    return self._proxy_request(head_only=head_only)
            if req_type == "collections":
                target_url = REMOTE_BASE_FRESQ + self.path
                try:
                    with urllib.request.urlopen(target_url, timeout=20) as resp:
                        raw = resp.read()
                        data = json.loads(raw.decode("utf-8"))
                        payload = _rewrite_fresq_collections(data)
                        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
                        self.send_response(200)
                        self.send_header("Content-Type", "application/json; charset=utf-8")
                        self.send_header("Content-Length", str(len(body)))
                        self.end_headers()
                        if not head_only:
                            self.wfile.write(body)
                        return None
                except Exception:
                    return self._proxy_request(head_only=head_only)
            if req_type == "background":
                target_url = REMOTE_BASE_FRESQ + self.path
                try:
                    with urllib.request.urlopen(target_url, timeout=20) as resp:
                        raw = resp.read()
                        data = json.loads(raw.decode("utf-8"))
                        payload = _rewrite_fresq_background(data)
                        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
                        self.send_response(200)
                        self.send_header("Content-Type", "application/json; charset=utf-8")
                        self.send_header("Content-Length", str(len(body)))
                        self.end_headers()
                        if not head_only:
                            self.wfile.write(body)
                        return None
                except Exception:
                    return self._proxy_request(head_only=head_only)
            if req_type == "image":
                target_url = REMOTE_BASE_FRESQ + self.path
                try:
                    with urllib.request.urlopen(target_url, timeout=20) as resp:
                        raw = resp.read()
                        data = json.loads(raw.decode("utf-8"))
                        # Use optimal quality (1000px) - fast enough + good quality
                        # Progressive loading disabled - it breaks canvas clipping in editor
                        payload = _rewrite_image_urls(data, "fresq", width_image=OPTIMAL_WIDTH_IMAGE, width_background=w_bg, progressive=False)
                        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
                        self.send_response(200)
                        self.send_header("Content-Type", "application/json; charset=utf-8")
                        self.send_header("Content-Length", str(len(body)))
                        self.end_headers()
                        if not head_only:
                            self.wfile.write(body)
                        return None
                except Exception:
                    return self._proxy_request(head_only=head_only)
            return self._proxy_request(head_only=head_only)

        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        if not head_only:
            self.wfile.write(body)
        return None

    def _proxy_request(self, head_only=False):
        # Serve local file if present (e.g., Fresq bitrix cache assets).
        if self._serve_local_file(head_only=head_only):
            return None
        # Choose upstream depending on cookie (applico/fresq).
        cookie = self.headers.get("Cookie", "")
        remote = REMOTE_BASE_APPLICO if _should_use_applico(cookie) else REMOTE_BASE_FRESQ
        target_url = remote + self.path
        return self._proxy_request_url(target_url, head_only=head_only)

    def _proxy_request_url(self, target_url, head_only=False):
        req = urllib.request.Request(target_url, method="HEAD" if head_only else "GET")
        req.add_header("User-Agent", "fresq-recovery-proxy")
        try:
            with urllib.request.urlopen(req, timeout=20) as resp:
                self.send_response(resp.status)
                for key, value in resp.getheaders():
                    if key.lower() in {"connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade"}:
                        continue
                    self.send_header(key, value)
                self.end_headers()
                if not head_only:
                    try:
                        self.wfile.write(resp.read())
                    except (BrokenPipeError, ConnectionResetError):
                        # Client closed the connection; nothing to do.
                        return
        except (BrokenPipeError, ConnectionResetError):
            # Client disconnected mid-response.
            return
        except Exception as exc:
            try:
                self.send_error(502, f"Proxy error: {exc}")
            except (BrokenPipeError, ConnectionResetError):
                return

    def _handle_imgproxy(self, head_only=False):
        parsed = urlparse(self.path)
        qs = parse_qs(parsed.query)
        raw_url = (qs.get("url") or [""])[0]
        if not raw_url:
            self.send_error(400, "Missing url")
            return None
        # Some inputs already point to https://applico.ru/imgproxy?...; unwrap to the real image.
        raw_url = _unwrap_imgproxy_url(raw_url)
        width = int((qs.get("w") or ["1600"])[0])
        bw = (qs.get("bw") or ["0"])[0] == "1"

        CACHE_DIR.mkdir(parents=True, exist_ok=True)
        ext = os.path.splitext(urlparse(raw_url).path)[1].lower() or ".jpg"
        # NOTE: cache key includes a version bump to avoid stale cached images when we tweak processing.
        key = f"{raw_url}|{width}|bw={int(bw)}|v3"
        cache_key = str(abs(hash(key)))
        out_path = CACHE_DIR / f"{cache_key}_w{width}{ext}"
        if not out_path.exists():
            tmp_path = CACHE_DIR / f"{cache_key}_tmp{ext}"
            try:
                with urllib.request.urlopen(raw_url, timeout=30) as resp:
                    tmp_path.write_bytes(resp.read())
                try:
                    # Use ffmpeg so bw=1 really produces grayscale (sips tended to keep output identical).
                    vf = f"scale={width}:-1"
                    if bw:
                        vf += ",hue=s=0"
                    cmd = [
                        "ffmpeg",
                        "-hide_banner",
                        "-loglevel",
                        "error",
                        "-y",
                        "-i",
                        str(tmp_path),
                        "-vf",
                        vf,
                    ]
                    # Preserve transparency for PNG (indexed+tRNS and RGBA)
                    if ext.lower() == ".png":
                        cmd += ["-pix_fmt", "rgba"]
                    cmd.append(str(out_path))
                    subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                except Exception:
                    # Fallback: serve the original downloaded bytes.
                    out_path = tmp_path
                if tmp_path.exists() and out_path != tmp_path:
                    tmp_path.unlink(missing_ok=True)
            except Exception as exc:
                self.send_error(502, f"Image proxy error: {exc}")
                return None

        ctype, _ = mimetypes.guess_type(str(out_path))
        self.send_response(200)
        self.send_header("Content-Type", ctype or "application/octet-stream")
        self.send_header("Content-Length", str(out_path.stat().st_size))
        self.end_headers()
        if not head_only:
            self.wfile.write(out_path.read_bytes())
        return None

    def _serve_local_file(self, head_only=False):
        try:
            rel = self.path.lstrip("/")
            local_path = os.path.join(BASE_DIR, rel)
            if not os.path.isfile(local_path):
                return False
            ctype, _ = mimetypes.guess_type(local_path)
            self.send_response(200)
            self.send_header("Content-Type", ctype or "application/octet-stream")
            self.send_header("Content-Length", str(os.path.getsize(local_path)))
            self.end_headers()
            if not head_only:
                with open(local_path, "rb") as f:
                    self.wfile.write(f.read())
            return True
        except Exception:
            return False


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))

    class ReusableTCPServer(socketserver.ThreadingTCPServer):
        allow_reuse_address = True

    with ReusableTCPServer(("", port), ProxyHandler) as httpd:
        httpd.daemon_threads = True
        print(f"Serving on http://localhost:{port}")
        httpd.serve_forever()
