"use client";

// Smart Gallery data layer. Serves EITHER a device-local mock (when the Shell isn't configured — the
// demo keeps working offline) OR the live be-crm data door (crm.gallery.*), behind one StorageAdapter
// so the embedded @photo-gallery/sdk is mode-agnostic. Tenant scoping, comment authorship and byte
// authorization are all enforced server-side; this is just glue.
//
// Live contract (be-crm):
//   query crm.gallery.state.load             {}                                   -> PersistedState
//   cmd   crm.gallery.state.apply            StateChanges                         -> { upserted, removed }
//   cmd   crm.gallery.media.presignUpload    { mediaId, mime, sizeBytes, filename? } -> { ref, uploadUrl, method, headers? }
//   cmd   crm.gallery.media.presignDownload  { refs: string[] }                   -> { urls, expiresInSeconds }
//   query crm.gallery.stats                  {}                                   -> { items, albums, people, bytes }
//   query crm.gallery.lock.status            {}                                   -> { hasPassword }
//   cmd   crm.gallery.lock.set               { password: string | null }          -> { hasPassword }
//   cmd   crm.gallery.lock.verify            { password: string }                 -> { ok }
//
// BYTES NEVER PASS THROUGH be-crm OR THE BFF. `putMedia` mints a short-lived signed PUT URL and the
// browser transfers straight to object storage — the same rule media-api.ts follows for attachments.

import { useMemo } from "react";
import { useAppShell, useAuth } from "@abe-kap/appshell-sdk/react";
import {
  createLocalStorageAdapter,
  type GalleryFeatures,
  type GalleryUser,
  type MediaItem,
  type PersistedState,
  type StateChanges,
  type StorageAdapter,
  type StoredBlob,
  type ThemeTokens,
} from "@photo-gallery/sdk";
import { user as demoUser } from "@/components/dashboard/account-data";
import { useMyAccess } from "@/lib/access";
import { isShellConfigured } from "./appshell";

const SHELL = isShellConfigured();

/** Device-local store key for demo mode. Namespaced so it can't collide with the SDK's own demo. */
const LOCAL_STORE_KEY = "lup:smart-gallery:v1";

/** be-crm caps a single presignDownload at 500 refs — chunk anything larger. */
const PRESIGN_CHUNK = 500;

/** Largest single upload the gallery will attempt (matches be-crm's gallery cap). */
export const MAX_GALLERY_BYTES = 200 * 1024 * 1024;

/* ======================================================================== */
/* Wire types (be-crm shapes)                                               */
/* ======================================================================== */

interface StateLoadDTO {
  media: MediaItem[];
  albums: PersistedState["albums"];
  people: PersistedState["people"];
  labelAliases?: Record<string, string>;
  deletedLabels?: string[];
}

interface PresignUploadDTO {
  ref: string;
  uploadUrl: string;
  method: "PUT";
  headers?: Record<string, string>;
}

interface PresignDownloadDTO {
  urls: Record<string, string>;
  expiresInSeconds: number;
}

/** The subset of the AppShell SDK this module needs — keeps the adapter unit-testable. */
interface DataDoor {
  query<T>(action: string, variables?: Record<string, unknown>): Promise<T>;
  command<T>(action: string, variables?: Record<string, unknown>): Promise<T>;
}

/* ======================================================================== */
/* The live adapter — be-crm data door                                      */
/* ======================================================================== */

function chunk<T>(items: T[], size: number): T[][] {
  const out: T[][] = [];
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
  return out;
}

/**
 * A StorageAdapter backed by the be-crm data door.
 *
 * Metadata (media/albums/people) rides the door as JSON; bytes go direct to object storage via
 * short-lived signed URLs. `storageRef` is the durable handle we persist — `src` is only ever a
 * signed URL with a TTL, so it is re-resolved from the refs on every `load()`.
 */
