"use client";

import { useEffect, useRef, useState, type CSSProperties } from "react";

import { API_BASE_URL } from "@/lib/api";
import { safeUrl } from "@/lib/safeUrl";

import { tokens } from "./tokens";
import { object, report } from "./fixtures";
import type { HeroBarData } from "./mappers";

// Default presentation data (unwired usage): the existing design fixtures.
const HERO_FIXTURE: HeroBarData = { report, object };

// estimate ids are server-issued UUIDs — reject anything else before it lands
// in the PDF request path so a tampered id cannot be injected.
const PDF_UUID_RE =
  /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;

// Build + validate the PDF download URL for an estimate id. Returns null when
// there is no estimate yet (→ disabled button), the id is not a UUID, or the
// resolved URL fails the safeUrl scheme allowlist. API_BASE_URL is relative
// ("" in dev, "/trade-in" behind Caddy), so resolve against the document origin
// purely for the safeUrl http/https check; the <a> keeps the relative href
// (the browser resolves it against the current page, like OfferCard's link).
function pdfDownloadHref(estimateId: string | null | undefined): string | null {
  if (!estimateId || !PDF_UUID_RE.test(estimateId)) return null;
  const href = `${API_BASE_URL}/api/v1/trade-in/estimate/${estimateId}/pdf`;
  if (typeof window === "undefined") return href;
  return safeUrl(new URL(href, window.location.origin).toString())
    ? href
    : null;
}

// ── Locator mini-map (Leaflet + OSM) ────────────────────────────────────────
// Replaces the old static building.png stock photo — user-reported bug: that
// single asset was shown for EVERY estimate regardless of the real address,
// misleading users into thinking they were looking at their own building.
// PORTS the Leaflet-CDN loader pattern from ./SourcesMap.tsx (itself ported
// from the dead v1 tree's MapCard.tsx) — copied rather than imported so this
// file stays a self-contained port with no shared runtime module and no npm
// Leaflet dep, same rationale as SourcesMap.
/* eslint-disable @typescript-eslint/no-explicit-any -- интероп с CDN-библиотекой Leaflet (см. SourcesMap.tsx) */
const LEAFLET_VER = "1.9.4";
const LEAFLET_CSS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.css`;
const LEAFLET_JS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.js`;
const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";

/** Подгружает Leaflet с CDN один раз, резолвит window.L. (mirror SourcesMap.tsx) */
function loadLeaflet(): Promise<any> {
  return new Promise((resolve, reject) => {
    const w = window as any;
    if (w.L) {
      resolve(w.L);
      return;
    }
    if (!document.querySelector(`link[data-leaflet]`)) {
      const link = document.createElement("link");
      link.rel = "stylesheet";
      link.href = LEAFLET_CSS;
      link.integrity = LEAFLET_CSS_SRI;
      link.crossOrigin = "anonymous";
      link.setAttribute("data-leaflet", "1");
      document.head.appendChild(link);
    }
    const existing = document.querySelector<HTMLScriptElement>(`script[data-leaflet]`);
    if (existing) {
      existing.addEventListener("load", () => resolve(w.L));
      existing.addEventListener("error", () => reject(new Error("leaflet load failed")));
      return;
    }
    const script = document.createElement("script");
    script.src = LEAFLET_JS;
    script.integrity = LEAFLET_JS_SRI;
    script.crossOrigin = "anonymous";
    script.setAttribute("data-leaflet", "1");
    script.onload = () => resolve(w.L);
    script.onerror = () => reject(new Error("leaflet load failed"));
    document.body.appendChild(script);
  });
}

// Overview zoom: close enough to recognise the actual building on a 560×152
// box without feeling zoomed-in on bare rooftops (SourcesMap's multi-pin
// overlay map fits bounds instead — this is a single-point locator, not an
// exploration map).
const HERO_MAP_ZOOM = 16;

interface HeroMiniMapProps {
  lat: number | null;
  lon: number | null;
}

/**
 * Small OSM/Leaflet locator map centred on the subject address, replacing the
 * old static building.png photo. Deliberately near-non-interactive — this is
 * a 560×152 "you are here" badge, not an explorable map (ParamsPanel/
 * SourcesMap already cover that): dragging/zoomControl/scroll-zoom/dbl-click
 * zoom are all off so the box reads as a locator, not a broken-feeling mini
 * map, and never steals the page's scroll or click focus.
 */
