import * as core from "@actions/core"
import * as github from "@actions/github"
import type { Context } from "@actions/github/lib/context"
import type {
  IssueCommentEvent,
  IssuesEvent,
  PullRequestReviewCommentEvent,
  WorkflowDispatchEvent,
  WorkflowRunEvent,
  PullRequestEvent,
} from "@octokit/webhooks-types"
import { Octokit } from "@octokit/rest"
import { graphql } from "@octokit/graphql"
import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share/session"
import { Session } from "@/session/session"
import type { SessionID } from "../../session/schema"
import { AppRuntime } from "@/effect/app-runtime"
import { Process } from "@/util/process"
import { USER_EVENTS, REPO_EVENTS, SUPPORTED_EVENTS } from "./github-types"
import type { UserEvent, RepoEvent } from "./github-types"
import {
  normalizeRunEnv,
  getOidcToken,
  exchangeForAppToken,
  revokeAppToken,
} from "./github-run-auth"
import {
  createGitOps,
  checkoutNewBranch,
  checkoutLocalBranch,
  checkoutForkBranch,
  pushToNewBranch,
  pushToLocalBranch,
  pushToForkBranch,
  branchIsDirty,
  configureGit,
  restoreGitConfig,
} from "./github-run-git"
import {
  assertPermissions,
  addReaction,
  removeReaction,
  createComment,
  createPR,
  footer,
} from "./github-run-api"
import { fetchIssue, fetchPR, buildPromptDataForIssue, buildPromptDataForPR } from "./github-run-fetch"
import { getUserPrompt, subscribeSessionEvents, chat, summarize, isIssueCommentEvent } from "./github-run-prompt"

