import type { Response } from "express"
import { prisma } from "@codeflash-ai/common"
import { AuthorizedUserReq } from "../types.js"
import { githubApp } from "../github/github-app.js"
import { isUserCollaborator } from "../github/github-utils.js"
import { userNickname } from "../auth0-mgmt.js"
import type { Octokit } from "octokit"

export interface CommitStagingCodeRequestBody {
  traceId: string
  fileChanges: Record<string, string> // filePath -> newContent
  commitMessage?: string
}

export interface CommitStagingCodeDependencies {
  prisma: {
    optimization_events: {
      findFirst: typeof prisma.optimization_events.findFirst
    }
  }
  getInstallationOctokit: (installationId: number) => Promise<Octokit>
  userNickname: typeof userNickname
  isUserCollaborator: typeof isUserCollaborator
}

let dependencies: CommitStagingCodeDependencies = {
  prisma: {
    optimization_events: {
      findFirst: prisma.optimization_events.findFirst,
    },
  },
  getInstallationOctokit: async (installationId: number) =>
    await (githubApp.getInstallationOctokit(installationId) as Promise<Octokit>),
  userNickname,
  isUserCollaborator,
}

export function setCommitStagingCodeDependencies(newDependencies: CommitStagingCodeDependencies) {
  dependencies = newDependencies
}

export function resetCommitStagingCodeDependencies() {
  dependencies = {
    prisma: {
      optimization_events: {
        findFirst: prisma.optimization_events.findFirst,
      },
    },
    getInstallationOctokit: async (installationId: number) =>
      await (githubApp.getInstallationOctokit(installationId) as Promise<Octokit>),
    userNickname,
    isUserCollaborator,
  }
}

export default async function commitStagingCode(req: AuthorizedUserReq, res: Response) {
  const result = await executeCommitStagingCode(req.body, req.userId)
  return res.status(result.status).json(result.data)
}

export interface CommitStagingCodeResult {
  status: number
  data: Record<string, unknown>
}

export async function executeCommitStagingCode(
  body: Record<string, unknown>,
  userId?: string,
): Promise<CommitStagingCodeResult> {
  if (!userId) {
    return {
      status: 401,
      data: { error: "Unauthorized - user ID is required" },
    }
  }

  const { traceId, fileChanges, commitMessage } = body as unknown as CommitStagingCodeRequestBody

  if (!traceId) {
    return {
      status: 400,
      data: { error: "Missing traceId in request body" },
    }
  }

  if (!fileChanges || Object.keys(fileChanges).length === 0) {
    return {
      status: 400,
      data: { error: "Missing fileChanges in request body" },
    }
  }

  try {
    // Get user nickname first
    const nickname = await dependencies.userNickname(userId)
    if (nickname == null) {
      return {
        status: 401,
        data: { error: "Unauthorized - could not get user nickname" },
      }
    }

    // Find the staging event (only for git_branch storage type)
    const stagingEvent = await dependencies.prisma.optimization_events.findFirst({
      where: {
        trace_id: traceId,
        user_id: userId,
        is_staging: true,
        staging_storage_type: "git_branch",
      },
      include: {
        repository: true,
      },
    })

    if (!stagingEvent) {
      return {
        status: 404,
        data: { error: "No git_branch staging event found for the given traceId" },
      }
    }

    const metadata = (stagingEvent.metadata ?? {}) as Record<string, unknown>
    const stagingBranchName = metadata.staging_branch_name as string | undefined

    if (!stagingBranchName) {
      return {
        status: 404,
        data: { error: "No staging branch name found in metadata" },
      }
    }

    // Get repository info
    const repository = stagingEvent.repository
    if (!repository?.installation_id) {
      return {
        status: 404,
        data: { error: "No repository or installation found for this staging event" },
      }
    }

    const [owner, repo] = String(repository.full_name).split("/")
    const installationOctokit = await dependencies.getInstallationOctokit(
      Number(repository.installation_id),
    )

    // Check if user is a collaborator before proceeding
    const isCollaborator = await dependencies.isUserCollaborator(
      installationOctokit,
      owner,
      repo,
      nickname,
    )

    if (!isCollaborator) {
      return {
        status: 403,
        data: {
          error: `User ${nickname} is not a collaborator on ${owner}/${repo}`,
          nickname,
        },
      }
    }

    // Commit the changes to the staging branch
    const commitSha = await commitChangesToBranch(
      installationOctokit,
      owner,
      repo,
      stagingBranchName,
      fileChanges,
      commitMessage || `Update optimized code via Codeflash`,
      nickname,
    )

    return {
      status: 200,
      data: {
        message: "Changes committed successfully",
        commitSha,
        stagingBranchName,
      },
    }
  } catch (error) {
    console.error("[commit-staging-code] Error:", error)
    return {
      status: 500,
      data: {
        error: `Failed to commit changes: ${error instanceof Error ? error.message : "Unknown error"}`,
      },
    }
  }
}

async function commitChangesToBranch(
  octokit: Octokit,
  owner: string,
  repo: string,
  branchName: string,
  fileChanges: Record<string, string>,
  commitMessage: string,
  authorName: string,
): Promise<string> {
  // Get the current branch reference
  const branchRef = await octokit.rest.git.getRef({
    owner,
    repo,
    ref: `heads/${branchName}`,
  })

  const currentCommitSha = branchRef.data.object.sha

  // Get the current commit to get the tree SHA
  const currentCommit = await octokit.rest.git.getCommit({
    owner,
    repo,
    commit_sha: currentCommitSha,
  })

  const baseTreeSha = currentCommit.data.tree.sha

  // Create blobs for each file change
  const treeItems: Array<{
    path: string
    mode: "100644"
    type: "blob"
    sha: string
  }> = []

  for (const [filePath, content] of Object.entries(fileChanges)) {
    const blob = await octokit.rest.git.createBlob({
      owner,
      repo,
      content: Buffer.from(content).toString("base64"),
      encoding: "base64",
    })

    treeItems.push({
      path: filePath,
      mode: "100644",
      type: "blob",
      sha: blob.data.sha,
    })
  }

  // Create a new tree with the changes
  const newTree = await octokit.rest.git.createTree({
    owner,
    repo,
    base_tree: baseTreeSha,
    tree: treeItems,
  })

  // Create a new commit
  const newCommit = await octokit.rest.git.createCommit({
    owner,
    repo,
    message: commitMessage,
    tree: newTree.data.sha,
    parents: [currentCommitSha],
    author: {
      name: authorName,
      email: `${authorName}@users.noreply.github.com`,
    },
  })

  // Update the branch reference to point to the new commit
  await octokit.rest.git.updateRef({
    owner,
    repo,
    ref: `heads/${branchName}`,
    sha: newCommit.data.sha,
  })

  return newCommit.data.sha
}