function HeroMiniMap({ lat, lon }: HeroMiniMapProps) {
  const mapRef = useRef<HTMLDivElement>(null);
  const [mapError, setMapError] = useState(false);

  useEffect(() => {
    if (lat == null || lon == null) return;
    let map: any = null;
    let cancelled = false;
    // Reset a stale failure from a previous address/CDN hiccup before this
    // attempt — otherwise one bad load latches mapError forever (caught in
    // review on a sibling PR) and every later estimate shows the placeholder
    // even once the CDN is reachable again.
    setMapError(false);

    loadLeaflet()
      .then((L) => {
        if (cancelled || !mapRef.current) return;
        map = L.map(mapRef.current, {
          scrollWheelZoom: false, // embedded in the page — must not steal page scroll
          dragging: false, // locator badge, not an explorable map
          touchZoom: false,
          doubleClickZoom: false,
          zoomControl: false, // no room for +/- controls at this size
          keyboard: false,
        }).setView([lat, lon], HERO_MAP_ZOOM);
        L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
          attribution: "© OpenStreetMap",
          maxZoom: 19,
        }).addTo(map);
        L.circleMarker([lat, lon], {
          radius: 8,
          color: "#fff",
          weight: 3,
          fillColor: tokens.accent,
          fillOpacity: 1,
        }).addTo(map);
        setTimeout(() => map && map.invalidateSize(), 120);
      })
      .catch(() => {
        if (!cancelled) setMapError(true);
      });

    return () => {
      cancelled = true;
      if (map) map.remove();
    };
  }, [lat, lon]);

  // Honest empty states — no coordinates yet (fresh/un-geocoded estimate) or
  // the CDN failed — never a blank rectangle or a broken-image icon.
  if (lat == null || lon == null || mapError) {
    return (
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          background: tokens.mapBg,
          color: tokens.muted,
          fontSize: 11,
          textAlign: "center",
          // Left padding clears the address card's lane (left:16 + width:182 =
          // 198, plus a gutter). The card sits at z-index 750 and would
          // otherwise cover this centred text — reported on prod right after
          // the z-index fix landed. Centring happens inside the content box,
          // so the text now centres in the free area to the right of the card.
          padding: "0 20px 0 214px",
        }}
      >
        {lat == null || lon == null
          ? "Карта появится после расчёта адреса"
          : "Не удалось загрузить карту"}
      </div>
    );
  }

  return (
    <div
      ref={mapRef}
      style={{ position: "absolute", inset: 0, background: tokens.mapBg }}
      aria-label="Расположение объекта на карте"
    />
  );
}
/* eslint-enable @typescript-eslint/no-explicit-any */

interface HeroBarProps {
  data?: HeroBarData;
  estimateId?: string | null;
  // M4: when false (no real, sufficient estimate yet) the estimate-dependent
  // meta/controls — the «ДЕЙСТВИТЕЛЕН ДО» validity line and «КАК РАССЧИТАНО» —
  // are hidden, mirroring how the PDF control already gates on estimate presence.
  hasEstimate: boolean;
  onOpenInfo: () => void;
  // #2275 mobile quick-view: a real fluid layout instead of the fixed-width
  // desktop one — meta/buttons stack, the locator mini-map (and the
  // address/coef card baked into it) is dropped since it assumes a 560×152 box
  // that cannot reflow. «КАК РАССЧИТАНО» still opens the same location-index
  // drawer, so no functionality is lost, only the redundant map-card copy.
  compact?: boolean;
}

const pdfBtnStyle: CSSProperties = {
  flex: 1,
  height: 50,
  background: tokens.surface.w55,
  border: `1px solid ${tokens.line}`,
  borderRadius: 7,
  display: "flex",
  alignItems: "center",
  gap: 11,
  padding: "0 16px",
  cursor: "pointer",
  fontFamily: tokens.font.sans,
  color: tokens.ink,
  textDecoration: "none",
  transition: "all .15s",
};

// M4 — once a result is on screen the primary action is «СКАЧАТЬ PDF-ОТЧЁТ»
// (re-running the estimate is secondary), so the download becomes the single
// filled/primary button and «ОЦЕНИТЬ КВАРТИРУ» demotes to outline (ParamsPanel).
const pdfFilledStyle: CSSProperties = {
  ...pdfBtnStyle,
  background: `linear-gradient(90deg, ${tokens.accentDeep}, ${tokens.accent})`,
  border: `1px solid ${tokens.accent}`,
  color: tokens.onAccent,
  boxShadow: "0 2px 10px rgba(46,139,255,.25)",
};

