"use server";

import "server-only";
import { after } from "next/server";
import { getLexClient } from "@/data/lex-client";
import { generateTid } from "./tid";
import { loadCurrentUser } from "./queries";
import { refresh } from "next/cache";
import * as trail from "@/lib/lexicons/app/sidetrail/trail";
import * as walk from "@/lib/lexicons/app/sidetrail/walk";
import * as completion from "@/lib/lexicons/app/sidetrail/completion";
import * as uploadBlob from "@/lib/lexicons/com/atproto/repo/uploadBlob";
import type { l } from "@atproto/lex";
import {
  getDb,
  trails,
  walks as walksTable,
  completions as completionsTable,
  type TrailRecord,
  type WalkRecord,
  type CompletionRecord,
} from "@/data/db";
import { eq, and } from "drizzle-orm";
import { cidForLex } from "@atproto/lex-cbor";

// ============================================================================
// Helpers
// ============================================================================

function extractRkey(uri: string): string {
  const parts = uri.split("/");
  return parts[parts.length - 1];
}

// ============================================================================
// Walk Mutations
// ============================================================================

export async function startWalk(trailUri: string, trailCid: string): Promise<void> {
  const client = await getLexClient();
  const db = getDb();
  const authorDid = client.assertDid;

  const [existingWalk] = await db
    .select({ uri: walksTable.uri })
    .from(walksTable)
    .where(and(eq(walksTable.authorDid, authorDid), eq(walksTable.trailUri, trailUri)))
    .limit(1);

  if (existingWalk) {
    return;
  }

  const [trailRow] = await db.select().from(trails).where(eq(trails.uri, trailUri)).limit(1);

  if (!trailRow) {
    throw new Error("Trail not found");
  }

  const trailData = trailRow.record;

  if (!trailData.stops || trailData.stops.length === 0) {
    throw new Error("Trail has no stops");
  }

  const now = new Date().toISOString();
  const firstStopTid = trailData.stops[0].tid;

  const rkey = generateTid();
  const uri = `at://${authorDid}/app.sidetrail.walk/${rkey}`;

  const walkRecord: WalkRecord = {
    $type: "app.sidetrail.walk",
    trail: { uri: trailUri, cid: trailCid },
    visitedStops: [firstStopTid],
    createdAt: now,
    updatedAt: now,
  };

  const cid = (await cidForLex(walkRecord)).toString();

  await db.insert(walksTable).values({
    uri,
    cid,
    authorDid,
    rkey,
    trailUri,
    record: walkRecord,
    createdAt: new Date(now),
  });

  after(async () => {
    await client.create(
      walk.main,
      {
        trail: {
          uri: trailUri as l.AtUri,
          cid: trailCid,
        },
        visitedStops: [firstStopTid],
        createdAt: now,
        updatedAt: now,
      },
      { rkey },
    );
  });
}

export async function visitStop(walkUri: string, stopTid: string): Promise<void> {
  const client = await getLexClient();
  const db = getDb();

  const walkRkey = extractRkey(walkUri);

  const [walkRow] = await db.select().from(walksTable).where(eq(walksTable.uri, walkUri)).limit(1);

  if (!walkRow) {
    throw new Error("Walk not found");
  }

  const walkData = walkRow.record;

  const visitedStops = walkData.visitedStops.includes(stopTid)
    ? [...walkData.visitedStops.filter((t) => t !== stopTid), stopTid]
    : [...walkData.visitedStops, stopTid];
  const now = new Date().toISOString();

  const updatedRecord: WalkRecord = {
    $type: "app.sidetrail.walk",
    trail: { uri: walkData.trail.uri, cid: walkData.trail.cid },
    visitedStops,
    createdAt: walkData.createdAt,
    updatedAt: now,
  };

  await db.update(walksTable).set({ record: updatedRecord }).where(eq(walksTable.uri, walkUri));

  after(async () => {
    await client.put(
      walk.main,
      {
        trail: {
          uri: walkData.trail.uri as l.AtUri,
          cid: walkData.trail.cid,
        },
        visitedStops,
        createdAt: walkData.createdAt as l.Datetime,
        updatedAt: now,
      },
      { rkey: walkRkey },
    );
  });

  refresh();
}

