import { useEffect, useState } from "preact/hooks";

import { MarkdownContent, stripLeadingTitleHeading } from "./MarkdownContent";
import type {
  AuthState,
  CourseCard,
  CourseChapter,
  CourseLesson,
  DiscussionCard,
  DiscussionReply,
  EventCard,
  PostCard,
  PrototypeData,
} from "./types";

function formatTimestamp(value: string): string {
  if (!value) {
    return "Unknown time";
  }

  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return "Unknown time";
  }

  return new Intl.DateTimeFormat("en-US", {
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  }).format(date);
}

function normalizePathname(pathname: string): string {
  if (!pathname || pathname === "/") {
    return "/";
  }

  return pathname.replace(/\/+$/, "") || "/";
}

function parseDiscussionRoute(pathname: string): number | null {
  const match = normalizePathname(pathname).match(/^\/discussions\/(\d+)$/);
  if (!match) {
    return null;
  }

  const issueId = Number(match[1]);
  return Number.isFinite(issueId) ? issueId : null;
}

function isSignInRoute(pathname: string): boolean {
  return normalizePathname(pathname) === "/signin";
}

function isCoursesIndexRoute(pathname: string): boolean {
  return normalizePathname(pathname) === "/courses";
}

function isDiscussionsIndexRoute(pathname: string): boolean {
  return normalizePathname(pathname) === "/discussions";
}

function isActivityRoute(pathname: string): boolean {
  return normalizePathname(pathname) === "/activity";
}

function parseCourseRoute(pathname: string): { owner: string; repo: string } | null {
  const match = normalizePathname(pathname).match(/^\/courses\/([^/]+)\/([^/]+)$/);
  if (!match) {
    return null;
  }

  return {
    owner: decodeURIComponent(match[1]),
    repo: decodeURIComponent(match[2]),
  };
}

function parseLessonRoute(pathname: string): {
  owner: string;
  repo: string;
  chapter: string;
  lesson: string;
} | null {
  const match = normalizePathname(pathname).match(
    /^\/courses\/([^/]+)\/([^/]+)\/lessons\/([^/]+)\/([^/]+)$/,
  );
  if (!match) {
    return null;
  }

  return {
    owner: decodeURIComponent(match[1]),
    repo: decodeURIComponent(match[2]),
    chapter: decodeURIComponent(match[3]),
    lesson: decodeURIComponent(match[4]),
  };
}

function normalizeRouteKey(value: string): string {
  return decodeURIComponent(value).trim().toLowerCase();
}

function findCourseByRoute(
  courses: CourseCard[],
  route: { owner: string; repo: string },
): CourseCard | undefined {
  const routeOwner = normalizeRouteKey(route.owner);
  const routeRepo = normalizeRouteKey(route.repo);
  const routeFullName = `${routeOwner}/${routeRepo}`;

  return courses.find((course) => {
    const [repoOwner = "", repoName = ""] = course.repo.split("/", 2);
    const courseOwner = normalizeRouteKey(course.owner || repoOwner);
    const courseName = normalizeRouteKey(course.name || repoName);
    const courseFullName = normalizeRouteKey(course.repo);

    return (
      (courseOwner === routeOwner && courseName === routeRepo) ||
      courseFullName === routeFullName ||
      courseName === routeRepo
    );
  });
}

function findLessonByRoute(
  course: CourseCard,
  route: { chapter: string; lesson: string },
): { chapter: CourseChapter; lesson: CourseLesson } | undefined {
  const routeChapter = normalizeRouteKey(route.chapter);
  const routeLesson = normalizeRouteKey(route.lesson);

  for (const chapter of course.outline) {
    if (normalizeRouteKey(chapter.slug) !== routeChapter) {
      continue;
    }

    const lesson = chapter.lessons.find((entry) => normalizeRouteKey(entry.slug) === routeLesson);
    if (lesson) {
      return { chapter, lesson };
    }
  }

  return undefined;
}

