import { GraphQLError } from "graphql";
import { builder } from "../builder";
import {
  VideoType,
  VideoListType,
  AdminVideoListType,
  VideoLikeResponseType,
  VideoPlayResponseType,
  VideoLikeStatusType,
} from "../types/index";
import {
  videos,
  video_models,
  video_likes,
  video_plays,
  users,
  files,
} from "../../db/schema/index";
import {
  eq,
  and,
  lte,
  desc,
  asc,
  inArray,
  count,
  ilike,
  lt,
  gte,
  arrayContains,
  isNull,
  or,
  type SQL,
} from "drizzle-orm";
import { requireAdmin } from "../../lib/acl";
import type { DB } from "../../db/connection";

async function enrichVideo(db: DB, video: typeof videos.$inferSelect) {
  // Fetch models
  const modelRows = await db
    .select({
      id: users.id,
      artist_name: users.artist_name,
      slug: users.slug,
      avatar: users.avatar,
      description: users.description,
    })
    .from(video_models)
    .leftJoin(users, eq(video_models.user_id, users.id))
    .where(eq(video_models.video_id, video.id));

  // Fetch movie file
  let movieFile = null;
  if (video.movie) {
    const mf = await db.select().from(files).where(eq(files.id, video.movie)).limit(1);
    movieFile = mf[0] || null;
  }

  // Count likes
  const likesCount = await db
    .select({ count: count() })
    .from(video_likes)
    .where(eq(video_likes.video_id, video.id));
  const playsCount = await db
    .select({ count: count() })
    .from(video_plays)
    .where(eq(video_plays.video_id, video.id));

  const models = modelRows
    .filter((m) => m.id !== null)
    .map((m) => ({
      id: m.id!,
      artist_name: m.artist_name,
      slug: m.slug,
      avatar: m.avatar,
      description: m.description,
    }));

  return {
    ...video,
    models,
    movie_file: movieFile,
    likes_count: likesCount[0]?.count || 0,
    plays_count: playsCount[0]?.count || 0,
  };
}

builder.queryField("videos", (t) =>
  t.field({
    type: VideoListType,
    args: {
      modelId: t.arg.string(),
      featured: t.arg.boolean(),
      limit: t.arg.int(),
      search: t.arg.string(),
      offset: t.arg.int(),
      sortBy: t.arg.string(),
      duration: t.arg.string(),
      tag: t.arg.string(),
    },
    resolve: async (_root, args, ctx) => {
      const pageSize = args.limit ?? 24;
      const offset = args.offset ?? 0;

      const conditions: SQL<unknown>[] = [lte(videos.upload_date, new Date())];
      if (!ctx.currentUser) conditions.push(or(eq(videos.premium, false), isNull(videos.premium))!);
      if (args.featured !== null && args.featured !== undefined) {
        conditions.push(eq(videos.featured, args.featured));
      }
      if (args.search) {
        conditions.push(ilike(videos.title, `%${args.search}%`));
      }
      if (args.tag) {
        conditions.push(arrayContains(videos.tags, [args.tag]));
      }
      if (args.modelId) {
        const videoIds = await ctx.db
          .select({ video_id: video_models.video_id })
          .from(video_models)
          .where(eq(video_models.user_id, args.modelId));
        if (videoIds.length === 0) return { items: [], total: 0 };
        conditions.push(
          inArray(
            videos.id,
            videoIds.map((v) => v.video_id),
          ),
        );
      }

      const order =
        args.sortBy === "most_liked"
          ? desc(videos.likes_count)
          : args.sortBy === "most_played"
            ? desc(videos.plays_count)
            : args.sortBy === "name"
              ? asc(videos.title)
              : desc(videos.upload_date);

      const where = and(...conditions);

      // Duration filter requires JOIN to files table
      if (args.duration && args.duration !== "all") {
        const durationCond =
          args.duration === "short"
            ? lt(files.duration, 600)
            : args.duration === "medium"
              ? and(gte(files.duration, 600), lt(files.duration, 1200))
              : gte(files.duration, 1200);

        const fullWhere = and(where, durationCond);
        const [rows, totalRows] = await Promise.all([
          ctx.db
            .select({ v: videos })
            .from(videos)
            .leftJoin(files, eq(videos.movie, files.id))
            .where(fullWhere)
            .orderBy(order)
            .limit(pageSize)
            .offset(offset),
          ctx.db
            .select({ total: count() })
            .from(videos)
            .leftJoin(files, eq(videos.movie, files.id))
            .where(fullWhere),
        ]);
        const videoList = rows.map((r) => r.v);
        const items = await Promise.all(videoList.map((v) => enrichVideo(ctx.db, v)));
        return { items, total: totalRows[0]?.total ?? 0 };
      }

      const [rows, totalRows] = await Promise.all([
        ctx.db.select().from(videos).where(where).orderBy(order).limit(pageSize).offset(offset),
        ctx.db.select({ total: count() }).from(videos).where(where),
      ]);
      const items = await Promise.all(rows.map((v) => enrichVideo(ctx.db, v)));
      return { items, total: totalRows[0]?.total ?? 0 };
    },
  }),
);

