import {
  collection,
  doc,
  onSnapshot,
  addDoc,
  getDoc,
  updateDoc,
  query,
  orderBy,
  limit,
  serverTimestamp,
  Timestamp,
  type DocumentData,
  type FirestoreDataConverter,
  type QueryDocumentSnapshot,
  type SnapshotOptions,
  type Unsubscribe,
} from "firebase/firestore";
import { firestoreDb } from "@/firebase";
import { isContainerType, ParticleSchema } from "@/api/types";
import type { Particle, ParticleType, ParticlePropertiesMap } from "@/api/types";

// --- Converter ---

const particleConverter: FirestoreDataConverter<Particle> = {
  toFirestore(particle: Particle): DocumentData {
    const { id: _id, created_at, updated_at, ...rest } = particle;
    return {
      ...rest,
      created_at: Timestamp.fromDate(created_at),
      ...(updated_at && { updated_at: Timestamp.fromDate(updated_at) }),
    };
  },
  fromFirestore(
    snap: QueryDocumentSnapshot,
    options?: SnapshotOptions,
  ): Particle {
    const raw = snap.data(options);
    if (typeof raw.type !== "string") {
      throw new Error(`Invalid particle type: ${raw.type}`);
    }
    const type = raw.type as ParticleType;
    switch (type) {
      case "stream":
        return ParticleSchema.parse({
          id: snap.id,
          type: raw.type,
          properties: raw.properties,
          created_at: (raw.created_at as Timestamp).toDate(),
          created_by_email: raw.created_by_email,
          updated_at: raw.updated_at ? (raw.updated_at as Timestamp).toDate() : undefined,
          visible_to: raw.visible_to,
          playback_markers: raw.playback_markers
            ? Object.fromEntries(
              Object.entries(raw.playback_markers).map(([key, value]) => [
                key,
                (value as Timestamp).toDate(),
              ]),
            )
            : undefined,
          last_child_created_at: raw.last_child_created_at ? (raw.last_child_created_at as Timestamp).toDate() : undefined,
        });
      case "folder":
        return ParticleSchema.parse({
          id: snap.id,
          type: raw.type,
          properties: raw.properties,
          created_at: (raw.created_at as Timestamp).toDate(),
          created_by_email: raw.created_by_email,
          updated_at: raw.updated_at ? (raw.updated_at as Timestamp).toDate() : undefined,
          visible_to: raw.visible_to,
        });
      case "media":
      case "file":
      case "text":
      case "quest":
      case "paper":
        return ParticleSchema.parse({
          id: snap.id,
          type: raw.type,
          properties: raw.properties,
          created_at: (raw.created_at as Timestamp).toDate(),
          created_by_email: raw.created_by_email,
          updated_at: raw.updated_at ? (raw.updated_at as Timestamp).toDate() : undefined,
        });
      default:
        throw new Error(`Unknown particle type: ${type}`);
    }
  },
};

// --- Typed reference helpers ---

function typedDoc(path: string) {
  return doc(firestoreDb, path).withConverter(particleConverter);
}

function typedCollection(path: string) {
  return collection(firestoreDb, path).withConverter(particleConverter);
}

// --- Exported operations ---

export function subscribeToParticle(
  docPath: string,
  onData: (particle: Particle | null) => void,
  onError: (error: Error) => void,
): Unsubscribe {
  return onSnapshot(
    typedDoc(docPath),
    (snap) => {
      onData(snap.exists() ? snap.data() : null);
    },
    onError,
  );
}

export async function getParticle(docPath: string): Promise<Particle | null> {
  const doc = await getDoc(typedDoc(docPath));
  if (!doc.exists()) {
    return null;
  }

  return doc.data();
}