export const GithubRunCommand = cmd({
  command: "run",
  describe: "run the GitHub agent",
  builder: (yargs) =>
    yargs
      .option("event", {
        type: "string",
        describe: "GitHub mock event to run the agent for",
      })
      .option("token", {
        type: "string",
        describe: "GitHub personal access token (github_pat_********)",
      }),
  async handler(args) {
    await bootstrap(process.cwd(), async () => {
      const isMock = args.token || args.event

      const context = isMock ? (JSON.parse(args.event!) as Context) : github.context
      if (!SUPPORTED_EVENTS.includes(context.eventName as (typeof SUPPORTED_EVENTS)[number])) {
        core.setFailed(`Unsupported event type: ${context.eventName}`)
        process.exit(1)
      }

      const isUserEvent = USER_EVENTS.includes(context.eventName as UserEvent)
      const isRepoEvent = REPO_EVENTS.includes(context.eventName as RepoEvent)
      const isCommentEvent = ["issue_comment", "pull_request_review_comment"].includes(context.eventName)
      const isIssuesEvent = context.eventName === "issues"
      const isScheduleEvent = context.eventName === "schedule"
      const isWorkflowDispatchEvent = context.eventName === "workflow_dispatch"

      const { providerID, modelID, runId, share, oidcBaseUrl, useGithubToken } = normalizeRunEnv()
      const variant = process.env["VARIANT"] || undefined
      const { owner, repo } = context.repo
      const payload = context.payload as
        | IssueCommentEvent
        | IssuesEvent
        | PullRequestReviewCommentEvent
        | WorkflowDispatchEvent
        | WorkflowRunEvent
        | PullRequestEvent
      const issueEvent = isIssueCommentEvent(payload) ? payload : undefined
      const actor = isScheduleEvent ? undefined : context.actor

      const issueId = isRepoEvent
        ? undefined
        : context.eventName === "issue_comment" || context.eventName === "issues"
          ? (payload as IssueCommentEvent | IssuesEvent).issue.number
          : (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
      const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
      const shareBaseUrl = isMock ? "https://dev.saeeol.ai" : "https://saeeol.ai"

      const commentType = isCommentEvent
        ? context.eventName === "pull_request_review_comment"
          ? "pr_review"
          : "issue"
        : undefined
      const git = createGitOps()
      const triggerCommentId = isCommentEvent
        ? (payload as IssueCommentEvent | PullRequestReviewCommentEvent).comment.id
        : undefined

      let appToken: string | undefined
      let octoRest: Octokit | undefined
      let octoGraph: typeof graphql
      let gitConfig: string | undefined
      let session: { id: SessionID; title: string; version: string }
      let shareId: string | undefined
      let exitCode = 0

      const chatFn = (msg: string, files: Parameters<typeof chat>[5] = []) =>
        chat(session.id, variant, providerID, modelID, msg, files)

      try {
        if (useGithubToken) {
          const githubToken = process.env["GITHUB_TOKEN"]
          if (!githubToken) {
            throw new Error(
              "GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
            )
          }
          appToken = githubToken
        } else {
          const actionToken = isMock ? args.token! : await getOidcToken()
          appToken = await exchangeForAppToken(actionToken, oidcBaseUrl, owner, repo)
        }
        octoRest = new Octokit({ auth: appToken })
        octoGraph = graphql.defaults({
          headers: { authorization: `token ${appToken}` },
        })

        const { userPrompt, promptFiles } = await getUserPrompt({
          isRepoEvent,
          isIssuesEvent,
          isCommentEvent,
          eventName: context.eventName,
          payload,
          appToken,
        })
        if (!useGithubToken) {
          gitConfig = await configureGit(git, appToken, !!isMock)
        }
        if (isUserEvent) {
          await assertPermissions(octoRest, owner, repo, actor!)
          await addReaction(octoRest, owner, repo, triggerCommentId, commentType, issueId)
        }

        const repoData = await octoRest.rest.repos.get({ owner, repo })
        session = await AppRuntime.runPromise(
          Session.Service.use((svc) =>
            svc.create({ permission: [{ permission: "question", action: "deny", pattern: "*" }] }),
          ),
        )
        subscribeSessionEvents(session.id)
        shareId = await (async () => {
          if (share === false) return
          if (!share && repoData.data.private) return
          await AppRuntime.runPromise(SessionShare.Service.use((svc) => svc.share(session.id)))
          return session.id.slice(-8)
        })()
        console.log("saeeol session", session.id)

        const fallbackTitle = issueEvent
          ? issueEvent.issue.title
          : (payload as PullRequestReviewCommentEvent).pull_request.title

        if (isRepoEvent) {
          if (isWorkflowDispatchEvent && actor) {
            console.log(`Triggered by: ${actor}`)
          }
          const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
          const branch = await checkoutNewBranch(git, branchPrefix)
          const head = await git.text(["rev-parse", "HEAD"])
          const response = await chatFn(userPrompt, promptFiles)
          const { dirty, uncommittedChanges, switched } = await branchIsDirty(git, head, branch)
          if (switched) {
            console.log("Agent managed its own branch, skipping infrastructure push/PR")
            console.log("Response:", response)
          } else if (dirty) {
            const summary = await summarize(response, chatFn, fallbackTitle)
            await pushToNewBranch(git, summary, branch, uncommittedChanges, isScheduleEvent ? undefined : actor)
            const triggerType = isWorkflowDispatchEvent ? "workflow_dispatch" : "scheduled workflow"
            const pr = await createPR(
              octoRest, owner, repo, git,
              repoData.data.default_branch,
              branch,
              summary,
              `${response}\n\nTriggered by ${triggerType}${footer(shareId, shareBaseUrl, runUrl)}`,
            )
            if (pr) {
              console.log(`Created PR #${pr}`)
            } else {
              console.log("Skipped PR creation (no new commits)")
            }
          } else {
            console.log("Response:", response)
          }
        } else if (
          ["pull_request", "pull_request_review_comment"].includes(context.eventName) ||
          issueEvent?.issue.pull_request
        ) {
          const prData = await fetchPR(octoGraph, owner, repo, issueId!)
          if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) {
            await checkoutLocalBranch(git, prData)
            const head = await git.text(["rev-parse", "HEAD"])
            const dataPrompt = buildPromptDataForPR(prData, triggerCommentId)
            const response = await chatFn(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
            const { dirty, uncommittedChanges, switched } = await branchIsDirty(git, head, prData.headRefName)
            if (switched) {
              console.log("Agent managed its own branch, skipping infrastructure push")
            }
            if (dirty && !switched) {
              const summary = await summarize(response, chatFn, fallbackTitle)
              await pushToLocalBranch(git, summary, uncommittedChanges, actor)
            }
            await createComment(octoRest, owner, repo, issueId!, `${response}${footer(shareId, shareBaseUrl, runUrl)}`)
            await removeReaction(octoRest, owner, repo, triggerCommentId, commentType, issueId)
          } else {
            const forkBranch = await checkoutForkBranch(git, prData, issueId!)
            const head = await git.text(["rev-parse", "HEAD"])
            const dataPrompt = buildPromptDataForPR(prData, triggerCommentId)
            const response = await chatFn(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
            const { dirty, uncommittedChanges, switched } = await branchIsDirty(git, head, forkBranch)
            if (switched) {
              console.log("Agent managed its own branch, skipping infrastructure push")
            }
            if (dirty && !switched) {
              const summary = await summarize(response, chatFn, fallbackTitle)
              await pushToForkBranch(git, summary, prData, uncommittedChanges, actor)
            }
            await createComment(octoRest, owner, repo, issueId!, `${response}${footer(shareId, shareBaseUrl, runUrl)}`)
            await removeReaction(octoRest, owner, repo, triggerCommentId, commentType, issueId)
          }
        } else {
          const branch = await checkoutNewBranch(git, "issue", issueId)
          const head = await git.text(["rev-parse", "HEAD"])
          const issueData = await fetchIssue(octoGraph, owner, repo, issueId!)
          const dataPrompt = buildPromptDataForIssue(issueData, triggerCommentId)
          const response = await chatFn(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
          const { dirty, uncommittedChanges, switched } = await branchIsDirty(git, head, branch)
          if (switched) {
            await createComment(octoRest, owner, repo, issueId!, `${response}${footer(shareId, shareBaseUrl, runUrl)}`)
            await removeReaction(octoRest, owner, repo, triggerCommentId, commentType, issueId)
          } else if (dirty) {
            const summary = await summarize(response, chatFn, fallbackTitle)
            await pushToNewBranch(git, summary, branch, uncommittedChanges, actor)
            const pr = await createPR(
              octoRest, owner, repo, git,
              repoData.data.default_branch,
              branch,
              summary,
              `${response}\n\nCloses #${issueId}${footer(shareId, shareBaseUrl, runUrl)}`,
            )
            if (pr) {
              await createComment(octoRest, owner, repo, issueId!, `Created PR #${pr}${footer(shareId, shareBaseUrl, runUrl)}`)
            } else {
              await createComment(octoRest, owner, repo, issueId!, `${response}${footer(shareId, shareBaseUrl, runUrl)}`)
            }
            await removeReaction(octoRest, owner, repo, triggerCommentId, commentType, issueId)
          } else {
            await createComment(octoRest, owner, repo, issueId!, `${response}${footer(shareId, shareBaseUrl, runUrl)}`)
            await removeReaction(octoRest, owner, repo, triggerCommentId, commentType, issueId)
          }
        }
      } catch (e: any) {
        exitCode = 1
        console.error(e instanceof Error ? e.message : String(e))
        let msg = e
        if (e instanceof Process.RunFailedError) {
          msg = e.stderr.toString()
        } else if (e instanceof Error) {
          msg = e.message
        }
        if (isUserEvent) {
          if (octoRest) {
            await createComment(octoRest, owner, repo, issueId!, `${msg}${footer(shareId, shareBaseUrl, runUrl)}`)
            await removeReaction(octoRest, owner, repo, triggerCommentId, commentType, issueId)
          }
        }
        core.setFailed(msg)
      } finally {
        if (!useGithubToken) {
          await restoreGitConfig(git, gitConfig)
          if (appToken) await revokeAppToken(appToken)
        }
      }
      process.exit(exitCode)
    })
  },
})
