import { protectedProcedure, TRPCError } from './router';
import { z } from 'zod';
import { eq, and, or, like, sql, desc, asc } from 'drizzle-orm';
import type { DrizzleDB } from '../../src/db/config/migrations';
import {
  revisions,
  revisionChanges,
  scripts,
  projects,
  projectMembers,
} from '../../src/db/schema';

// H1 fix: verifies user has access to the project owning the script a revision belongs to
async function verifyScriptAccess(
  db: DrizzleDB,
  scriptId: number,
  userId: number
) {
  const scriptRows = await db
    .select({ id: scripts.id, projectId: scripts.projectId })
    .from(scripts)
    .where(eq(scripts.id, scriptId));

  const script = scriptRows[0];
  if (!script) {
    throw new TRPCError({ code: 'NOT_FOUND', message: `Script ${scriptId} not found` });
  }

  const projectRows = await db
    .select({ id: projects.id, ownerId: projects.ownerId })
    .from(projects)
    .where(eq(projects.id, script.projectId));

  const project = projectRows[0];
  if (!project) {
    throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${script.projectId} not found` });
  }

  if (project.ownerId === userId) return { script, project };

  const memberRows = await db
    .select()
    .from(projectMembers)
    .where(and(eq(projectMembers.projectId, script.projectId), eq(projectMembers.userId, userId)));

  if (memberRows.length === 0) {
    throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to script ${scriptId}` });
  }

  return { script, project };
}

// Resolves revision → script → project and verifies user access
async function verifyRevisionAccess(
  db: DrizzleDB,
  revisionId: number,
  userId: number
) {
  const revisionRows = await db
    .select()
    .from(revisions)
    .where(eq(revisions.id, revisionId));

  const revision = revisionRows[0];
  if (!revision) {
    throw new TRPCError({ code: 'NOT_FOUND', message: `Revision ${revisionId} not found` });
  }

  const { script, project } = await verifyScriptAccess(db, revision.scriptId, userId);
  return { revision, script, project };
}

function computeDiffForContent(
  db: DrizzleDB,
  oldContent: string,
  newContent: string,
  revisionId: number
) {
  const oldLines = oldContent.split('\n');
  const newLines = newContent.split('\n');
  const linesPerPage = 55;
  const maxLen = Math.max(oldLines.length, newLines.length);

  let sceneCounter = 0;
  const changesToInsert = [];

  for (let i = 0; i < maxLen; i++) {
    const oldLine = oldLines[i];
    const newLine = newLines[i];

    if (oldLine === newLine) continue;

    let changeType: 'addition' | 'deletion' | 'modification';
    if (!oldLine && newLine) {
      changeType = 'addition';
    } else if (oldLine && !newLine) {
      changeType = 'deletion';
    } else {
      changeType = 'modification';
    }

    if (newLine?.trim().toUpperCase().startsWith('INT.') ||
        newLine?.trim().toUpperCase().startsWith('EXT.')) {
      sceneCounter++;
    }

    changesToInsert.push({
      revisionId,
      changeType,
      elementType: null,
      oldContent: changeType !== 'addition' ? oldLine || null : null,
      newContent: changeType !== 'deletion' ? newLine || null : null,
      sceneNumber: sceneCounter || null,
      lineNumber: i + 1,
      pageNumber: Math.ceil((i + 1) / linesPerPage),
    });
  }

  if (changesToInsert.length > 0) {
    return db.insert(revisionChanges).values(changesToInsert).returning();
  }

  return [];
}

async function getLatestVersionForScript(
  db: DrizzleDB,
  scriptId: number,
  branchName: string
): Promise<number> {
  const maxResult = await db
    .select({ maxVersion: sql<number>`MAX(${revisions.versionNumber})` })
    .from(revisions)
    .where(and(eq(revisions.scriptId, scriptId), eq(revisions.branchName, branchName)));

  return maxResult[0]?.maxVersion ?? 0;
}

// Export reset function for testing
export async function resetInMemoryState(db: DrizzleDB) {
  await db.delete(revisionChanges);
  await db.delete(revisions);
}

// Helper to get next revision ID
async function getNextRevisionId(db: DrizzleDB): Promise<number> {
  const result = await db
    .select({ maxId: sql<number>`MAX(${revisions.id})` })
    .from(revisions);
  return (result[0]?.maxId ?? 0) + 1;
}

