import type { Response } from "express"
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 GetStagingCodeRequestBody {
  stagingBranchName: string
  baseBranch: string
  fullRepoName: string
  installationId: number
  functionName?: string
  filePath?: string
}

export interface GetStagingCodeDependencies {
  getInstallationOctokit: (installationId: number) => Promise<Octokit>
  userNickname: typeof userNickname
  isUserCollaborator: typeof isUserCollaborator
}

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

export function setGetStagingCodeDependencies(newDependencies: GetStagingCodeDependencies) {
  dependencies = newDependencies
}

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

export default async function getStagingCode(req: AuthorizedUserReq, res: Response) {
  const body = req.body as GetStagingCodeRequestBody

  if (!body.stagingBranchName || !body.fullRepoName || !body.installationId) {
    return res
      .status(400)
      .json({ error: "Missing required fields: stagingBranchName, fullRepoName, installationId" })
  }

  const result = await retrieveStagingCode(body, req.userId)
  return res.status(result.status).json(result.data)
}

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

export async function retrieveStagingCode(
  body: GetStagingCodeRequestBody,
  userId: string,
): Promise<StagingCodeResult> {
  try {
    const { stagingBranchName, baseBranch, fullRepoName, installationId, functionName, filePath } =
      body

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

    const [owner, repo] = fullRepoName.split("/")
    if (!owner || !repo) {
      return {
        status: 400,
        data: { error: "Invalid fullRepoName format, expected 'owner/repo'" },
      }
    }

    const installationOctokit = await dependencies.getInstallationOctokit(installationId)

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

    if (!isCollaborator) {
      console.log(`[get-staging-code] ${nickname} is not a collaborator on ${owner}/${repo}`)
      return {
        status: 403,
        data: {
          error: `User ${nickname} is not a collaborator on ${owner}/${repo}`,
          nickname,
        },
      }
    }

    // Get diff contents from the staging branch
    const diffContents = await getBranchDiffContents(
      installationOctokit,
      owner,
      repo,
      baseBranch || "main",
      stagingBranchName,
    )

    return {
      status: 200,
      data: {
        storageType: "git_branch",
        diffContents,
        stagingBranchName,
        baseBranch,
        functionName,
        filePath,
      },
    }
  } catch (error) {
    console.error("[get-staging-code] Error:", error)
    return {
      status: 500,
      data: {
        error: `Failed to retrieve staging code: ${error instanceof Error ? error.message : "Unknown error"}`,
      },
    }
  }
}

export interface DiffContent {
  oldContent: string
  newContent: string
}

async function getBranchDiffContents(
  octokit: Octokit,
  owner: string,
  repo: string,
  baseBranch: string,
  stagingBranch: string,
): Promise<Record<string, DiffContent>> {
  // Compare the two branches to get the changed files
  const comparison = await octokit.rest.repos.compareCommits({
    owner,
    repo,
    base: baseBranch,
    head: stagingBranch,
  })

  // Filter to only modified/added files
  const filesToFetch = (comparison.data.files ?? []).filter(
    file => file.status === "modified" || file.status === "added",
  )

  // Fetch both old and new content for each file in parallel
  const fileContents = await Promise.all(
    filesToFetch.map(async file => {
      try {
        // Fetch new content from staging branch
        const stagingFileResponse = await octokit.rest.repos.getContent({
          owner,
          repo,
          path: file.filename,
          ref: stagingBranch,
        })

        let newContent = ""
        if ("content" in stagingFileResponse.data) {
          newContent = Buffer.from(stagingFileResponse.data.content, "base64").toString("utf-8")
        }

        // Fetch old content from base branch (empty string for added files)
        let oldContent = ""
        if (file.status === "modified") {
          try {
            const baseFileResponse = await octokit.rest.repos.getContent({
              owner,
              repo,
              path: file.filename,
              ref: baseBranch,
            })
            if ("content" in baseFileResponse.data) {
              oldContent = Buffer.from(baseFileResponse.data.content, "base64").toString("utf-8")
            }
          } catch {
            // File doesn't exist in base branch, keep oldContent as empty string
          }
        }

        return {
          filename: file.filename,
          oldContent,
          newContent,
        }
      } catch {
        // File doesn't exist in staging branch (shouldn't happen for modified/added files)
      }
      return null
    }),
  )

  // Build diffContents record
  const diffContents: Record<string, DiffContent> = {}

  for (const file of fileContents) {
    if (file) {
      diffContents[file.filename] = {
        oldContent: file.oldContent,
        newContent: file.newContent,
      }
    }
  }

  return diffContents
}