builder.queryField("video", (t) =>
  t.field({
    type: VideoType,
    nullable: true,
    args: {
      slug: t.arg.string({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      const video = await ctx.db
        .select()
        .from(videos)
        .where(and(eq(videos.slug, args.slug), lte(videos.upload_date, new Date())))
        .limit(1);

      if (!video[0]) return null;

      if (video[0].premium && !ctx.currentUser) {
        throw new GraphQLError("Unauthorized");
      }

      return enrichVideo(ctx.db, video[0]);
    },
  }),
);

builder.queryField("adminGetVideo", (t) =>
  t.field({
    type: VideoType,
    nullable: true,
    args: {
      id: t.arg.string({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      requireAdmin(ctx);
      const video = await ctx.db.select().from(videos).where(eq(videos.id, args.id)).limit(1);
      if (!video[0]) return null;
      return enrichVideo(ctx.db, video[0]);
    },
  }),
);

builder.queryField("videoLikeStatus", (t) =>
  t.field({
    type: VideoLikeStatusType,
    args: {
      videoId: t.arg.string({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      if (!ctx.currentUser) return { liked: false };
      const existing = await ctx.db
        .select()
        .from(video_likes)
        .where(
          and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)),
        )
        .limit(1);
      return { liked: existing.length > 0 };
    },
  }),
);

builder.mutationField("likeVideo", (t) =>
  t.field({
    type: VideoLikeResponseType,
    args: {
      videoId: t.arg.string({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      if (!ctx.currentUser) throw new GraphQLError("Unauthorized");

      const existing = await ctx.db
        .select()
        .from(video_likes)
        .where(
          and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)),
        )
        .limit(1);

      if (existing.length > 0) throw new GraphQLError("Already liked");

      await ctx.db.insert(video_likes).values({
        video_id: args.videoId,
        user_id: ctx.currentUser.id,
      });

      await ctx.db
        .update(videos)
        .set({
          likes_count:
            ((
              await ctx.db
                .select({ c: videos.likes_count })
                .from(videos)
                .where(eq(videos.id, args.videoId))
                .limit(1)
            )[0]?.c as number) + 1 || 1,
        })
        .where(eq(videos.id, args.videoId));

      const likesCount = await ctx.db
        .select({ count: count() })
        .from(video_likes)
        .where(eq(video_likes.video_id, args.videoId));
      return { liked: true, likes_count: likesCount[0]?.count || 1 };
    },
  }),
);

builder.mutationField("unlikeVideo", (t) =>
  t.field({
    type: VideoLikeResponseType,
    args: {
      videoId: t.arg.string({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      if (!ctx.currentUser) throw new GraphQLError("Unauthorized");

      const existing = await ctx.db
        .select()
        .from(video_likes)
        .where(
          and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)),
        )
        .limit(1);

      if (existing.length === 0) throw new GraphQLError("Not liked");

      await ctx.db
        .delete(video_likes)
        .where(
          and(eq(video_likes.video_id, args.videoId), eq(video_likes.user_id, ctx.currentUser.id)),
        );

      await ctx.db
        .update(videos)
        .set({
          likes_count: Math.max(
            (((
              await ctx.db
                .select({ c: videos.likes_count })
                .from(videos)
                .where(eq(videos.id, args.videoId))
                .limit(1)
            )[0]?.c as number) || 1) - 1,
            0,
          ),
        })
        .where(eq(videos.id, args.videoId));

      const likesCount = await ctx.db
        .select({ count: count() })
        .from(video_likes)
        .where(eq(video_likes.video_id, args.videoId));
      return { liked: false, likes_count: likesCount[0]?.count || 0 };
    },
  }),
);

builder.mutationField("recordVideoPlay", (t) =>
  t.field({
    type: VideoPlayResponseType,
    args: {
      videoId: t.arg.string({ required: true }),
      sessionId: t.arg.string(),
    },
    resolve: async (_root, args, ctx) => {
      const play = await ctx.db
        .insert(video_plays)
        .values({
          video_id: args.videoId,
          user_id: ctx.currentUser?.id || null,
          session_id: args.sessionId || null,
        })
        .returning({ id: video_plays.id });

      const playsCount = await ctx.db
        .select({ count: count() })
        .from(video_plays)
        .where(eq(video_plays.video_id, args.videoId));

      await ctx.db
        .update(videos)
        .set({ plays_count: playsCount[0]?.count || 0 })
        .where(eq(videos.id, args.videoId));

      return {
        success: true,
        play_id: play[0].id,
        plays_count: playsCount[0]?.count || 0,
      };
    },
  }),
);

builder.mutationField("updateVideoPlay", (t) =>
  t.field({
    type: "Boolean",
    args: {
      videoId: t.arg.string({ required: true }),
      playId: t.arg.string({ required: true }),
      durationWatched: t.arg.int({ required: true }),
      completed: t.arg.boolean({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      const play = await ctx.db
        .select()
        .from(video_plays)
        .where(eq(video_plays.id, args.playId))
        .limit(1);

      if (!play[0]) return false;

      // If play belongs to a user, verify ownership
      if (play[0].user_id && (!ctx.currentUser || play[0].user_id !== ctx.currentUser.id)) {
        throw new GraphQLError("Forbidden");
      }

      await ctx.db
        .update(video_plays)
        .set({
          duration_watched: args.durationWatched,
          completed: args.completed,
          date_updated: new Date(),
        })
        .where(eq(video_plays.id, args.playId));
      return true;
    },
  }),
);

builder.queryField("analytics", (t) =>
  t.field({
    type: "JSON",
    nullable: true,
    resolve: async (_root, _args, ctx) => {
      if (!ctx.currentUser || ctx.currentUser.role !== "model") {
        throw new GraphQLError("Unauthorized");
      }

      const userId = ctx.currentUser.id;

      // Get all videos by this user (via video_models)
      const modelVideoIds = await ctx.db
        .select({ video_id: video_models.video_id })
        .from(video_models)
        .where(eq(video_models.user_id, userId));

      if (modelVideoIds.length === 0) {
        return {
          total_videos: 0,
          total_likes: 0,
          total_plays: 0,
          plays_by_date: {},
          likes_by_date: {},
          videos: [],
        };
      }

      const videoIds = modelVideoIds.map((v) => v.video_id);
      const videoList = await ctx.db.select().from(videos).where(inArray(videos.id, videoIds));
      const plays = await ctx.db
        .select()
        .from(video_plays)
        .where(inArray(video_plays.video_id, videoIds));
      const likes = await ctx.db
        .select()
        .from(video_likes)
        .where(inArray(video_likes.video_id, videoIds));

      const totalLikes = videoList.reduce((sum, v) => sum + (v.likes_count || 0), 0);
      const totalPlays = videoList.reduce((sum, v) => sum + (v.plays_count || 0), 0);

      const playsByDate = plays.reduce((acc: Record<string, number>, play) => {
        const date = new Date(play.date_created).toISOString().split("T")[0];
        if (!acc[date]) acc[date] = 0;
        acc[date]++;
        return acc;
      }, {});

      const likesByDate = likes.reduce((acc: Record<string, number>, like) => {
        const date = new Date(like.date_created).toISOString().split("T")[0];
        if (!acc[date]) acc[date] = 0;
        acc[date]++;
        return acc;
      }, {});

      const videoAnalytics = videoList.map((video) => {
        const vPlays = plays.filter((p) => p.video_id === video.id);
        const completedPlays = vPlays.filter((p) => p.completed).length;
        const avgWatchTime =
          vPlays.length > 0
            ? vPlays.reduce((sum, p) => sum + (p.duration_watched || 0), 0) / vPlays.length
            : 0;

        return {
          id: video.id,
          title: video.title,
          slug: video.slug,
          upload_date: video.upload_date,
          likes: video.likes_count || 0,
          plays: video.plays_count || 0,
          completed_plays: completedPlays,
          completion_rate: video.plays_count ? (completedPlays / video.plays_count) * 100 : 0,
          avg_watch_time: Math.round(avgWatchTime),
        };
      });

      return {
        total_videos: videoList.length,
        total_likes: totalLikes,
        total_plays: totalPlays,
        plays_by_date: playsByDate,
        likes_by_date: likesByDate,
        videos: videoAnalytics,
      };
    },
  }),
);

// ─── Admin queries & mutations ────────────────────────────────────────────────

builder.queryField("adminListVideos", (t) =>
  t.field({
    type: AdminVideoListType,
    args: {
      search: t.arg.string(),
      premium: t.arg.boolean(),
      featured: t.arg.boolean(),
      limit: t.arg.int(),
      offset: t.arg.int(),
    },
    resolve: async (_root, args, ctx) => {
      requireAdmin(ctx);
      const limit = args.limit ?? 50;
      const offset = args.offset ?? 0;

      const conditions: SQL<unknown>[] = [];
      if (args.search) conditions.push(ilike(videos.title, `%${args.search}%`));
      if (args.premium !== null && args.premium !== undefined)
        conditions.push(eq(videos.premium, args.premium));
      if (args.featured !== null && args.featured !== undefined)
        conditions.push(eq(videos.featured, args.featured));

      const where = conditions.length > 0 ? and(...conditions) : undefined;
      const [rows, totalRows] = await Promise.all([
        ctx.db
          .select()
          .from(videos)
          .where(where)
          .orderBy(desc(videos.upload_date))
          .limit(limit)
          .offset(offset),
        ctx.db.select({ total: count() }).from(videos).where(where),
      ]);
      const items = await Promise.all(rows.map((v) => enrichVideo(ctx.db, v)));
      return { items, total: totalRows[0]?.total ?? 0 };
    },
  }),
);

builder.mutationField("createVideo", (t) =>
  t.field({
    type: VideoType,
    args: {
      title: t.arg.string({ required: true }),
      slug: t.arg.string({ required: true }),
      description: t.arg.string(),
      imageId: t.arg.string(),
      movieId: t.arg.string(),
      tags: t.arg.stringList(),
      premium: t.arg.boolean(),
      featured: t.arg.boolean(),
      uploadDate: t.arg.string(),
    },
    resolve: async (_root, args, ctx) => {
      requireAdmin(ctx);
      const inserted = await ctx.db
        .insert(videos)
        .values({
          title: args.title,
          slug: args.slug,
          description: args.description || null,
          image: args.imageId || null,
          movie: args.movieId || null,
          tags: args.tags || [],
          premium: args.premium ?? false,
          featured: args.featured ?? false,
          upload_date: args.uploadDate ? new Date(args.uploadDate) : new Date(),
        })
        .returning();
      return enrichVideo(ctx.db, inserted[0]);
    },
  }),
);

builder.mutationField("updateVideo", (t) =>
  t.field({
    type: VideoType,
    nullable: true,
    args: {
      id: t.arg.string({ required: true }),
      title: t.arg.string(),
      slug: t.arg.string(),
      description: t.arg.string(),
      imageId: t.arg.string(),
      movieId: t.arg.string(),
      tags: t.arg.stringList(),
      premium: t.arg.boolean(),
      featured: t.arg.boolean(),
      uploadDate: t.arg.string(),
    },
    resolve: async (_root, args, ctx) => {
      requireAdmin(ctx);
      const updates: Record<string, unknown> = {};
      if (args.title !== undefined && args.title !== null) updates.title = args.title;
      if (args.slug !== undefined && args.slug !== null) updates.slug = args.slug;
      if (args.description !== undefined) updates.description = args.description;
      if (args.imageId !== undefined) updates.image = args.imageId;
      if (args.movieId !== undefined) updates.movie = args.movieId;
      if (args.tags !== undefined && args.tags !== null) updates.tags = args.tags;
      if (args.premium !== undefined && args.premium !== null) updates.premium = args.premium;
      if (args.featured !== undefined && args.featured !== null) updates.featured = args.featured;
      if (args.uploadDate !== undefined && args.uploadDate !== null)
        updates.upload_date = new Date(args.uploadDate);

      const updated = await ctx.db
        .update(videos)
        .set(updates as Partial<typeof videos.$inferInsert>)
        .where(eq(videos.id, args.id))
        .returning();
      if (!updated[0]) return null;
      return enrichVideo(ctx.db, updated[0]);
    },
  }),
);

builder.mutationField("deleteVideo", (t) =>
  t.field({
    type: "Boolean",
    args: {
      id: t.arg.string({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      requireAdmin(ctx);
      await ctx.db.delete(videos).where(eq(videos.id, args.id));
      return true;
    },
  }),
);

builder.mutationField("setVideoModels", (t) =>
  t.field({
    type: "Boolean",
    args: {
      videoId: t.arg.string({ required: true }),
      userIds: t.arg.stringList({ required: true }),
    },
    resolve: async (_root, args, ctx) => {
      requireAdmin(ctx);
      await ctx.db.delete(video_models).where(eq(video_models.video_id, args.videoId));
      if (args.userIds.length > 0) {
        await ctx.db.insert(video_models).values(
          args.userIds.map((userId) => ({
            video_id: args.videoId,
            user_id: userId,
          })),
        );
      }
      return true;
    },
  }),
);