export default function HeroBar({
  data = HERO_FIXTURE,
  estimateId,
  hasEstimate,
  onOpenInfo,
  compact = false,
}: HeroBarProps) {
  const pdfHref = hasEstimate ? pdfDownloadHref(estimateId) : null;
  // A downloadable report exists ⇔ the estimate is ready ⇒ the PDF button is the
  // filled/primary CTA (M4). Otherwise it stays a disabled outline.
  const pdfFilled = Boolean(pdfHref);
  const pdfBtnInner = (filled: boolean) => {
    const frame = filled ? "#fff" : "#2e8bff";
    const rule = filled ? "rgba(255,255,255,.75)" : "#6f8195";
    return (
      <>
        <svg
          width="18"
          height="20"
          viewBox="0 0 18 20"
          fill="none"
          aria-hidden="true"
        >
          <path d="M2 1h9l5 5v13H2z" stroke={frame} strokeWidth="1.2" />
          <path d="M11 1v5h5" stroke={frame} strokeWidth="1.2" />
          <line x1="5" y1="11" x2="13" y2="11" stroke={rule} />
          <line x1="5" y1="14" x2="13" y2="14" stroke={rule} />
        </svg>
        <span style={{ fontSize: 11, fontWeight: 600, letterSpacing: "1px" }}>
          СКАЧАТЬ PDF-ОТЧЁТ
        </span>
      </>
    );
  };

  return (
    <div
      style={{
        display: "flex",
        flexDirection: compact ? "column" : "row",
        gap: compact ? 16 : 30,
        padding: compact ? "0" : "14px 2px 12px",
        flex: "0 0 auto",
      }}
    >
      <style>{`
        .hero-pdf-btn:hover { border-color: ${tokens.accent}; background: rgba(46,139,255,.07); }
        .hero-pdf-btn:active { transform: translateY(1px); }
        .hero-pdf-btn-filled { transition: all .18s; }
        .hero-pdf-btn-filled:hover { background: ${tokens.accentDeep}; box-shadow: 0 6px 22px rgba(46,139,255,.4); }
        .hero-pdf-btn-filled:active { transform: translateY(1px); }
        .hero-calc-btn:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
        .hero-coef-row:hover { background: rgba(46,139,255,.09); }
      `}</style>

      {/* LEFT: meta + buttons */}
      <div
        style={{
          flex: 1,
          minWidth: 0,
          display: "flex",
          flexDirection: "column",
          justifyContent: "center",
          gap: compact ? 14 : 22,
        }}
      >
        <div
          style={{
            display: "flex",
            flexWrap: compact ? "wrap" : "nowrap",
            gap: compact ? 18 : 26,
            rowGap: compact ? 8 : undefined,
            alignItems: "flex-start",
          }}
        >
          <div>
            <div
              style={{
                fontSize: 9,
                letterSpacing: "2px",
                color: tokens.muted2,
                marginBottom: 6,
              }}
            >
              ОТЧЁТ
            </div>
            <div
              style={{
                fontFamily: tokens.font.mono,
                fontSize: 14,
                color: tokens.ink,
              }}
            >
              {data.report.id}
            </div>
          </div>
          <div
            style={{
              borderLeft: `1px solid ${tokens.lineSoft}`,
              paddingLeft: 26,
            }}
          >
            <div
              style={{
                fontSize: 9,
                letterSpacing: "2px",
                color: tokens.muted2,
                marginBottom: 6,
              }}
            >
              ДАТА
            </div>
            <div
              style={{
                fontFamily: tokens.font.mono,
                fontSize: 14,
                color: tokens.ink,
              }}
            >
              {data.report.date}
            </div>
          </div>
          {hasEstimate && (
            <div
              style={{
                borderLeft: `1px solid ${tokens.lineSoft}`,
                paddingLeft: 26,
              }}
            >
              <div
                style={{
                  fontSize: 9,
                  letterSpacing: "2px",
                  color: tokens.muted2,
                  marginBottom: 6,
                }}
              >
                ДЕЙСТВИТЕЛЕН ДО
              </div>
              <div
                style={{
                  fontFamily: tokens.font.mono,
                  fontSize: 14,
                  color: tokens.ink,
                }}
              >
                {data.report.validUntil}
              </div>
            </div>
          )}
        </div>

        <div
          style={{
            display: "flex",
            flexDirection: compact ? "column" : "row",
            gap: 12,
            width: "100%",
            maxWidth: compact ? "none" : 440,
          }}
        >
          {pdfHref ? (
            <a
              className={pdfFilled ? "hero-pdf-btn-filled" : "hero-pdf-btn"}
              href={pdfHref}
              target="_blank"
              rel="noopener noreferrer"
              style={pdfFilled ? pdfFilledStyle : pdfBtnStyle}
            >
              {pdfBtnInner(pdfFilled)}
            </a>
          ) : (
            <button
              type="button"
              disabled
              title="Отчёт станет доступен после расчёта оценки"
              style={{ ...pdfBtnStyle, cursor: "not-allowed", opacity: 0.45 }}
            >
              {pdfBtnInner(false)}
            </button>
          )}
          {hasEstimate && (
            <button
              type="button"
              className="hero-calc-btn"
              onClick={onOpenInfo}
              style={{
                flex: 1,
                height: 50,
                background: tokens.surface.w40,
                border: `1px solid ${tokens.line}`,
                borderRadius: 7,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                cursor: "pointer",
                fontFamily: tokens.font.sans,
                color: tokens.muted,
                fontSize: 11,
                fontWeight: 600,
                letterSpacing: "1.5px",
                transition: "all .15s",
              }}
            >
              КАК РАССЧИТАНО
            </button>
          )}
        </div>
      </div>

      {/* RIGHT: locator mini-map + overlays — dropped in compact mode (#2275):
          the box is a fixed 560×152 with several absolutely-positioned
          children (address card) pinned to that size, so it cannot reflow to
          a phone width. «КАК РАССЧИТАНО» above still opens the same
          location-index drawer, so no functionality is lost.
          User-reported bug: this used to be a single static building.png
          photo shown for EVERY estimate regardless of the real address (a
          user could be looking at someone else's building) — replaced with a
          real Leaflet/OSM map centred on the subject's own coordinates. */}
      {!compact && (
        <div
          style={{
            position: "relative",
            width: 560,
            height: 152,
            flex: "0 0 auto",
            border: `1px solid ${tokens.line3}`,
            borderRadius: 6,
            overflow: "hidden",
            background: tokens.mapBg,
            boxShadow: "0 6px 26px rgba(40,80,130,.10)",
          }}
        >
          <HeroMiniMap lat={data.object.lat} lon={data.object.lon} />

          {/* Left fade so the address card (below) stays legible over busy
            map tiles instead of a floating card with no visual anchor — kept
            from the old photo styling, where it served the same purpose.
            pointerEvents:none so it never blocks map interaction/attribution
            underneath. The old bottom vignette + scanning HUD sweep line are
            dropped: both existed purely to stylise/recede the stock photo and
            have no equivalent purpose over a live basemap. */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              background:
                "linear-gradient(90deg,rgba(238,244,250,.96) 0%,rgba(238,244,250,.5) 22%,transparent 40%)",
              pointerEvents: "none",
              // Above Leaflet's tile pane (200) / overlay pane (400); below the
              // marker (600) so the subject pin still reads through the fade.
              zIndex: 500,
            }}
          />

          {/* address card */}
          <div
            style={{
              position: "absolute",
              left: 16,
              top: "50%",
              transform: "translateY(-50%)",
              width: 182,
              // MUST be set explicitly. .leaflet-container is position:relative
              // with z-index:auto, so it does NOT open a stacking context — its
              // internal panes (tiles 200 … popup 700) compete directly with
              // these siblings, and an auto/0 card is painted UNDER the map.
              // Shipped without this in #2529 and the address + location figure
              // vanished from the hero on prod. 750 clears every pane; the
              // attribution control (800) sits bottom-right and never overlaps
              // this left-anchored card, so its link stays clickable.
              zIndex: 750,
              background: tokens.surface.w72,
              backdropFilter: "blur(5px)",
              border: `1px solid ${tokens.line}`,
              borderRadius: 5,
              padding: "11px 13px",
            }}
          >
            <div
              style={{
                fontSize: 9,
                letterSpacing: "2.5px",
                color: tokens.muted2,
                marginBottom: 3,
              }}
            >
              АДРЕС
            </div>
            <div style={{ fontSize: 14, fontWeight: 600, lineHeight: 1.3 }}>
              {data.object.address}
              <br />
              <span
                style={{ fontSize: 11, fontWeight: 400, color: tokens.muted }}
              >
                {data.object.city}
              </span>
            </div>
            <div
              style={{
                height: 1,
                background: tokens.lineSoft,
                margin: "8px 0",
              }}
            />
            <div
              // role=button + tabIndex + Enter/Space (#2264 C6): keyboard-operable
              // without switching the element to a <button> (which, with preflight
              // off, would need a UA-chrome reset and risk the row's exact box
              // geometry / negative-margin hover bleed). Opens the methodology drawer.
              role="button"
              tabIndex={0}
              className="hero-coef-row"
              onClick={onOpenInfo}
              onKeyDown={(e) => {
                if (e.key === "Enter" || e.key === " ") {
                  e.preventDefault();
                  onOpenInfo();
                }
              }}
              // Honest framing (post location-coef rewrite, see
              // backend/app/services/location_index.py): this is a comparison
              // vs. the city median, not a price multiplier, and it never
              // affects the quoted estimate. title= is a plain hover tooltip
              // (zero layout cost) carrying that caveat since the compact pill
              // has no room to spell it out inline.
              aria-label="Локация относительно города — справочно, не влияет на оценку"
              title="Сравнение медианы ₽/м² района и города. На итоговую оценку не влияет."
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                cursor: "pointer",
                borderRadius: 4,
                margin: "-3px -5px",
                padding: "3px 5px",
                transition: "background .15s",
              }}
            >
              <span
                style={{
                  fontSize: "8.5px",
                  letterSpacing: "1.5px",
                  color: tokens.muted2,
                }}
              >
                ЛОКАЦИЯ
              </span>
              <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
                {data.object.locationIndexOk ? (
                  <span
                    style={{
                      fontFamily: tokens.font.mono,
                      fontSize: 15,
                      fontWeight: 500,
                      color: tokens.accent,
                    }}
                  >
                    {data.object.locationIndexLabel}
                  </span>
                ) : (
                  <span
                    style={{
                      fontSize: "9px",
                      letterSpacing: ".5px",
                      // body2 (not muted2): муть на badgeTint давала 2.3:1 < AA;
                      // body2 на badgeTint ≈ 6.3:1 — читаемо, остаётся «приглушённым».
                      color: tokens.body2,
                      background: tokens.badgeTint,
                      border: `1px solid ${tokens.line2}`,
                      borderRadius: 10,
                      padding: "1px 8px",
                    }}
                  >
                    {/* Distinct honest reasons instead of one blank dash — see
                      mapObject/locationIndexBadge (./mappers.ts): "вне ЕКБ"
                      (out_of_coverage — index only covers Yekaterinburg),
                      "мало данных" (insufficient_data — too few comparable
                      listings), "нет данных" (not fetched yet for this
                      estimate). Full explanation lives in the drawer below. */}
                    {data.object.locationIndexLabel}
                  </span>
                )}
                <span
                  style={{
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    width: 14,
                    height: 14,
                    border: `1px solid ${tokens.accent}`,
                    borderRadius: "50%",
                    fontSize: "8.5px",
                    color: tokens.accent,
                    fontWeight: 600,
                  }}
                  aria-hidden="true"
                >
                  ?
                </span>
              </span>
            </div>
          </div>

          {/* Fix #3 (audit) — the "0/25/50/75/100м" distance ruler here was a
              hardcoded tick scale over a generic stock photo with no real
              measurement behind it (not tied to any actual distance/scale
              value). Removed rather than fabricating a scale. */}

          {/* corner brackets — decorative HUD framing only; pointerEvents:none
            so they never sit on top of the map's own bottom-right OSM
            attribution link. */}
          <div
            style={{
              position: "absolute",
              left: 8,
              top: 8,
              width: 14,
              height: 14,
              borderLeft: `1.5px solid ${tokens.accent}`,
              borderTop: `1.5px solid ${tokens.accent}`,
              pointerEvents: "none",
            }}
          />
          <div
            style={{
              position: "absolute",
              right: 8,
              top: 8,
              width: 14,
              height: 14,
              borderRight: `1.5px solid ${tokens.accent}`,
              borderTop: `1.5px solid ${tokens.accent}`,
              pointerEvents: "none",
            }}
          />
          <div
            style={{
              position: "absolute",
              left: 8,
              bottom: 8,
              width: 14,
              height: 14,
              borderLeft: `1.5px solid ${tokens.accent}`,
              borderBottom: `1.5px solid ${tokens.accent}`,
              pointerEvents: "none",
            }}
          />
          <div
            style={{
              position: "absolute",
              right: 8,
              bottom: 8,
              width: 14,
              height: 14,
              borderRight: `1.5px solid ${tokens.accent}`,
              borderBottom: `1.5px solid ${tokens.accent}`,
              pointerEvents: "none",
            }}
          />
        </div>
      )}
    </div>
  );
}