function usePathname() {
  const [pathname, setPathname] = useState(() => normalizePathname(window.location.pathname));

  useEffect(() => {
    function handlePopState() {
      setPathname(normalizePathname(window.location.pathname));
    }

    window.addEventListener("popstate", handlePopState);
    return () => {
      window.removeEventListener("popstate", handlePopState);
    };
  }, []);

  function navigate(nextPath: string) {
    const nextUrl = new URL(nextPath, window.location.origin);
    const normalized = normalizePathname(nextUrl.pathname);
    const renderedPath = `${normalized}${nextUrl.search}`;
    if (
      normalized === pathname &&
      renderedPath === `${window.location.pathname}${window.location.search}`
    ) {
      return;
    }

    window.history.pushState({}, "", renderedPath);
    window.scrollTo({ top: 0, behavior: "auto" });
    setPathname(normalized);
  }

  return { pathname, navigate };
}

function SectionHeader(props: { title: string }) {
  return (
    <header className="section-header">
      <h2>{props.title}</h2>
    </header>
  );
}

function EmptyState(props: { copy: string }) {
  return <p className="empty-state">{props.copy}</p>;
}

function canUseInteractiveAuth(auth: AuthState): boolean {
  return auth.authenticated && auth.can_reply;
}

function forgejoSignInUrl(returnTo: string): string {
  const target = new URL("/api/auth/forgejo/start", window.location.origin);
  target.searchParams.set("return_to", returnTo);
  return `${target.pathname}${target.search}`;
}

function TopBar(props: {
  auth: AuthState;
  pathname: string;
  onGoHome: () => void;
  onGoCourses: () => void;
  onGoDiscussions: () => void;
  onGoActivity: () => void;
  onGoSignIn: () => void;
  onSignOut: () => void;
}) {
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const {
    auth,
    pathname,
    onGoHome,
    onGoCourses,
    onGoDiscussions,
    onGoActivity,
    onGoSignIn,
    onSignOut,
  } = props;

  function navClass(targetPath: string) {
    const normalized = normalizePathname(pathname);
    const isActive =
      targetPath === "/"
        ? normalized === "/"
        : normalized === targetPath || normalized.startsWith(`${targetPath}/`);
    return isActive ? "topbar-link active" : "topbar-link";
  }

  const normalizedPathname = normalizePathname(pathname);
  const brandClass = normalizedPathname === "/" ? "topbar-brand active" : "topbar-brand";
  const menuClass = isMenuOpen ? "topbar-menu open" : "topbar-menu";

  useEffect(() => {
    setIsMenuOpen(false);
  }, [pathname]);

  function handleNavigation(callback: () => void) {
    setIsMenuOpen(false);
    callback();
  }

  return (
    <header className="topbar">
      <button
        type="button"
        className={brandClass}
        aria-label="Go to Robot U home"
        onClick={() => {
          handleNavigation(onGoHome);
        }}
      >
        Robot U
      </button>

      <button
        type="button"
        className="topbar-menu-button"
        aria-controls="topbar-menu"
        aria-expanded={isMenuOpen}
        aria-label="Toggle navigation menu"
        onClick={() => {
          setIsMenuOpen((currentValue) => !currentValue);
        }}
      >
        <span aria-hidden="true" />
        <span aria-hidden="true" />
        <span aria-hidden="true" />
      </button>

      <div id="topbar-menu" className={menuClass}>
        <nav className="topbar-nav" aria-label="Primary navigation">
          <button
            type="button"
            className={navClass("/courses")}
            onClick={() => {
              handleNavigation(onGoCourses);
            }}
          >
            Courses
          </button>
          <button
            type="button"
            className={navClass("/discussions")}
            onClick={() => {
              handleNavigation(onGoDiscussions);
            }}
          >
            Discussions
          </button>
          <button
            type="button"
            className={navClass("/activity")}
            onClick={() => {
              handleNavigation(onGoActivity);
            }}
          >
            Activity
          </button>
        </nav>

        <div className="topbar-auth">
          {auth.authenticated ? (
            <p>{auth.login}</p>
          ) : (
            <p className="topbar-auth-muted">Not signed in</p>
          )}
          {auth.authenticated ? (
            <button
              type="button"
              className="secondary-button"
              onClick={() => {
                handleNavigation(onSignOut);
              }}
            >
              Sign out
            </button>
          ) : (
            <button
              type="button"
              className="secondary-button"
              onClick={() => {
                handleNavigation(onGoSignIn);
              }}
            >
              Sign in
            </button>
          )}
        </div>
      </div>
    </header>
  );
}