export const revisionsRouter = {
  listRevisions: protectedProcedure
    .input(z.object({
      scriptId: z.number().int().positive(),
      branchName: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      await verifyScriptAccess(ctx.db!, input.scriptId, ctx.userId!);
      const conditions = [eq(revisions.scriptId, input.scriptId)];

      if (input.branchName) {
        conditions.push(eq(revisions.branchName, input.branchName));
      }

      const results = await ctx.db!
        .select()
        .from(revisions)
        .where(and(...conditions))
        .orderBy(desc(revisions.versionNumber));

      return results;
    }),

  getRevision: protectedProcedure
    .input(z.object({
      id: z.number().int().positive(),
    }))
    .query(async ({ input, ctx }) => {
      const { revision } = await verifyRevisionAccess(ctx.db!, input.id, ctx.userId!);
      return revision;
    }),

  createRevision: protectedProcedure
    .input(z.object({
      scriptId: z.number().int().positive(),
      title: z.string().min(1).max(255),
      summary: z.string().max(2000).optional(),
      content: z.string().max(100000),
      branchName: z.string().default('main'),
      parentRevisionId: z.number().int().positive().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      if (!ctx.userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
      }
      await verifyScriptAccess(ctx.db!, input.scriptId, ctx.userId!);

      const nextVersion = await getLatestVersionForScript(
        ctx.db,
        input.scriptId,
        input.branchName
      ) + 1;

      const nextId = await getNextRevisionId(ctx.db);

      const result = await ctx.db
        .insert(revisions)
        .values({
          id: nextId,
          scriptId: input.scriptId,
          versionNumber: nextVersion,
          branchName: input.branchName,
          parentRevisionId: input.parentRevisionId || null,
          title: input.title,
          summary: input.summary || null,
          content: input.content,
          authorId: ctx.userId,
          status: 'draft',
          reviewedById: null,
          reviewedAt: null,
          createdAt: new Date(),
          updatedAt: new Date(),
        })
        .returning();

      const revision = result[0]!;

      if (input.parentRevisionId) {
        const parentResult = await ctx.db
          .select()
          .from(revisions)
          .where(eq(revisions.id, input.parentRevisionId));
        const parent = parentResult[0];
        if (parent) {
          await computeDiffForContent(ctx.db, parent.content, input.content, revision.id);
        }
      }

      return revision;
    }),

  updateRevision: protectedProcedure
    .input(z.object({
      id: z.number().int().positive(),
      title: z.string().min(1).max(255).optional(),
      summary: z.string().max(2000).optional(),
      content: z.string().optional(),
      status: z.enum(['draft', 'pending_review', 'accepted', 'rejected']).optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { revision } = await verifyRevisionAccess(ctx.db!, input.id, ctx.userId!);

      const updated = await ctx.db
        .update(revisions)
        .set({
          title: input.title ?? revision.title,
          summary: input.summary ?? revision.summary,
          content: input.content ?? revision.content,
          status: input.status ?? revision.status,
          updatedAt: new Date(),
        })
        .where(eq(revisions.id, input.id))
        .returning();

      return updated[0]!;
    }),

  deleteRevision: protectedProcedure
    .input(z.object({
      id: z.number().int().positive(),
    }))
    .mutation(async ({ input, ctx }) => {
      await verifyRevisionAccess(ctx.db!, input.id, ctx.userId!);

      const result = await ctx.db
        .delete(revisions)
        .where(eq(revisions.id, input.id))
        .returning();

      await ctx.db
        .delete(revisionChanges)
        .where(eq(revisionChanges.revisionId, input.id));

      return { success: true };
    }),

  getRevisionChanges: protectedProcedure
    .input(z.object({
      revisionId: z.number().int().positive(),
    }))
    .query(async ({ input, ctx }) => {
      await verifyRevisionAccess(ctx.db!, input.revisionId, ctx.userId!);

      const changes = await ctx.db
        .select()
        .from(revisionChanges)
        .where(eq(revisionChanges.revisionId, input.revisionId))
        .orderBy(asc(revisionChanges.lineNumber));

      return changes;
    }),

  compareRevisions: protectedProcedure
    .input(z.object({
      baseRevisionId: z.number().int().positive(),
      targetRevisionId: z.number().int().positive(),
    }))
    .query(async ({ input, ctx }) => {
      const { revision: baseRevision } = await verifyRevisionAccess(ctx.db!, input.baseRevisionId, ctx.userId!);
      const { revision: targetRevision } = await verifyRevisionAccess(ctx.db!, input.targetRevisionId, ctx.userId!);

      const oldLines = baseRevision.content.split('\n');
      const newLines = targetRevision.content.split('\n');

      let additions = 0;
      let deletions = 0;
      let modifications = 0;

      const maxLen = Math.max(oldLines.length, newLines.length);
      for (let i = 0; i < maxLen; i++) {
        const oldLine = oldLines[i];
        const newLine = newLines[i];

        if (oldLine === newLine) continue;

        if (!oldLine && newLine) {
          additions++;
        } else if (oldLine && !newLine) {
          deletions++;
        } else {
          modifications++;
        }
      }

      return {
        baseRevision,
        targetRevision,
        diff: {
          additions,
          deletions,
          modifications,
        },
      };
    }),

  acceptRevision: protectedProcedure
    .input(z.object({
      revisionId: z.number().int().positive(),
    }))
    .mutation(async ({ input, ctx }) => {
      if (!ctx.userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
      }
      await verifyRevisionAccess(ctx.db!, input.revisionId, ctx.userId!);

      const updated = await ctx.db
        .update(revisions)
        .set({
          status: 'accepted',
          reviewedById: ctx.userId,
          reviewedAt: new Date(),
          updatedAt: new Date(),
        })
        .where(eq(revisions.id, input.revisionId))
        .returning();

      return updated[0]!;
    }),

  rejectRevision: protectedProcedure
    .input(z.object({
      revisionId: z.number().int().positive(),
      reason: z.string().max(1000).optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      if (!ctx.userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
      }
      const { revision } = await verifyRevisionAccess(ctx.db!, input.revisionId, ctx.userId!);

      const newSummary = input.reason
        ? (revision.summary || '') + '\n[Rejected: ' + input.reason + ']'
        : revision.summary;

      const updated = await ctx.db
        .update(revisions)
        .set({
          status: 'rejected',
          reviewedById: ctx.userId,
          reviewedAt: new Date(),
          summary: newSummary,
          updatedAt: new Date(),
        })
        .where(eq(revisions.id, input.revisionId))
        .returning();

      return updated[0]!;
    }),

  rollbackToRevision: protectedProcedure
    .input(z.object({
      scriptId: z.number().int().positive(),
      revisionId: z.number().int().positive(),
    }))
    .mutation(async ({ input, ctx }) => {
      if (!ctx.userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
      }
      await verifyScriptAccess(ctx.db!, input.scriptId, ctx.userId!);
      const { revision: targetRevision } = await verifyRevisionAccess(ctx.db!, input.revisionId, ctx.userId!);

      if (targetRevision.scriptId !== input.scriptId) {
        throw new TRPCError({ code: 'BAD_REQUEST', message: 'Revision does not belong to the specified script' });
      }

      const nextVersion = await getLatestVersionForScript(
        ctx.db,
        input.scriptId,
        targetRevision.branchName
      ) + 1;

      const nextId = await getNextRevisionId(ctx.db);

      const rollbackRevision = await ctx.db
        .insert(revisions)
        .values({
          id: nextId,
          scriptId: input.scriptId,
          versionNumber: nextVersion,
          branchName: targetRevision.branchName,
          parentRevisionId: targetRevision.id,
          title: `Rollback to v${targetRevision.versionNumber}: ${targetRevision.title}`,
          summary: `Rolled back to revision ${targetRevision.id}`,
          content: targetRevision.content,
          authorId: ctx.userId,
          status: 'draft',
          reviewedById: null,
          reviewedAt: null,
          createdAt: new Date(),
          updatedAt: new Date(),
        })
        .returning();

      return rollbackRevision[0]!;
    }),

  getTimeline: protectedProcedure
    .input(z.object({
      scriptId: z.number().int().positive(),
    }))
    .query(async ({ input, ctx }) => {
      await verifyScriptAccess(ctx.db!, input.scriptId, ctx.userId!);
      const scriptRevisions = await ctx.db
        .select()
        .from(revisions)
        .where(eq(revisions.scriptId, input.scriptId))
        .orderBy(asc(revisions.createdAt));

      const timeline = await Promise.all(
        scriptRevisions.map(async (rev) => {
          const changes = await ctx.db
            .select()
            .from(revisionChanges)
            .where(eq(revisionChanges.revisionId, rev.id));

          const additions = changes.filter(c => c.changeType === 'addition').length;
          const deletions = changes.filter(c => c.changeType === 'deletion').length;
          const modifications = changes.filter(c => c.changeType === 'modification').length;

          return {
            revision: rev,
            changeCount: changes.length,
            additions,
            deletions,
            modifications,
          };
        })
      );

      return timeline;
    }),

  getBranches: protectedProcedure
    .input(z.object({
      scriptId: z.number().int().positive(),
    }))
    .query(async ({ input, ctx }) => {
      await verifyScriptAccess(ctx.db!, input.scriptId, ctx.userId!);
      const scriptRevisions = await ctx.db
        .select()
        .from(revisions)
        .where(eq(revisions.scriptId, input.scriptId));

      const branchMap = new Map<string, typeof scriptRevisions>();
      for (const rev of scriptRevisions) {
        if (!branchMap.has(rev.branchName)) {
          branchMap.set(rev.branchName, []);
        }
        branchMap.get(rev.branchName)!.push(rev);
      }

      const branches = Array.from(branchMap.entries()).map(([branchName, revs]) => {
        const sorted = revs.sort((a, b) => b.versionNumber - a.versionNumber);
        const latest = sorted[0]!;

        return {
          branchName,
          revisionCount: revs.length,
          latestVersion: latest.versionNumber,
          latestRevision: latest,
        };
      });

      return branches;
    }),

  createBranch: protectedProcedure
    .input(z.object({
      scriptId: z.number().int().positive(),
      branchName: z.string().min(1),
      fromRevisionId: z.number().int().positive().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      if (!ctx.userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
      }
      await verifyScriptAccess(ctx.db!, input.scriptId, ctx.userId!);

      const existingResult = await ctx.db
        .select({ id: revisions.id })
        .from(revisions)
        .where(and(
          eq(revisions.scriptId, input.scriptId),
          eq(revisions.branchName, input.branchName)
        ));

      if (existingResult.length > 0) {
        throw new TRPCError({ code: 'CONFLICT', message: `Branch '${input.branchName}' already exists for this script` });
      }

      let sourceContent = '';
      let parentRevisionId: number | null = null;

      if (input.fromRevisionId) {
        const sourceResult = await ctx.db
          .select()
          .from(revisions)
          .where(eq(revisions.id, input.fromRevisionId));
        const source = sourceResult[0];
        if (!source) {
          throw new TRPCError({ code: 'NOT_FOUND', message: `Source revision ${input.fromRevisionId} not found` });
        }
        sourceContent = source.content;
        parentRevisionId = source.id;
      } else {
        const mainRevisions = await ctx.db
          .select()
          .from(revisions)
          .where(and(
            eq(revisions.scriptId, input.scriptId),
            eq(revisions.branchName, 'main')
          ))
          .orderBy(desc(revisions.versionNumber))
          .limit(1);

        if (mainRevisions.length > 0) {
          sourceContent = mainRevisions[0]!.content;
          parentRevisionId = mainRevisions[0]!.id;
        }
      }

      const nextId = await getNextRevisionId(ctx.db);

      const branchRevision = await ctx.db
        .insert(revisions)
        .values({
          id: nextId,
          scriptId: input.scriptId,
          versionNumber: 1,
          branchName: input.branchName,
          parentRevisionId,
          title: `Branch: ${input.branchName}`,
          summary: null,
          content: sourceContent,
          authorId: ctx.userId,
          status: 'draft',
          reviewedById: null,
          reviewedAt: null,
          createdAt: new Date(),
          updatedAt: new Date(),
        })
        .returning();

      return branchRevision[0]!;
    }),

  mergeBranch: protectedProcedure
    .input(z.object({
      scriptId: z.number().int().positive(),
      sourceBranch: z.string(),
      targetBranch: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      if (!ctx.userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
      }
      await verifyScriptAccess(ctx.db!, input.scriptId, ctx.userId!);

      if (input.sourceBranch === input.targetBranch) {
        throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot merge a branch into itself' });
      }

      const sourceRevisions = await ctx.db
        .select()
        .from(revisions)
        .where(and(
          eq(revisions.scriptId, input.scriptId),
          eq(revisions.branchName, input.sourceBranch)
        ))
        .orderBy(desc(revisions.versionNumber))
        .limit(1);

      if (sourceRevisions.length === 0) {
        throw new TRPCError({ code: 'NOT_FOUND', message: `Source branch '${input.sourceBranch}' has no revisions` });
      }

      const sourceContent = sourceRevisions[0]!.content;
      const nextVersion = await getLatestVersionForScript(
        ctx.db,
        input.scriptId,
        input.targetBranch
      ) + 1;

      const nextId = await getNextRevisionId(ctx.db);

      const mergeRevision = await ctx.db
        .insert(revisions)
        .values({
          id: nextId,
          scriptId: input.scriptId,
          versionNumber: nextVersion,
          branchName: input.targetBranch,
          parentRevisionId: null,
          title: `Merge from '${input.sourceBranch}'`,
          summary: `Merged ${input.sourceBranch} into ${input.targetBranch}`,
          content: sourceContent,
          authorId: ctx.userId,
          status: 'draft',
          reviewedById: null,
          reviewedAt: null,
          createdAt: new Date(),
          updatedAt: new Date(),
        })
        .returning();

      return mergeRevision[0]!;
    }),
};