export async function completeTrail(walkUri: string): Promise<void> {
  const client = await getLexClient();
  const db = getDb();
  const authorDid = client.assertDid;

  const [walkRow] = await db.select().from(walksTable).where(eq(walksTable.uri, walkUri)).limit(1);

  if (!walkRow) {
    throw new Error("Walk not found");
  }

  const walkData = walkRow.record;
  const trailUri = walkData.trail.uri;
  const now = new Date().toISOString();

  const allWalks = await db
    .select()
    .from(walksTable)
    .where(and(eq(walksTable.trailUri, trailUri), eq(walksTable.authorDid, authorDid)));

  const completionRkey = generateTid();
  const completionUri = `at://${authorDid}/app.sidetrail.completion/${completionRkey}`;

  const completionRecord: CompletionRecord = {
    $type: "app.sidetrail.completion",
    trail: { uri: trailUri, cid: walkData.trail.cid },
    createdAt: now,
  };

  const cid = (await cidForLex(completionRecord)).toString();

  await db.insert(completionsTable).values({
    uri: completionUri,
    cid,
    authorDid,
    rkey: completionRkey,
    trailUri,
    record: completionRecord,
    createdAt: new Date(now),
  });

  await db
    .delete(walksTable)
    .where(and(eq(walksTable.trailUri, trailUri), eq(walksTable.authorDid, authorDid)));

  after(async () => {
    await client.create(
      completion.main,
      {
        trail: {
          uri: trailUri as l.AtUri,
          cid: walkData.trail.cid,
        },
        createdAt: now,
      },
      { rkey: completionRkey },
    );
    for (const w of allWalks) {
      await client.delete(walk.main, { rkey: w.rkey });
    }
  });
}

export async function abandonWalk(walkUri: string): Promise<void> {
  const client = await getLexClient();
  const db = getDb();
  const authorDid = client.assertDid;

  const [walkRow] = await db.select().from(walksTable).where(eq(walksTable.uri, walkUri)).limit(1);

  if (!walkRow) {
    return;
  }

  const trailUri = walkRow.record.trail.uri;

  const allWalks = await db
    .select()
    .from(walksTable)
    .where(and(eq(walksTable.trailUri, trailUri), eq(walksTable.authorDid, authorDid)));

  await db
    .delete(walksTable)
    .where(and(eq(walksTable.trailUri, trailUri), eq(walksTable.authorDid, authorDid)));

  after(async () => {
    for (const w of allWalks) {
      await client.delete(walk.main, { rkey: w.rkey });
    }
  });
}

// ============================================================================
// Completion Mutations
// ============================================================================

export async function deleteCompletion(completionUri: string): Promise<void> {
  const client = await getLexClient();
  const db = getDb();

  const completionRkey = extractRkey(completionUri);

  await db.delete(completionsTable).where(eq(completionsTable.uri, completionUri));

  after(async () => {
    await client.delete(completion.main, { rkey: completionRkey });
  });
}

export async function forgetTrail(trailUri: string): Promise<void> {
  const client = await getLexClient();
  const db = getDb();

  const authorDid = client.assertDid;

  const matchingWalks = await db
    .select()
    .from(walksTable)
    .where(and(eq(walksTable.trailUri, trailUri), eq(walksTable.authorDid, authorDid)));

  const matchingCompletions = await db
    .select()
    .from(completionsTable)
    .where(and(eq(completionsTable.trailUri, trailUri), eq(completionsTable.authorDid, authorDid)));

  await db
    .delete(walksTable)
    .where(and(eq(walksTable.trailUri, trailUri), eq(walksTable.authorDid, authorDid)));

  await db
    .delete(completionsTable)
    .where(and(eq(completionsTable.trailUri, trailUri), eq(completionsTable.authorDid, authorDid)));

  after(async () => {
    for (const w of matchingWalks) {
      await client.delete(walk.main, { rkey: w.rkey });
    }
    for (const c of matchingCompletions) {
      await client.delete(completion.main, { rkey: c.rkey });
    }
  });
}

// ============================================================================
// Trail Mutations
// ============================================================================

type PublishDraftResult =
  | { success: true; uri: string; rkey: string; handle: string }
  | { success: false; errors: string[]; inlineErrors: Record<string, string> };

type ExternalEmbedInput = {
  uri: string;
  title?: string;
  description?: string;
  thumb?: string; // URL string from draft
};