export function subscribeToParticleChildren(
  collectionPath: string,
  onData: (children: Particle[]) => void,
  onError: (error: Error) => void,
  orderByField: string = "created_at",
  orderDirection: "asc" | "desc" = "desc",
): Unsubscribe {
  const q = query(typedCollection(collectionPath), orderBy(orderByField, orderDirection));
  return onSnapshot(
    q,
    (snap) => {
      onData(snap.docs.map((d) => d.data()));
    },
    onError,
  );
}

export function subscribeToLatestChild(
  collectionPath: string,
  onData: (child: Particle | null) => void,
  onError: (error: Error) => void,
): Unsubscribe {
  const q = query(
    typedCollection(collectionPath),
    orderBy("created_at", "desc"),
    limit(1),
  );
  return onSnapshot(
    q,
    (snap) => {
      onData(snap.empty ? null : snap.docs[0].data());
    },
    onError,
  );
}

// This creates a new particle document with the given properties and returns its ID.
export async function createParticle<T extends ParticleType>(
  collectionPath: string,
  type: T,
  properties: ParticlePropertiesMap[T],
  createdByEmail: string,
  // Must be passed for container types
  visibleTo?: string[],
): Promise<string> {
  if (isContainerType(type) && (!visibleTo || visibleTo.length === 0)) {
    throw new Error(
      `visibleTo is required for container type ${type} and cannot be empty`,
    );
  }

  const particle: Particle = ParticleSchema.parse({
    id: "", // ignored by toFirestore, but needed to satisfy the type
    type,
    properties,
    created_at: new Date(),
    created_by_email: createdByEmail,
    ...(visibleTo ? { visible_to: visibleTo } : {}),
  });
  const ref = await addDoc(typedCollection(collectionPath), particle);
  return ref.id;
}

// This allows updating properties without overwriting the entire properties object
export async function updateParticleProperties<T extends ParticleType>(
  docPath: string,
  properties: Partial<ParticlePropertiesMap[T]>,
): Promise<void> {
  const particleRef = typedDoc(docPath);
  // Take the partial and create a new object with dot notation
  // e.g. { title: "New Title" } becomes { "properties.title": "New Title" }
  const updatedProperties: Record<string, any> = {};
  for (const key in properties) {
    updatedProperties[`properties.${key}`] = properties[key];
  }
  await updateDoc(particleRef, {
    ...updatedProperties,
    updated_at: serverTimestamp(),
  });
}

export async function updateParticleVisibleTo(
  docPath: string,
  visibleTo: string[],
): Promise<void> {
  const particleRef = typedDoc(docPath);
  const particle = await getParticle(docPath);
  if (!particle) {
    throw new Error(`Particle not found at path: ${docPath}`);
  }
  if (!isContainerType(particle.type)) {
    throw new Error(
      `Only container particles can have visible_to field. Particle at ${docPath} is of type ${particle.type}`,
    );
  }

  await updateDoc(particleRef, {
    visible_to: visibleTo,
    updated_at: serverTimestamp(),
  });
}

// CAUTION: use the other type safe update functions in most cases
// There is no checking whether this field actually exists on the particle type, so it can lead to inconsistent data if used incorrectly
export async function updateParticle(
  docPath: string,
  fieldName: string,
  value: any,
): Promise<void> {
  const particleRef = typedDoc(docPath);
  await updateDoc(particleRef, {
    [fieldName]: value,
    updated_at: serverTimestamp(),
  });
}

export async function updateStreamLastChildAt(
  docPath: string,
): Promise<void> {
  const particleRef = typedDoc(docPath);
  await updateDoc(particleRef, {
    last_child_created_at: serverTimestamp(),
    updated_at: serverTimestamp(),
  });
}

export async function updateStreamPlaybackMarker(
  docPath: string,
  humanId: string,
  playbackPositionAt: Date,
): Promise<void> {
  const particleRef = typedDoc(docPath);
  const markerField = `playback_markers.${humanId}`;
  await updateDoc(particleRef, {
    [markerField]: Timestamp.fromDate(playbackPositionAt),
    updated_at: serverTimestamp(),
  });
}