export function createDataDoorAdapter(sdk: DataDoor): StorageAdapter {
  /** Resolve durable refs → fresh signed GET URLs, in chunks the door will accept. */
  async function resolveRefs(refs: string[]): Promise<Record<string, string>> {
    const unique = [...new Set(refs.filter(Boolean))];
    if (!unique.length) return {};
    const urls: Record<string, string> = {};
    for (const group of chunk(unique, PRESIGN_CHUNK)) {
      const res = await sdk.command<PresignDownloadDTO>("crm.gallery.media.presignDownload", { refs: group });
      Object.assign(urls, res.urls ?? {});
    }
    return urls;
  }

  return {
    name: "crm-data-door",

    async load(): Promise<PersistedState | null> {
      const state = await sdk.query<StateLoadDTO>("crm.gallery.state.load", {});
      const media = state.media ?? [];
      // Signed URLs expire, so `src` is always rebuilt from `storageRef` at load time. Items with no
      // ref (e.g. a seeded remote URL) keep whatever `src` they were stored with.
      const urls = await resolveRefs(media.map((m) => m.storageRef ?? "").filter(Boolean));
      return {
        media: media.map((m) => (m.storageRef && urls[m.storageRef] ? { ...m, src: urls[m.storageRef]! } : m)),
        albums: state.albums ?? [],
        people: state.people ?? [],
        labelAliases: state.labelAliases ?? {},
        deletedLabels: state.deletedLabels ?? [],
        version: 1,
      };
    },

    // Incremental sync is the real path (see applyChanges). `save` only runs if the store ever falls
    // back to whole-state persistence; express it as one big change set so behaviour is identical.
    async save(state: PersistedState): Promise<void> {
      await sdk.command("crm.gallery.state.apply", {
        upsertMedia: state.media,
        upsertAlbums: state.albums,
        upsertPeople: state.people,
        labelAliases: state.labelAliases ?? {},
        deletedLabels: state.deletedLabels ?? [],
      });
    },

    /**
     * Incremental persistence — only the entities that actually changed. This is what makes a shared
     * tenant library safe for concurrent editors: two people touching different photos never clobber
     * each other, because neither sends the other's rows.
     */
    async applyChanges(changes: StateChanges): Promise<void> {
      // StateChanges is a closed interface; the door takes an open variables bag.
      await sdk.command("crm.gallery.state.apply", { ...changes });
    },

    /** Presign → direct PUT → resolve a display URL. Bytes never touch be-crm or the BFF. */
    async putMedia(id: string, blob: Blob, meta: { name: string; mime: string }): Promise<StoredBlob> {
      if (blob.size > MAX_GALLERY_BYTES) {
        throw new Error(`File is too large (max ${Math.floor(MAX_GALLERY_BYTES / (1024 * 1024))} MB).`);
      }
      const mime = meta.mime || blob.type || "application/octet-stream";
      const presigned = await sdk.command<PresignUploadDTO>("crm.gallery.media.presignUpload", {
        mediaId: id,
        mime,
        sizeBytes: blob.size,
        filename: meta.name,
      });
      const res = await fetch(presigned.uploadUrl, {
        method: presigned.method ?? "PUT",
        headers: { "content-type": mime, ...(presigned.headers ?? {}) },
        body: blob,
      });
      if (!res.ok) throw new Error(`Upload failed (${res.status}).`);
      const urls = await resolveRefs([presigned.ref]);
      return { ref: presigned.ref, url: urls[presigned.ref] ?? presigned.ref };
    },
  };
}

/* ======================================================================== */
/* Public hooks                                                             */
/* ======================================================================== */

export interface GalleryStorage {
  /** True when persisting to be-crm; false when running on the device-local demo store. */
  live: boolean;
  adapter: StorageAdapter;
}

/**
 * The storage backend for the embedded gallery. Live against the be-crm data door once the Shell is
 * configured, otherwise a device-local store so the demo works with no backend at all.
 *
 * SHELL is a build-time constant, so this branch is stable across renders (Rules-of-Hooks safe).
 */
export function useGalleryStorage(): GalleryStorage {
  const { sdk } = useAppShell();
  // The adapter identity must be stable — PhotoGallery captures it in a ref on first render.
  return useMemo<GalleryStorage>(
    () =>
      SHELL
        ? { live: true, adapter: createDataDoorAdapter(sdk as unknown as DataDoor) }
        : { live: false, adapter: createLocalStorageAdapter(LOCAL_STORE_KEY) },
    [sdk],
  );
}

/**
 * The signed-in identity handed to the gallery so comments are attributed to a real person rather
 * than a free-text name the author can type themselves.
 *
 * Falls back to the same static demo user the sidebar and topbar use when the Shell isn't wired, so
 * the comment module behaves identically in the demo — the CRM never shows an anonymous author.
 * be-crm re-stamps `authorId` from the PAT on every write regardless, so this value is a display
 * convenience, never the source of authority.
 */