function CourseItem(props: { course: CourseCard; onOpenCourse: (course: CourseCard) => void }) {
  const { course, onOpenCourse } = props;

  return (
    <button
      type="button"
      className="card course-card-button"
      onClick={() => {
        onOpenCourse(course);
      }}
    >
      <h3>{course.title}</h3>
      <p className="muted-copy">{course.summary}</p>
      <p className="meta-line">
        {course.repo} · {course.lessons} lessons · {course.chapters} chapters
      </p>
    </button>
  );
}

function PostItem(props: { post: PostCard }) {
  const { post } = props;

  return (
    <article className="card">
      <h3>{post.title}</h3>
      <p className="muted-copy">{post.summary}</p>
      <p className="meta-line">{post.repo}</p>
    </article>
  );
}

function EventItem(props: { event: EventCard }) {
  const { event } = props;

  return (
    <article className="card">
      <h3>{event.title}</h3>
      <p className="muted-copy">{event.when}</p>
      <p className="meta-line">{event.source}</p>
    </article>
  );
}

function DiscussionPreviewItem(props: {
  discussion: DiscussionCard;
  onOpenDiscussion: (id: number) => void;
}) {
  const { discussion, onOpenDiscussion } = props;

  return (
    <button
      type="button"
      className="discussion-preview-card"
      onClick={() => {
        onOpenDiscussion(discussion.id);
      }}
    >
      <h3>{discussion.title}</h3>
      <p className="meta-line">
        {discussion.repo} · {discussion.author} · {formatTimestamp(discussion.updated_at)} ·{" "}
        {discussion.replies} replies
      </p>
    </button>
  );
}

function DiscussionReplyCard(props: { reply: DiscussionReply }) {
  const { reply } = props;

  return (
    <article className="reply-card">
      <p className="reply-author">{reply.author}</p>
      <p className="meta-line">{formatTimestamp(reply.created_at)}</p>
      <MarkdownContent markdown={reply.body} className="thread-copy" />
    </article>
  );
}

function repoParts(fullName: string): { owner: string; repo: string } | null {
  const [owner, repo] = fullName.split("/", 2);
  if (!owner || !repo) {
    return null;
  }

  return { owner, repo };
}

async function responseError(response: Response, fallback: string): Promise<Error> {
  const payload = (await response.json().catch(() => null)) as { detail?: string } | null;
  return new Error(payload?.detail || fallback);
}

async function postDiscussionReply(
  discussion: DiscussionCard,
  body: string,
): Promise<DiscussionReply> {
  const repo = repoParts(discussion.repo);
  if (!repo) {
    throw new Error("This discussion is missing repository information.");
  }

  const response = await fetch("/api/discussions/replies", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      owner: repo.owner,
      repo: repo.repo,
      number: discussion.number,
      body,
    }),
  });

  if (!response.ok) {
    throw await responseError(response, `Reply failed with ${response.status}`);
  }

  return (await response.json()) as DiscussionReply;
}