export async function publishDraft(draftData: {
  title: string;
  description: string;
  stops: Array<{
    tid: string;
    title: string;
    content: string;
    buttonText?: string;
    external?: ExternalEmbedInput;
  }>;
  accentColor: string;
  backgroundColor: string;
}): Promise<PublishDraftResult> {
  const errors: string[] = [];
  const inlineErrors: Record<string, string> = {};

  if (!draftData.title.trim()) {
    errors.push("trail needs a title");
  }

  if (!draftData.description.trim()) {
    errors.push("trail needs a description");
  }

  if (draftData.stops.length < 2) {
    errors.push("trails need at least 2 stops to be published");
  }

  if (draftData.stops.length > 12) {
    errors.push("trails can have a maximum of 12 stops");
  }

  for (let i = 0; i < draftData.stops.length; i++) {
    const stop = draftData.stops[i];
    const stopErrors: string[] = [];

    if (!stop.title.trim()) {
      stopErrors.push("needs a title");
    }

    if (!stop.content.trim()) {
      stopErrors.push("needs content");
    }

    if (stopErrors.length > 0) {
      inlineErrors[stop.tid] = stopErrors.join(", ");
      errors.push(`stop ${i + 1} ${stopErrors.join(", ")}`);
    }
  }

  if (errors.length > 0) {
    return {
      success: false,
      errors,
      inlineErrors,
    };
  }

  const client = await getLexClient();
  const db = getDb();
  const now = new Date().toISOString();

  const stops = await Promise.all(
    draftData.stops.map(async (stop) => {
      let thumbBlob: l.BlobRef | undefined = undefined;

      if (stop.external?.thumb) {
        try {
          const response = await fetch(stop.external.thumb);
          if (response.ok) {
            const blob = await response.blob();
            const buffer = await blob.arrayBuffer();

            const uploadResponse = await client.call(uploadBlob.main, new Uint8Array(buffer));
            thumbBlob = uploadResponse.blob as l.BlobRef;
          }
        } catch (error) {
          console.error("Failed to upload thumbnail:", error);
        }
      }

      const stopRecord: trail.Stop = {
        tid: generateTid(),
        title: stop.title,
        content: stop.content,
      };

      if (stop.buttonText) {
        stopRecord.buttonText = stop.buttonText;
      }

      if (stop.external) {
        const external: trail.External = {
          uri: stop.external.uri as l.Uri,
        };
        if (stop.external.title) {
          external.title = stop.external.title;
        }
        if (stop.external.description) {
          external.description = stop.external.description;
        }
        if (thumbBlob) {
          external.thumb = thumbBlob;
        }
        stopRecord.external = external;
      }

      return stopRecord;
    }),
  );

  const rkey = generateTid();
  const authorDid = client.assertDid;
  const uri = `at://${authorDid}/app.sidetrail.trail/${rkey}`;

  const currentUser = await loadCurrentUser();
  const handle = currentUser?.handle ?? "unknown";

  const trailRecord: TrailRecord = {
    $type: "app.sidetrail.trail",
    title: draftData.title,
    description: draftData.description,
    stops: stops.map((s) => {
      const stop: TrailRecord["stops"][0] = {
        tid: s.tid,
        title: s.title,
        content: s.content,
      };
      if (s.buttonText) stop.buttonText = s.buttonText;
      if (s.external) {
        const external: NonNullable<TrailRecord["stops"][0]["external"]> = {
          uri: s.external.uri,
        };
        if (s.external.title) external.title = s.external.title;
        if (s.external.description) external.description = s.external.description;
        if (s.external.thumb) external.thumb = s.external.thumb as typeof external.thumb;
        stop.external = external;
      }
      return stop;
    }),
    accentColor: draftData.accentColor,
    backgroundColor: draftData.backgroundColor,
    createdAt: now,
  };

  const cid = (await cidForLex(trailRecord)).toString();

  await db.insert(trails).values({
    uri,
    cid,
    authorDid,
    rkey,
    record: trailRecord,
    createdAt: new Date(now),
  });

  after(async () => {
    await client.create(
      trail.main,
      {
        title: draftData.title,
        description: draftData.description,
        stops,
        accentColor: draftData.accentColor,
        backgroundColor: draftData.backgroundColor,
        createdAt: now,
      },
      { rkey },
    );
  });

  return { success: true, uri, rkey, handle };
}

export async function deleteTrail(trailUri: string): Promise<void> {
  const client = await getLexClient();
  const db = getDb();

  const trailRkey = extractRkey(trailUri);

  await db.delete(trails).where(eq(trails.uri, trailUri));

  after(async () => {
    await client.delete(trail.main, { rkey: trailRkey });
  });
}