export function useGalleryUser(): GalleryUser {
  const { user, context } = useAuth();
  return useMemo<GalleryUser>(() => {
    if (!user) return { id: demoUser.id, name: demoUser.name };
    const avatarUrl = user.avatarUrl ?? context?.principal?.avatarUrl;
    return {
      id: user.id,
      name: user.displayName || user.email || demoUser.name,
      ...(user.email ? { email: user.email } : {}),
      ...(avatarUrl ? { avatarUrl } : {}),
    };
  }, [user, context]);
}

/* ======================================================================== */
/* Recently Deleted lock                                                    */
/* ======================================================================== */

/**
 * The SDK's `lockProvider` contract, declared here rather than imported so this module keeps
 * compiling against an SDK build that predates the prop. It is structurally identical to
 * `PhotoGalleryProps['lockProvider']`.
 */
export interface GalleryLockProvider {
  status(): Promise<{ hasPassword: boolean }>;
  /** `null` clears the password. */
  set(password: string | null): Promise<void>;
  verify(password: string): Promise<boolean>;
}

/**
 * A server-backed, per-user lock for the Recently Deleted view.
 *
 * Without this the SDK falls back to a device-local `localStorage` hash: the lock exists only on
 * the browser that set it, so the same account on a second device sees an unlocked trash. Backed
 * by `crm.gallery.lock.*`, the password becomes an account property — be-crm stores only a
 * scrypt hash with a per-record random salt, keyed by (tenant, principal), and rate-limits verify.
 *
 * Returns `undefined` in demo mode ON PURPOSE: with no backend there is nowhere to put the hash,
 * and the SDK's own device-local behaviour is the right fallback for a demo.
 *
 * SHELL is a build-time constant, so this branch is stable across renders (Rules-of-Hooks safe).
 */
export function useGalleryLockProvider(): GalleryLockProvider | undefined {
  const { sdk } = useAppShell();
  return useMemo<GalleryLockProvider | undefined>(() => {
    if (!SHELL) return undefined;
    const door = sdk as unknown as DataDoor;
    return {
      status: () => door.query<{ hasPassword: boolean }>("crm.gallery.lock.status", {}),
      // The SDK's contract returns void; the door's `{ hasPassword }` is redundant after a set.
      set: async (password) => {
        await door.command("crm.gallery.lock.set", { password });
      },
      // A wrong password is a normal `{ ok: false }`, not an error. A 403 (the verify lockout)
      // still throws, which is what the SDK's prompt should surface.
      verify: async (password) => (await door.command<{ ok: boolean }>("crm.gallery.lock.verify", { password })).ok,
    };
  }, [sdk]);
}

/* ======================================================================== */
/* Permission-gated features                                                */
/* ======================================================================== */

/**
 * The gallery capabilities the CRM gates behind team permissions. Each SDK feature toggle
 * (and the whole-view `media.view` gate) maps to one permission id from be-crm's Media group.
 * The SDK still enforces nothing here — hiding a control is UX; be-crm enforces every write via
 * GALLERY_VISIBILITY + tenant scoping regardless of what the UI shows.
 */
const MEDIA_PERMISSIONS = [
  "media.view", "media.upload", "media.capture", "media.edit",
  "media.delete", "media.export", "media.share", "media.map", "media.ai",
] as const;

/** SDK feature toggle → the permission id that unlocks it. */
const FEATURE_PERMISSION: Record<keyof GalleryFeatures, string> = {
  editor: "media.edit",
  camera: "media.capture",
  import: "media.upload",
  export: "media.export",
  sharing: "media.share",
  map: "media.map",
  ai: "media.ai",
};

export interface ResolvedGalleryFeatures {
  /** Feature toggles to hand the SDK's `features` prop, resolved from the caller's permissions. */
  features: GalleryFeatures;
  /** Whether the whole Smart Gallery view should render at all (`media.view`). */
  canView: boolean;
}

/**
 * Resolve the gallery's feature flags + view access from the signed-in user's CRM permissions.
 *
 * Fallback rule (SAFE default — a control is enabled unless the user is positively known to lack it):
 *  1. While access is still loading, stay permissive so features don't flash on then vanish.
 *  2. Superadmins and owners get everything (they hold every permission anyway; this is explicit).
 *  3. A member who has been assigned AT LEAST ONE `media.*` permission is gated precisely: each
 *     feature is enabled only if its mapped permission is in their list.
 *  4. A member with ZERO `media.*` permissions assigned is treated as fully enabled. Roles are not
 *     configured with Media perms until an admin opts in (§5 just made them assignable), so gating a
 *     freshly-seeded member down to nothing would cripple the gallery before anyone could grant them
 *     anything. The "has at least one media.* perm" signal is what flips a role from this permissive
 *     default into precise per-feature gating.
 *
 * Because be-crm's mock access (demo, no Shell) returns superadmin + all perms, the demo shows
 * everything via rule 2.
 */
