import {readFileSync, writeFileSync, existsSync} from "fs"
import {api, bot, util} from "simplex-chat"
import {T} from "@simplex-chat/types"
import {parseConfig} from "./config.js"
import {SupportBot} from "./bot.js"
import {GrokApiClient, GrokMessage} from "./grok.js"
import {loadGrokContext} from "./context.js"
import {welcomeMessage} from "./messages.js"
import {profileMutex, log, logError, getGroupInfo, getContact} from "./util.js"

interface BotState {
  teamGroupId?: number
  grokContactId?: number
  grokUserId?: number
}

function readState(path: string): BotState {
  if (!existsSync(path)) return {}
  try { return JSON.parse(readFileSync(path, "utf-8")) } catch { return {} }
}

function writeState(path: string, state: BotState): void {
  writeFileSync(path, JSON.stringify(state), "utf-8")
}

async function main(): Promise<void> {
  const config = parseConfig(process.argv.slice(2))
  // Do not log config.db.connectionString — typically contains credentials.
  log("Config parsed", {
    stateFile: config.stateFile,
    backend: config.db.type,
    teamGroup: config.teamGroup,
    teamMembers: config.teamMembers,
    timezone: config.timezone,
    completeHours: config.completeHours,
  })
  const grokEnabled = config.grokApiKey !== null
  if (!grokEnabled) log("No GROK_API_KEY provided, disabling Grok support")

  const stateFilePath = config.stateFile
  const state = readState(stateFilePath)

  // Forward-reference for event handlers during init
  let supportBot: SupportBot | undefined

  // On restart, the active user may be Grok (if the previous run was killed
  // mid-profile-switch). bot.run() uses apiGetActiveUser() and would then
  // operate against the Grok userId as if it were the main user. Restore
  // the main user as active before bot.run(). Grok is identified by the
  // userId persisted in state.json on first resolution — comparing by
  // profile name is fragile to renames.
  if (state.grokUserId !== undefined) {
    const preChat = await api.ChatApi.init(config.db)
    try {
      const activeUser = await preChat.apiGetActiveUser()
      if (activeUser && activeUser.userId === state.grokUserId) {
        const users = await preChat.apiListUsers()
        const mainCandidates = users.filter(u => u.user.userId !== state.grokUserId)
        if (mainCandidates.length === 0) {
          throw new Error(
            `DB has only the Grok user (userId=${state.grokUserId}); no main user to restore. ` +
            `Likely a corrupted migration or partial restore.`
          )
        }
        if (mainCandidates.length > 1) {
          const names = mainCandidates.map(u => `${u.user.userId}:${u.user.profile.displayName}`).join(", ")
          throw new Error(
            `Ambiguous DB state: multiple non-Grok users [${names}]. ` +
            `Refusing to guess which is main — remove extras manually.`
          )
        }
        const mainUserInfo = mainCandidates[0]
        await preChat.apiSetActiveUser(mainUserInfo.user.userId)
        log(`Restored active user to ${mainUserInfo.user.profile.displayName} (userId=${mainUserInfo.user.userId})`)
      }
    } finally {
      await preChat.close()
    }
  }

  // Profile images (base64-encoded JPEG)
  const supportImage = "data:image/jpg;base64, "
  const grokImage = "data:image/jpg;base64, "

  const desiredCommands: T.ChatBotCommand[] = [
    ...(grokEnabled ? [{type: "command" as const, keyword: "grok", label: "Ask Grok"}] : []),
    {type: "command", keyword: "team", label: "Switch to team"},
  ]

  // Step 1: Init main bot via bot.run()
  log("Initializing main bot...")
  const [chat, mainUser, mainAddress] = await bot.run({
    profile: {displayName: "Ask SimpleX Team", fullName: "", image: supportImage},
    dbOpts: config.db,
    options: {
      addressSettings: {
        businessAddress: true,
        autoAccept: true,
        welcomeMessage,
      },
      commands: desiredCommands,
      useBotProfile: true,
      updateProfile: false,
    },
    events: {
      acceptingBusinessRequest: (evt) => supportBot?.onBusinessRequest(evt),
      newChatItems: (evt) => supportBot?.onNewChatItems(evt),
      chatItemUpdated: (evt) => supportBot?.onChatItemUpdated(evt),
      chatItemReaction: (evt) => supportBot?.onChatItemReaction(evt),
      leftMember: (evt) => supportBot?.onLeftMember(evt),
      joinedGroupMember: (evt) => supportBot?.onJoinedGroupMember(evt),
      connectedToGroupMember: (evt) => supportBot?.onMemberConnected(evt),
      newMemberContactReceivedInv: (evt) => supportBot?.onMemberContactReceivedInv(evt),
      contactConnected: (evt) => supportBot?.onContactConnected(evt),
      contactSndReady: (evt) => supportBot?.onContactSndReady(evt),
    },
  })
  log(`Main bot user: ${mainUser.profile.displayName} (userId=${mainUser.userId})`)

  // Step 2: Resolve Grok profile from same ChatApi instance.
  // Identify Grok strictly by the persisted userId in state.json. If no ID
  // is persisted, this is a first-time run — create the user and persist.
  let grokUser: T.User | null = null
  if (grokEnabled) {
    log("Resolving Grok profile...")
    if (state.grokUserId !== undefined) {
      const users = await chat.apiListUsers()
      grokUser = users.find(u => u.user.userId === state.grokUserId)?.user ?? null
      if (!grokUser) {
        throw new Error(
          `Persisted Grok userId=${state.grokUserId} not found in DB. ` +
          `Either restore the user or delete state.json to re-create Grok.`
        )
      }
    } else {
      log("Creating Grok profile...")
      grokUser = await chat.apiCreateActiveUser({displayName: "Grok", fullName: "", image: grokImage})
      // apiCreateActiveUser sets Grok as active — switch back to main
      await chat.apiSetActiveUser(mainUser.userId)
      state.grokUserId = grokUser.userId
      writeState(stateFilePath, state)
      log(`Persisted Grok userId=${grokUser.userId}`)
    }

    // Refresh Grok's profile if it has drifted from the canonical values.
    const grokProfile: T.Profile = {displayName: "Grok", fullName: "", image: grokImage}
    const currentProfile = util.fromLocalProfile(grokUser.profile)
    if (currentProfile.image !== grokProfile.image || currentProfile.displayName !== grokProfile.displayName || currentProfile.fullName !== grokProfile.fullName) {
      log("Grok profile changed, updating...")
      await chat.apiSetActiveUser(grokUser.userId)
      const summary = await chat.apiUpdateProfile(grokUser.userId, grokProfile)
      await chat.apiSetActiveUser(mainUser.userId)
      if (summary) {
        log(`Grok profile updated: ${summary.updateSuccesses} contact(s) updated, ${summary.updateFailures} failed`)
      } else {
        log("Unexpected: Grok profile did not change")
      }
    }
    log(`Grok profile: ${grokUser.profile.displayName} (userId=${grokUser.userId})`)
  }

  // Step 3: Read state file
  // Step 4: Enable auto-accept DM contacts
  await chat.apiSetAutoAcceptMemberContacts(mainUser.userId, true)
  log("Auto-accept member contacts enabled")

  // Step 5: Resolve Grok contact by ID. Avoid apiListContacts — it loads
  // every contact in one response and OOMs the native binding on large DBs.
  // Always restore grokContactId so the one-way gate can find and remove
  // Grok members even when Grok API is disabled.
  if (typeof state.grokContactId === "number") {
    const found = await getContact(chat, state.grokContactId)
    if (found) {
      config.grokContactId = found.contactId
      log(`Grok contact from state: ID=${config.grokContactId}`)
    } else {
      log(`Persisted Grok contact ID=${state.grokContactId} not found`)
    }
  }

  if (grokEnabled) {
    if (config.grokContactId === null) {
      log("Establishing bot↔Grok contact...")
      const invLink = await chat.apiCreateLink(mainUser.userId)
      // Switch to Grok profile to connect
      await profileMutex.runExclusive(async () => {
        await chat.apiSetActiveUser(grokUser!.userId)
        await chat.apiConnectActiveUser(invLink)
        await chat.apiSetActiveUser(mainUser.userId)
      })
      log("Grok connecting...")

      const grokProfileName = grokUser!.profile.displayName
      const evt = await chat.wait(
        "contactConnected",
        (e) =>
          e.user.userId === mainUser.userId
          && e.contact.profile.displayName === grokProfileName,
        60_000,
      )
      if (!evt) {
        console.error(`Timeout waiting for Grok contact (60s, displayName="${grokProfileName}"). Exiting.`)
        process.exit(1)
      }
      config.grokContactId = evt.contact.contactId
      state.grokContactId = config.grokContactId
      writeState(stateFilePath, state)
      log(`Grok contact established: ID=${config.grokContactId}`)
    }
  }

  // Step 6: Resolve team group by ID. Avoid apiListGroups — it loads every
  // group in one response and OOMs the native binding on large DBs.
  log("Resolving team group...")
  let existingGroup: T.GroupInfo | null = null

  if (typeof state.teamGroupId === "number") {
    existingGroup = await getGroupInfo(chat, state.teamGroupId)
    if (existingGroup) {
      config.teamGroup.id = existingGroup.groupId
      log(`Team group from state: ${config.teamGroup.id}:${existingGroup.groupProfile.displayName}`)
    } else {
      log(`Persisted team group ID=${state.teamGroupId} not found, will create`)
    }
  }

  const teamGroupPreferences: T.GroupPreferences = {
    directMessages: {enable: T.GroupFeatureEnabled.On},
    fullDelete: {enable: T.GroupFeatureEnabled.On},
    commands: [
      {type: "command", keyword: "join", label: "Join customer chat", params: "groupId"},
    ],
  }

  if (config.teamGroup.id === 0) {
    log(`Creating team group "${config.teamGroup.name}"...`)
    const newGroup = await chat.apiNewGroup(mainUser.userId, {
      displayName: config.teamGroup.name,
      fullName: "",
      groupPreferences: teamGroupPreferences,
    })
    config.teamGroup.id = newGroup.groupId
    state.teamGroupId = config.teamGroup.id
    writeState(stateFilePath, state)
    log(`Team group created: ${config.teamGroup.id}:${config.teamGroup.name}`)
  } else if (existingGroup) {
    // Only update profile if preferences or name changed
    const prefs = existingGroup.fullGroupPreferences
    const needsUpdate =
      existingGroup.groupProfile.displayName !== config.teamGroup.name ||
      prefs.directMessages?.enable !== T.GroupFeatureEnabled.On ||
      prefs.fullDelete?.enable !== T.GroupFeatureEnabled.On ||
      JSON.stringify(prefs.commands) !== JSON.stringify(teamGroupPreferences.commands)
    if (needsUpdate) {
      await chat.apiUpdateGroupProfile(config.teamGroup.id, {
        displayName: config.teamGroup.name,
        fullName: "",
        groupPreferences: teamGroupPreferences,
      })
      log("Team group profile updated")
    }
  }

  // Step 7: Ensure direct messages enabled (done via groupPreferences above)

  // Step 8: Create team group invite link (best-effort — bot works without it)
  let inviteLinkCreated = false
  try {
    try { await chat.apiDeleteGroupLink(config.teamGroup.id) } catch {}
    const teamGroupInviteLink = await chat.apiCreateGroupLink(
      config.teamGroup.id, T.GroupMemberRole.Member
    )
    inviteLinkCreated = true
    log("Team group invite link created")
    console.log(`\nTeam group invite link (expires in 10 min):\n${teamGroupInviteLink}\n`)
  } catch (err) {
    logError("Failed to create team group invite link (SMP relay may be unreachable). Bot will continue without it.", err)
  }

  let inviteLinkDeleted = false
  async function deleteInviteLink(): Promise<void> {
    if (inviteLinkDeleted) return
    inviteLinkDeleted = true
    try {
      await profileMutex.runExclusive(async () => {
        await chat.apiSetActiveUser(mainUser.userId)
        await chat.apiDeleteGroupLink(config.teamGroup.id)
      })
      log("Team group invite link deleted")
    } catch (err) {
      logError("Failed to delete invite link", err)
    }
  }
  let inviteLinkTimer: ReturnType<typeof setTimeout> | undefined
  if (inviteLinkCreated) {
    inviteLinkTimer = setTimeout(async () => {
      log("10 minutes elapsed, deleting invite link...")
      await deleteInviteLink()
    }, 10 * 60 * 1000)
    inviteLinkTimer.unref()
  }

  // Step 9: Validate team members (lookup by ID, one round-trip per member)
  if (config.teamMembers.length > 0) {
    log("Validating team members...")
    for (const member of config.teamMembers) {
      const contact = await getContact(chat, member.id)
      if (!contact) {
        console.error(`Team member not found: ID=${member.id}`)
        process.exit(1)
      }
      if (contact.profile.displayName !== member.name) {
        console.error(`Team member name mismatch: expected "${member.name}", got "${contact.profile.displayName}" (ID=${member.id})`)
        process.exit(1)
      }
      log(`Team member validated: ${member.id}:${member.name}`)
    }
  }

  // Load Grok context and build API client only if enabled
  let grokApi: GrokApiClient | null = null
  if (grokEnabled) {
    let initialContext: GrokMessage[] = []
    if (config.contextFile) {
      try {
        initialContext = loadGrokContext(config.contextFile)
        log(`Loaded Grok context: ${initialContext.length} message(s) from ${config.contextFile}`)
      } catch (err) {
        const e = err as NodeJS.ErrnoException
        if (e.code === "ENOENT") {
          log(`Warning: context file not found: ${config.contextFile}`)
        } else {
          logError(`Failed to load Grok context file ${config.contextFile}`, err)
          throw err
        }
      }
    }
    grokApi = new GrokApiClient(config.grokApiKey, config.aiUrl, config.aiModel, initialContext)
  }

  // Create SupportBot
  supportBot = new SupportBot(chat, grokApi, config, mainUser.userId, grokUser?.userId ?? null, desiredCommands)

  if (mainAddress) {
    supportBot.businessAddress = util.contactAddressStr(mainAddress.connLinkContact)
    log(`Business address: ${supportBot.businessAddress}`)
  }

  // Step 10: Register Grok event handlers (filtered by profile in handler)
  if (grokEnabled) {
    chat.on("receivedGroupInvitation", (evt) => supportBot?.onGrokGroupInvitation(evt))
    chat.on("connectedToGroupMember", (evt) => supportBot?.onGrokMemberConnected(evt))
    chat.on("newChatItems", (evt) => supportBot?.onGrokNewChatItems(evt))
  }

  // Step 10b: Refresh stale cards from before restart
  await supportBot.cards.refreshAllCards()

  log("SupportBot initialized. Bot running.")

  // Step 11: Graceful shutdown
  async function shutdown(signal: string): Promise<void> {
    log(`Received ${signal}, shutting down...`)
    clearTimeout(inviteLinkTimer)
    supportBot?.cards.destroy()
    await deleteInviteLink()
    process.exit(0)
  }
  process.on("SIGINT", () => shutdown("SIGINT"))
  process.on("SIGTERM", () => shutdown("SIGTERM"))
}

main().catch(err => {
  logError("Fatal error", err)
  process.exit(1)
})