function ComposeBox(props: {
  discussion: DiscussionCard;
  onReplyCreated: (discussionId: number, reply: DiscussionReply) => void;
  auth: AuthState;
  onGoSignIn: () => void;
}) {
  const { discussion, onReplyCreated, auth, onGoSignIn } = props;
  const [body, setBody] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const trimmedBody = body.trim();
  const canReply = canUseInteractiveAuth(auth);

  async function submitReply(event: SubmitEvent) {
    event.preventDefault();
    if (!canReply) {
      setError("Sign in before replying.");
      return;
    }

    if (!trimmedBody || isSubmitting) {
      return;
    }

    setError(null);
    setIsSubmitting(true);
    try {
      const reply = await postDiscussionReply(discussion, trimmedBody);
      onReplyCreated(discussion.id, reply);
      setBody("");
    } catch (replyError) {
      const message =
        replyError instanceof Error ? replyError.message : "Reply could not be posted.";
      setError(message);
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form className="compose-box" onSubmit={submitReply}>
      {!canReply ? (
        <div className="signin-callout">
          <p>
            {auth.authenticated
              ? "Reply posting is unavailable for this session."
              : "Sign in before replying."}
          </p>
          {!auth.authenticated ? (
            <button type="button" className="secondary-button" onClick={onGoSignIn}>
              Sign in
            </button>
          ) : null}
        </div>
      ) : null}
      <textarea
        className="compose-input"
        placeholder="Write a reply"
        value={body}
        disabled={!canReply}
        onInput={(event) => {
          setBody(event.currentTarget.value);
        }}
      />
      <div className="compose-actions">
        <button
          type="submit"
          className="compose-button"
          disabled={!canReply || !trimmedBody || isSubmitting}
        >
          {isSubmitting ? "Posting..." : "Post reply"}
        </button>
      </div>
      {error ? <p className="compose-error">{error}</p> : null}
    </form>
  );
}

function CoursePage(props: {
  course: CourseCard;
  onGoHome: () => void;
  onOpenLesson: (course: CourseCard, chapter: CourseChapter, lesson: CourseLesson) => void;
}) {
  const { course, onGoHome, onOpenLesson } = props;

  return (
    <section className="thread-view">
      <button type="button" className="back-link" onClick={onGoHome}>
        Back to courses
      </button>

      <article className="panel">
        <header className="thread-header">
          <h1>{course.title}</h1>
          <p className="meta-line">
            {course.repo} · {course.lessons} lessons · {course.chapters} chapters
          </p>
        </header>
        <p className="muted-copy">{course.summary}</p>
      </article>

      <article className="panel">
        <header className="subsection-header">
          <h2>Course outline</h2>
        </header>
        {course.outline.length > 0 ? (
          <div className="outline-list">
            {course.outline.map((chapter) => (
              <section key={chapter.slug} className="outline-chapter">
                <h3>{chapter.title}</h3>
                <div className="lesson-list">
                  {chapter.lessons.map((lesson, index) => (
                    <button
                      key={lesson.path}
                      type="button"
                      className="lesson-row lesson-row-button"
                      onClick={() => {
                        onOpenLesson(course, chapter, lesson);
                      }}
                    >
                      <p className="lesson-index">{index + 1 < 10 ? `0${index + 1}` : index + 1}</p>
                      <div>
                        <p className="lesson-title">{lesson.title}</p>
                        {lesson.summary ? <p className="lesson-summary">{lesson.summary}</p> : null}
                        <p className="meta-line">{lesson.file_path || lesson.path}</p>
                      </div>
                    </button>
                  ))}
                </div>
              </section>
            ))}
          </div>
        ) : (
          <EmptyState copy="This course repo has no lesson folders yet." />
        )}
      </article>
    </section>
  );
}

function LessonPage(props: {
  course: CourseCard;
  chapter: CourseChapter;
  lesson: CourseLesson;
  onGoCourse: () => void;
}) {
  const { course, chapter, lesson, onGoCourse } = props;
  const lessonBody = stripLeadingTitleHeading(lesson.body, lesson.title);

  return (
    <section className="thread-view">
      <button type="button" className="back-link" onClick={onGoCourse}>
        Back to course
      </button>

      <article className="panel">
        <header className="thread-header">
          <h1>{lesson.title}</h1>
          <p className="meta-line">
            {course.repo} · {chapter.title}
          </p>
        </header>
        {lesson.summary ? <p className="muted-copy">{lesson.summary}</p> : null}
      </article>

      <article className="panel">
        <header className="subsection-header">
          <h2>Lesson</h2>
        </header>
        {lessonBody ? (
          <MarkdownContent markdown={lessonBody} className="lesson-body" />
        ) : (
          <EmptyState copy="This lesson file is empty or could not be read from Forgejo." />
        )}
      </article>
    </section>
  );
}

function DiscussionPage(props: {
  discussion: DiscussionCard;
  auth: AuthState;
  onGoHome: () => void;
  onGoSignIn: () => void;
  onReplyCreated: (discussionId: number, reply: DiscussionReply) => void;
}) {
  const { discussion, auth, onGoHome, onGoSignIn, onReplyCreated } = props;

  return (
    <section className="thread-view">
      <button type="button" className="back-link" onClick={onGoHome}>
        Back to discussions
      </button>

      <article className="panel">
        <header className="thread-header">
          <h1>{discussion.title}</h1>
          <p className="meta-line">
            {discussion.repo} · Issue #{discussion.number} · {discussion.author} ·{" "}
            {formatTimestamp(discussion.updated_at)}
          </p>
        </header>
        <MarkdownContent markdown={discussion.body} className="thread-copy" />
      </article>

      <article className="panel">
        <header className="subsection-header">
          <h2>Replies</h2>
          <p className="meta-line">{discussion.comments.length}</p>
        </header>
        {discussion.comments.length > 0 ? (
          <div className="reply-list">
            {discussion.comments.map((reply) => (
              <DiscussionReplyCard key={reply.id} reply={reply} />
            ))}
          </div>
        ) : (
          <EmptyState copy="No replies yet." />
        )}
      </article>

      <article className="panel">
        <header className="subsection-header">
          <h2>Reply</h2>
        </header>
        <ComposeBox
          discussion={discussion}
          auth={auth}
          onGoSignIn={onGoSignIn}
          onReplyCreated={onReplyCreated}
        />
      </article>
    </section>
  );
}

function CoursesView(props: { data: PrototypeData; onOpenCourse: (course: CourseCard) => void }) {
  const { data, onOpenCourse } = props;

  return (
    <section className="page-section">
      <SectionHeader title="Courses" />
      {data.featured_courses.length > 0 ? (
        <div className="card-grid">
          {data.featured_courses.map((course) => (
            <CourseItem key={course.repo} course={course} onOpenCourse={onOpenCourse} />
          ))}
        </div>
      ) : (
        <EmptyState copy="No public repos with `/lessons/` were found." />
      )}
    </section>
  );
}

function DiscussionsView(props: { data: PrototypeData; onOpenDiscussion: (id: number) => void }) {
  const { data, onOpenDiscussion } = props;

  return (
    <section className="page-section">
      <SectionHeader title="Discussions" />
      {data.recent_discussions.length > 0 ? (
        <div className="stack">
          {data.recent_discussions.map((discussion) => (
            <DiscussionPreviewItem
              key={discussion.id}
              discussion={discussion}
              onOpenDiscussion={onOpenDiscussion}
            />
          ))}
        </div>
      ) : (
        <EmptyState copy="No visible Forgejo issues were returned for this account." />
      )}
    </section>
  );
}

function HomeView(props: {
  data: PrototypeData;
  onOpenCourse: (course: CourseCard) => void;
  onOpenDiscussion: (id: number) => void;
}) {
  const { data, onOpenCourse, onOpenDiscussion } = props;

  return (
    <>
      <section className="page-header">
        <p className="page-kicker">Robot U</p>
        <h1>Courses, projects, and discussions.</h1>
        <p className="muted-copy">
          A single place for lessons, member work, and community conversation.
        </p>
      </section>

      <CoursesView data={data} onOpenCourse={onOpenCourse} />

      <section className="two-column-grid">
        <div className="page-section">
          <SectionHeader title="Posts" />
          {data.recent_posts.length > 0 ? (
            <div className="stack">
              {data.recent_posts.map((post) => (
                <PostItem key={`${post.repo}:${post.title}`} post={post} />
              ))}
            </div>
          ) : (
            <EmptyState copy="No public repos with `/blogs/` were found." />
          )}
        </div>

        <div className="page-section">
          <SectionHeader title="Events" />
          {data.upcoming_events.length > 0 ? (
            <div className="stack">
              {data.upcoming_events.map((event) => (
                <EventItem key={`${event.source}:${event.title}`} event={event} />
              ))}
            </div>
          ) : (
            <EmptyState copy="ICS feeds are not configured yet." />
          )}
        </div>
      </section>

      <DiscussionsView data={data} onOpenDiscussion={onOpenDiscussion} />
    </>
  );
}

function SignInPage(props: { auth: AuthState; onGoHome: () => void }) {
  const { auth, onGoHome } = props;
  const query = new URLSearchParams(window.location.search);
  const error = query.get("error");
  const returnTo = query.get("return_to") || "/";

  function startForgejoSignIn() {
    window.location.assign(forgejoSignInUrl(returnTo));
  }

  return (
    <section className="signin-page">
      <button type="button" className="back-link" onClick={onGoHome}>
        Back
      </button>

      <article className="panel signin-panel">
        <header className="thread-header">
          <h1>Sign in</h1>
          <p className="muted-copy">
            Forgejo sign-in needs attention. You can retry the OAuth flow or return to the site.
          </p>
        </header>

        <div className="signin-actions">
          <button
            type="button"
            className="compose-button"
            disabled={!auth.oauth_configured}
            onClick={startForgejoSignIn}
          >
            Continue with Forgejo
          </button>
          {!auth.oauth_configured ? (
            <p className="compose-error">Forgejo OAuth is not configured on this backend yet.</p>
          ) : null}
          {error ? <p className="compose-error">{error}</p> : null}
        </div>
      </article>
    </section>
  );
}

async function fetchPrototypeData(signal?: AbortSignal): Promise<PrototypeData> {
  const response = await fetch("/api/prototype", {
    signal,
  });

  if (!response.ok) {
    throw new Error(`Prototype request failed with ${response.status}`);
  }

  return (await response.json()) as PrototypeData;
}

function appendDiscussionReply(
  currentData: PrototypeData | null,
  discussionId: number,
  reply: DiscussionReply,
): PrototypeData | null {
  if (!currentData) {
    return currentData;
  }

  return {
    ...currentData,
    recent_discussions: currentData.recent_discussions.map((discussion) => {
      if (discussion.id !== discussionId) {
        return discussion;
      }

      return {
        ...discussion,
        replies: discussion.replies + 1,
        updated_at: reply.created_at || discussion.updated_at,
        comments: [...discussion.comments, reply],
      };
    }),
  };
}

interface ActivityEntry {
  id: string;
  title: string;
  detail: string;
  timestamp: string;
  route: string | null;
}

function timestampMillis(value: string): number {
  const timestamp = new Date(value).getTime();
  return Number.isNaN(timestamp) ? 0 : timestamp;
}

function coursePath(course: CourseCard): string {
  return `/courses/${encodeURIComponent(course.owner)}/${encodeURIComponent(course.name)}`;
}

function buildActivityFeed(data: PrototypeData): ActivityEntry[] {
  const activities: ActivityEntry[] = [];

  for (const course of data.featured_courses) {
    if (course.updated_at) {
      activities.push({
        id: `course:${course.repo}`,
        title: `Course updated: ${course.title}`,
        detail: `${course.repo} · ${course.lessons} lessons`,
        timestamp: course.updated_at,
        route: coursePath(course),
      });
    }
  }

  for (const post of data.recent_posts) {
    if (post.updated_at) {
      activities.push({
        id: `post:${post.repo}`,
        title: `Post repo updated: ${post.title}`,
        detail: post.repo,
        timestamp: post.updated_at,
        route: null,
      });
    }
  }

  for (const discussion of data.recent_discussions) {
    activities.push({
      id: `discussion:${discussion.id}`,
      title: `Discussion updated: ${discussion.title}`,
      detail: `${discussion.repo} · ${discussion.replies} replies`,
      timestamp: discussion.updated_at,
      route: `/discussions/${discussion.id}`,
    });

    for (const reply of discussion.comments) {
      activities.push({
        id: `reply:${discussion.id}:${reply.id}`,
        title: `${reply.author} replied`,
        detail: discussion.title,
        timestamp: reply.created_at,
        route: `/discussions/${discussion.id}`,
      });
    }
  }

  return activities.sort(
    (left, right) => timestampMillis(right.timestamp) - timestampMillis(left.timestamp),
  );
}

function ActivityItem(props: { entry: ActivityEntry; onOpenRoute: (route: string) => void }) {
  const { entry, onOpenRoute } = props;

  if (entry.route) {
    return (
      <button
        type="button"
        className="activity-card activity-card-button"
        onClick={() => {
          if (entry.route) {
            onOpenRoute(entry.route);
          }
        }}
      >
        <h3>{entry.title}</h3>
        <p className="muted-copy">{entry.detail}</p>
        <p className="meta-line">{formatTimestamp(entry.timestamp)}</p>
      </button>
    );
  }

  return (
    <article className="activity-card">
      <h3>{entry.title}</h3>
      <p className="muted-copy">{entry.detail}</p>
      <p className="meta-line">{formatTimestamp(entry.timestamp)}</p>
    </article>
  );
}

function ActivityView(props: { data: PrototypeData; onOpenRoute: (route: string) => void }) {
  const activities = buildActivityFeed(props.data);

  return (
    <section className="page-section">
      <SectionHeader title="Activity" />
      {activities.length > 0 ? (
        <div className="activity-list">
          {activities.map((entry) => (
            <ActivityItem key={entry.id} entry={entry} onOpenRoute={props.onOpenRoute} />
          ))}
        </div>
      ) : (
        <EmptyState copy="No public site activity has been loaded yet." />
      )}
    </section>
  );
}

interface AppContentProps {
  data: PrototypeData;
  pathname: string;
  onOpenCourse: (course: CourseCard) => void;
  onOpenLesson: (course: CourseCard, chapter: CourseChapter, lesson: CourseLesson) => void;
  onOpenDiscussion: (id: number) => void;
  onOpenRoute: (route: string) => void;
  onGoSignIn: () => void;
  onReplyCreated: (discussionId: number, reply: DiscussionReply) => void;
  onGoHome: () => void;
  onGoCourses: () => void;
  onGoDiscussions: () => void;
}

function LessonRouteView(
  props: AppContentProps & {
    route: { owner: string; repo: string; chapter: string; lesson: string };
  },
) {
  const selectedCourse = findCourseByRoute(props.data.featured_courses, props.route);
  if (!selectedCourse) {
    return (
      <section className="page-message">
        <h1>Course not found.</h1>
        <button type="button" className="back-link" onClick={props.onGoCourses}>
          Back to courses
        </button>
      </section>
    );
  }

  const selectedLesson = findLessonByRoute(selectedCourse, props.route);
  if (!selectedLesson) {
    return (
      <section className="page-message">
        <h1>Lesson not found.</h1>
        <button
          type="button"
          className="back-link"
          onClick={() => {
            props.onOpenCourse(selectedCourse);
          }}
        >
          Back to course
        </button>
      </section>
    );
  }

  return (
    <LessonPage
      course={selectedCourse}
      chapter={selectedLesson.chapter}
      lesson={selectedLesson.lesson}
      onGoCourse={() => {
        props.onOpenCourse(selectedCourse);
      }}
    />
  );
}

function CourseRouteView(props: AppContentProps & { route: { owner: string; repo: string } }) {
  const selectedCourse = findCourseByRoute(props.data.featured_courses, props.route);
  if (!selectedCourse) {
    return (
      <section className="page-message">
        <h1>Course not found.</h1>
        <button type="button" className="back-link" onClick={props.onGoCourses}>
          Back to courses
        </button>
      </section>
    );
  }

  return (
    <CoursePage
      course={selectedCourse}
      onGoHome={props.onGoCourses}
      onOpenLesson={props.onOpenLesson}
    />
  );
}

function DiscussionRouteView(props: AppContentProps & { discussionId: number }) {
  const selectedDiscussion = props.data.recent_discussions.find(
    (discussion) => discussion.id === props.discussionId,
  );
  if (!selectedDiscussion) {
    return (
      <section className="page-message">
        <h1>Discussion not found.</h1>
        <button type="button" className="back-link" onClick={props.onGoDiscussions}>
          Back to discussions
        </button>
      </section>
    );
  }

  return (
    <DiscussionPage
      discussion={selectedDiscussion}
      auth={props.data.auth}
      onGoHome={props.onGoDiscussions}
      onGoSignIn={props.onGoSignIn}
      onReplyCreated={props.onReplyCreated}
    />
  );
}

function AppContent(props: AppContentProps) {
  if (isSignInRoute(props.pathname)) {
    return <SignInPage auth={props.data.auth} onGoHome={props.onGoHome} />;
  }

  if (isActivityRoute(props.pathname)) {
    return <ActivityView data={props.data} onOpenRoute={props.onOpenRoute} />;
  }

  if (isCoursesIndexRoute(props.pathname)) {
    return <CoursesView data={props.data} onOpenCourse={props.onOpenCourse} />;
  }

  if (isDiscussionsIndexRoute(props.pathname)) {
    return <DiscussionsView data={props.data} onOpenDiscussion={props.onOpenDiscussion} />;
  }

  const lessonRoute = parseLessonRoute(props.pathname);
  if (lessonRoute !== null) {
    return <LessonRouteView {...props} route={lessonRoute} />;
  }

  const courseRoute = parseCourseRoute(props.pathname);
  if (courseRoute !== null) {
    return <CourseRouteView {...props} route={courseRoute} />;
  }

  const discussionId = parseDiscussionRoute(props.pathname);
  if (discussionId === null) {
    return (
      <HomeView
        data={props.data}
        onOpenCourse={props.onOpenCourse}
        onOpenDiscussion={props.onOpenDiscussion}
      />
    );
  }

  return <DiscussionRouteView {...props} discussionId={discussionId} />;
}

function LoadedApp(
  props: AppContentProps & {
    onGoActivity: () => void;
    onSignOut: () => void;
  },
) {
  return (
    <>
      <TopBar
        auth={props.data.auth}
        pathname={props.pathname}
        onGoHome={props.onGoHome}
        onGoCourses={props.onGoCourses}
        onGoDiscussions={props.onGoDiscussions}
        onGoActivity={props.onGoActivity}
        onGoSignIn={props.onGoSignIn}
        onSignOut={props.onSignOut}
      />
      <main className="app-shell">
        <AppContent
          data={props.data}
          pathname={props.pathname}
          onOpenCourse={props.onOpenCourse}
          onOpenLesson={props.onOpenLesson}
          onOpenDiscussion={props.onOpenDiscussion}
          onOpenRoute={props.onOpenRoute}
          onGoSignIn={props.onGoSignIn}
          onReplyCreated={props.onReplyCreated}
          onGoHome={props.onGoHome}
          onGoCourses={props.onGoCourses}
          onGoDiscussions={props.onGoDiscussions}
        />
      </main>
    </>
  );
}

function AppStatusPage(props: { title: string; copy?: string }) {
  return (
    <main className="app-shell">
      <section className="page-message">
        <h1>{props.title}</h1>
        {props.copy ? <p className="muted-copy">{props.copy}</p> : null}
      </section>
    </main>
  );
}

export default function App() {
  const [data, setData] = useState<PrototypeData | null>(null);
  const [error, setError] = useState<string | null>(null);
  const { pathname, navigate } = usePathname();

  async function loadPrototype(signal?: AbortSignal) {
    try {
      const payload = await fetchPrototypeData(signal);
      setData(payload);
      setError(null);
    } catch (loadError) {
      if (signal?.aborted) {
        return;
      }

      const message =
        loadError instanceof Error ? loadError.message : "Unknown prototype loading error";
      setError(message);
    }
  }

  useEffect(() => {
    const controller = new AbortController();
    loadPrototype(controller.signal);
    return () => {
      controller.abort();
    };
  }, []);

  function openDiscussion(id: number) {
    navigate(`/discussions/${id}`);
  }

  function goSignIn() {
    window.location.assign(forgejoSignInUrl(pathname));
  }

  function goCourses() {
    navigate("/courses");
  }

  function goDiscussions() {
    navigate("/discussions");
  }

  function goActivity() {
    navigate("/activity");
  }

  function openCourse(course: CourseCard) {
    navigate(`/courses/${encodeURIComponent(course.owner)}/${encodeURIComponent(course.name)}`);
  }

  function openLesson(course: CourseCard, chapter: CourseChapter, lesson: CourseLesson) {
    navigate(
      `/courses/${encodeURIComponent(course.owner)}/${encodeURIComponent(course.name)}/lessons/${encodeURIComponent(chapter.slug)}/${encodeURIComponent(lesson.slug)}`,
    );
  }

  function addReplyToDiscussion(discussionId: number, reply: DiscussionReply) {
    setData((currentData) => appendDiscussionReply(currentData, discussionId, reply));
  }

  function goHome() {
    navigate("/");
  }

  async function signOut() {
    await fetch("/api/auth/session", {
      method: "DELETE",
    });
    await loadPrototype();
    navigate("/");
  }

  if (error) {
    return <AppStatusPage title="Backend data did not load." copy={error} />;
  }

  if (!data) {
    return <AppStatusPage title="Loading content." />;
  }

  return (
    <LoadedApp
      data={data}
      pathname={pathname}
      onOpenCourse={openCourse}
      onOpenLesson={openLesson}
      onOpenDiscussion={openDiscussion}
      onOpenRoute={navigate}
      onGoSignIn={goSignIn}
      onReplyCreated={addReplyToDiscussion}
      onGoHome={goHome}
      onGoCourses={goCourses}
      onGoDiscussions={goDiscussions}
      onGoActivity={goActivity}
      onSignOut={signOut}
    />
  );
}