export function useGalleryFeatures(): ResolvedGalleryFeatures {
  const access = useMyAccess();
  return useMemo<ResolvedGalleryFeatures>(() => {
    const has = (p: string) => access.permissions.includes(p);
    const privileged = access.isSuperadmin || access.roleSlugs.includes("owner");
    const hasAnyMedia = MEDIA_PERMISSIONS.some(has);
    // Permissive whenever we can't (yet) prove the user lacks a permission: loading, privileged, or a
    // member who has no Media perms assigned at all. Otherwise gate precisely on the mapped id.
    const allow = (perm: string) => access.loading || privileged || !hasAnyMedia || has(perm);

    const features: GalleryFeatures = {
      editor: allow(FEATURE_PERMISSION.editor),
      camera: allow(FEATURE_PERMISSION.camera),
      ai: allow(FEATURE_PERMISSION.ai),
      map: allow(FEATURE_PERMISSION.map),
      import: allow(FEATURE_PERMISSION.import),
      export: allow(FEATURE_PERMISSION.export),
      sharing: allow(FEATURE_PERMISSION.sharing),
    };

    return { features, canView: allow("media.view") };
  }, [access]);
}

/* ======================================================================== */
/* Theme bridge                                                             */
/* ======================================================================== */

/**
 * Maps the gallery's design tokens onto the CRM's own CSS variables.
 *
 * Every value is a `var(--crm-token)` reference rather than a literal hex, so the gallery inherits the
 * dashboard's palette through the SAME `[data-theme]` cascade the rest of the app uses — light/dark
 * switch together, and a future palette change reaches the gallery with no code edit here.
 */
export const GALLERY_THEME_TOKENS: ThemeTokens = {
  // Surfaces
  bgLight: "var(--bg)",
  bgDark: "var(--bg)",
  elevatedLight: "var(--panel-3)",
  elevatedDark: "var(--panel-3)",
  sidebarBgLight: "var(--sidebar)",
  sidebarBgDark: "var(--sidebar)",
  toolbarBgLight: "var(--panel)",
  toolbarBgDark: "var(--panel)",
  cardLight: "var(--panel)",
  cardDark: "var(--panel)",
  cardHoverLight: "var(--panel-3)",
  cardHoverDark: "var(--panel-3)",
  menuBgLight: "var(--panel-3)",
  menuBgDark: "var(--panel-3)",

  // Text
  textLight: "var(--text)",
  textDark: "var(--text)",
  textSecondaryLight: "var(--muted)",
  textSecondaryDark: "var(--muted)",
  textTertiaryLight: "var(--faint)",
  textTertiaryDark: "var(--faint)",

  // Lines + washes
  separatorLight: "var(--border)",
  separatorDark: "var(--border)",
  separatorStrongLight: "var(--border-2)",
  separatorStrongDark: "var(--border-2)",
  hoverLight: "var(--track)",
  hoverDark: "var(--track)",
  activeLight: "var(--border-2)",
  activeDark: "var(--border-2)",
  sidebarSelectedLight: "var(--track)",
  sidebarSelectedDark: "var(--track)",
  glassBorderLight: "var(--border-2)",
  glassBorderDark: "var(--border-2)",

  // Brand
  accent: "var(--orange)",
  accentStrongLight: "var(--orange-2)",
  accentStrongDark: "var(--orange-2)",
  accentContrast: "#ffffff",
  dangerLight: "var(--red)",
  dangerDark: "var(--red)",
  tileFav: "var(--red)",
  segmentedActive: "var(--panel-3)",

  // Overlays + chrome
  overlayBg: "rgba(2,2,6,0.94)",
  editorBg: "var(--panel-2)",
  shadowSm: "0 1px 2px rgba(0,0,0,0.18)",
  shadowMdLight: "var(--shadow)",
  shadowMdDark: "var(--shadow)",
  shadowLgLight: "0 30px 70px -20px rgba(15,23,42,0.25)",
  shadowLgDark: "0 30px 70px -20px rgba(0,0,0,0.7)",

  fontFamily: "var(--font)",
  radiusMenu: 14,
  sidebarRadius: 14,
};
