const pull = require("../server/node_modules/pull-stream")
const moment = require("../server/node_modules/moment")
const { getConfig } = require("../configs/config-manager.js")
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
const logLimit = getConfig().ssbLogStream?.limit || 1000

const norm = (s) => String(s || "").trim().toLowerCase()
const toNum = (v) => {
  const n = parseFloat(String(v ?? "").replace(",", "."))
  return Number.isFinite(n) ? n : NaN
}
const toInt = (v, fallback = 0) => {
  const n = parseInt(String(v ?? ""), 10)
  return Number.isFinite(n) ? n : fallback
}

const normalizeTags = (raw) => {
  if (raw === undefined || raw === null) return []
  if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean)
  return String(raw).split(",").map(t => t.trim()).filter(Boolean)
}

const matchSearch = (job, q) => {
  const qq = norm(q)
  if (!qq) return true
  const hay = [
    job.title,
    job.description,
    job.requirements,
    job.tasks,
    job.languages,
    Array.isArray(job.tags) ? job.tags.join(" ") : ""
  ].map(x => norm(x)).join(" ")
  return hay.includes(qq)
}

module.exports = ({ cooler, tribeCrypto }) => {
  let ssb
  const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }

  const readAll = async (ssbClient) =>
    new Promise((resolve, reject) =>
      pull(
        ssbClient.createLogStream({ limit: logLimit }),
        pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))
      )
    )

  const buildIndex = (messages, ssbClient) => {
    const tomb = new Set()
    const jobNodes = new Map()
    const parent = new Map()
    const child = new Map()
    const jobSubLatest = new Map()

    for (const m of messages) {
      const key = m.key
      const v = m.value || {}
      const c = v.content
      if (!c) continue

      if (c.type === "tombstone" && c.target) {
        tomb.add(c.target)
        continue
      }

      if (c.type === "job") {
        jobNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author })
        if (c.replaces) {
          parent.set(key, c.replaces)
          child.set(c.replaces, key)
        }
        continue
      }

      if (c.type === "job_sub" && c.jobId) {
        const author = v.author
        if (!author) continue
        const ts = v.timestamp || m.timestamp || 0
        const jobId = c.jobId
        const k = `${jobId}::${author}`
        const prev = jobSubLatest.get(k)
        if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
        continue
      }
    }

    const rootOf = (id) => {
      let cur = id
      while (parent.has(cur)) cur = parent.get(cur)
      return cur
    }

    const roots = new Set()
    for (const id of jobNodes.keys()) roots.add(rootOf(id))

    const tipOf = (id) => {
      let cur = id
      while (child.has(cur)) cur = child.get(cur)
      return cur
    }

    const tipByRoot = new Map()
    for (const r of roots) tipByRoot.set(r, tipOf(r))

    const subsByJob = new Map()
    for (const { jobId, author, value } of jobSubLatest.values()) {
      if (!subsByJob.has(jobId)) subsByJob.set(jobId, new Set())
      const set = subsByJob.get(jobId)
      if (value) set.add(author)
      else set.delete(author)
    }

    if (ssbClient) {
      for (const m of messages) {
        if (typeof m.value?.content !== 'string') continue
        try {
          const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 })
          if (!dec?.value?.content) continue
          const c = dec.value.content
          if (c.type !== 'job_sub' || !c.jobId) continue
          const author = dec.value.author
          if (!author) continue
          const ts = dec.value.timestamp || m.timestamp || 0
          const jobId = c.jobId
          const k = `${jobId}::${author}`
          const prev = jobSubLatest.get(k)
          if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId })
        } catch {}
      }
    }

    return { tomb, jobNodes, parent, child, rootOf, tipOf, tipByRoot, subsByJob }
  }

  const buildJobObject = (node, rootId, subscribers) => {
    const visibleSubs = (tribeCrypto && tribeCrypto.getKey(rootId)) || ssb?.id === (node.c?.author || node.author) ? subscribers : [];
    const c = node.c || {}
    let blobId = c.image || null
    if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1]

    const vacants = Math.max(1, toInt(c.vacants, 1))
    const salaryN = toNum(c.salary)
    const salary = Number.isFinite(salaryN) ? salaryN.toFixed(6) : "0.000000"

    const hoursOfferedN = toNum(c.hoursOffered)
    const hoursRequestedN = toNum(c.hoursRequested)
    return {
      id: node.key,
      rootId,
      job_type: c.job_type,
      title: c.title,
      description: c.description,
      requirements: c.requirements,
      languages: c.languages,
      job_time: c.job_time,
      tasks: c.tasks,
      location: c.location,
      vacants,
      salary,
      hoursOffered: Number.isFinite(hoursOfferedN) ? hoursOfferedN : 0,
      hoursRequested: Number.isFinite(hoursRequestedN) ? hoursRequestedN : 0,
      exchangeSkill: String(c.exchangeSkill || ""),
      image: blobId,
      author: c.author,
      createdAt: c.createdAt || new Date(node.ts).toISOString(),
      updatedAt: c.updatedAt || null,
      status: c.status || "OPEN",
      tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags),
      subscribers: Array.isArray(visibleSubs) ? visibleSubs : [],
      mapUrl: c.mapUrl || "",
      visibility: String(c.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC",
      clearnetPublic: !!c.clearnetPublic
    }
  }

  return {
    type: "job",

    async createJob(jobData) {
      const ssbClient = await openSsb()

      const job_type = String(jobData.job_type || "").toLowerCase()
      if (!["freelancer", "employee", "exchange"].includes(job_type)) throw new Error("Invalid job type")

      const title = String(jobData.title || "").trim()
      const description = String(jobData.description || "").trim()
      if (!title) throw new Error("Invalid title")
      if (!description) throw new Error("Invalid description")

      const vacants = Math.max(1, toInt(jobData.vacants, 1))
      const salaryN = toNum(jobData.salary)
      const salary = Number.isFinite(salaryN) ? salaryN.toFixed(6) : "0.000000"

      const job_time = String(jobData.job_time || "").toLowerCase()
      if (!["partial", "complete"].includes(job_time)) throw new Error("Invalid job time")

      const location = String(jobData.location || "").toLowerCase()
      if (!["remote", "presencial"].includes(location)) throw new Error("Invalid location")

      let blobId = jobData.image || null
      if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1]

      const tags = normalizeTags(jobData.tags)

      const hoursOfferedN = toNum(jobData.hoursOffered)
      const hoursRequestedN = toNum(jobData.hoursRequested)
      const content = {
        type: "job",
        job_type,
        title,
        description,
        requirements: String(jobData.requirements || ""),
        languages: String(jobData.languages || ""),
        job_time,
        tasks: String(jobData.tasks || ""),
        location,
        vacants,
        salary,
        hoursOffered: Number.isFinite(hoursOfferedN) && hoursOfferedN > 0 ? hoursOfferedN : 0,
        hoursRequested: Number.isFinite(hoursRequestedN) && hoursRequestedN > 0 ? hoursRequestedN : 0,
        exchangeSkill: String(jobData.exchangeSkill || "").trim(),
        image: blobId,
        tags,
        author: ssbClient.id,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        status: "OPEN",
        mapUrl: String(jobData.mapUrl || "").trim(),
        visibility: String(jobData.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC",
        clearnetPublic: jobData.clearnetPublic === true || jobData.clearnetPublic === 'true' || jobData.clearnetPublic === 'on'
      }

      return new Promise((res, rej) => ssbClient.publish(content, (e, m) => {
        if (e) return rej(e)
        if (m && m.key && tribeCrypto) {
          const key = tribeCrypto.generateTribeKey()
          tribeCrypto.setKey(m.key, key, 1)
        }
        res(m)
      }))
    },

    async resolveCurrentId(jobId) {
      const ssbClient = await openSsb()
      const messages = await readAll(ssbClient)
      const { tomb, child } = buildIndex(messages, ssbClient)

      let cur = jobId
      while (child.has(cur)) cur = child.get(cur)
      if (tomb.has(cur)) throw new Error("Job not found")
      return cur
    },

    async resolveRootId(jobId) {
      const ssbClient = await openSsb()
      const messages = await readAll(ssbClient)
      const { tomb, parent, child } = buildIndex(messages, ssbClient)

      let tip = jobId
      while (child.has(tip)) tip = child.get(tip)
      if (tomb.has(tip)) throw new Error("Job not found")

      let root = tip
      while (parent.has(root)) root = parent.get(root)
      return root
    },

    async updateJob(id, jobData) {
      const ssbClient = await openSsb()
      const messages = await readAll(ssbClient)
      const idx = buildIndex(messages, ssbClient)

      const tipId = await this.resolveCurrentId(id)
      const node = idx.jobNodes.get(tipId)
      if (!node || !node.c) throw new Error("Job not found")

      const existingContent = node.c
      const author = existingContent.author
      if (author !== ssbClient.id) throw new Error("Unauthorized")

      const patch = {}

      if (jobData.job_type !== undefined) {
        const jt = String(jobData.job_type || "").toLowerCase()
        if (!["freelancer", "employee", "exchange"].includes(jt)) throw new Error("Invalid job type")
        patch.job_type = jt
      }

      if (jobData.title !== undefined) {
        const t = String(jobData.title || "").trim()
        if (!t) throw new Error("Invalid title")
        patch.title = t
      }

      if (jobData.description !== undefined) {
        const d = String(jobData.description || "").trim()
        if (!d) throw new Error("Invalid description")
        patch.description = d
      }

      if (jobData.requirements !== undefined) patch.requirements = String(jobData.requirements || "")
      if (jobData.languages !== undefined) patch.languages = String(jobData.languages || "")
      if (jobData.tasks !== undefined) patch.tasks = String(jobData.tasks || "")

      if (jobData.job_time !== undefined) {
        const jt = String(jobData.job_time || "").toLowerCase()
        if (!["partial", "complete"].includes(jt)) throw new Error("Invalid job time")
        patch.job_time = jt
      }

      if (jobData.location !== undefined) {
        const loc = String(jobData.location || "").toLowerCase()
        if (!["remote", "presencial"].includes(loc)) throw new Error("Invalid location")
        patch.location = loc
      }

      if (jobData.vacants !== undefined) {
        const v = Math.max(1, toInt(jobData.vacants, 1))
        patch.vacants = v
      }

      if (jobData.salary !== undefined) {
        const s = toNum(jobData.salary)
        if (!Number.isFinite(s) || s < 0) throw new Error("Invalid salary")
        patch.salary = s.toFixed(6)
      }

      if (jobData.hoursOffered !== undefined) {
        const h = toNum(jobData.hoursOffered)
        if (!Number.isFinite(h) || h < 0) throw new Error("Invalid hoursOffered")
        patch.hoursOffered = h
      }
      if (jobData.hoursRequested !== undefined) {
        const h = toNum(jobData.hoursRequested)
        if (!Number.isFinite(h) || h < 0) throw new Error("Invalid hoursRequested")
        patch.hoursRequested = h
      }
      if (jobData.exchangeSkill !== undefined) patch.exchangeSkill = String(jobData.exchangeSkill || "").trim()

      if (jobData.tags !== undefined) patch.tags = normalizeTags(jobData.tags)

      if (jobData.image !== undefined) {
        let blobId = jobData.image
        if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1]
        patch.image = blobId || null
      }

      if (jobData.status !== undefined) {
        const s = String(jobData.status || "").toUpperCase()
        if (!["OPEN", "CLOSED"].includes(s)) throw new Error("Invalid status")
        patch.status = s
      }

      if (jobData.visibility !== undefined) {
        patch.visibility = String(jobData.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC"
      }
      if (jobData.clearnetPublic !== undefined) {
        patch.clearnetPublic = jobData.clearnetPublic === true || jobData.clearnetPublic === 'true' || jobData.clearnetPublic === 'on'
      }
      if (jobData.mapUrl !== undefined) patch.mapUrl = String(jobData.mapUrl || "").trim()

      const next = {
        ...existingContent,
        ...patch,
        author,
        createdAt: existingContent.createdAt,
        updatedAt: new Date().toISOString(),
        replaces: tipId,
        type: "job"
      }

      const tomb = {
        type: "tombstone",
        target: tipId,
        deletedAt: new Date().toISOString(),
        author: ssbClient.id
      }

      await new Promise((res, rej) => ssbClient.publish(tomb, (e) => e ? rej(e) : res()))
      return new Promise((res, rej) => ssbClient.publish(next, (e, m) => e ? rej(e) : res(m)))
    },

    async updateJobStatus(id, status) {
      return this.updateJob(id, { status: String(status || "").toUpperCase() })
    },

    async deleteJob(id) {
      const ssbClient = await openSsb()
      const tipId = await this.resolveCurrentId(id)
      const job = await this.getJobById(tipId)
      if (!job || job.author !== ssbClient.id) throw new Error("Unauthorized")

      const tomb = {
        type: "tombstone",
        target: tipId,
        deletedAt: new Date().toISOString(),
        author: ssbClient.id
      }

      return new Promise((res, rej) => ssbClient.publish(tomb, (e, r) => e ? rej(e) : res(r)))
    },

    async subscribeToJob(id, userId) {
      const ssbClient = await openSsb()
      const me = ssbClient.id
      const uid = userId || me

      const job = await this.getJobById(id)
      if (!job) throw new Error("Job not found")
      if (job.author === uid) throw new Error("Cannot subscribe to your own job")
      if (String(job.status || "").toUpperCase() !== "OPEN") throw new Error("Job is closed")
      if (Array.isArray(job.subscribers) && job.subscribers.includes(uid)) return { alreadySubscribed: true }

      const rootId = job.rootId || (await this.resolveRootId(id))

      const msg = {
        type: "job_sub",
        jobId: rootId,
        value: true,
        createdAt: new Date().toISOString()
      }

      const recps = [me, job.author]
      return new Promise((res, rej) => ssbClient.private.publish(msg, recps, (e, m) => e ? rej(e) : res(m)))
    },

    async unsubscribeFromJob(id, userId) {
      const ssbClient = await openSsb()
      const me = ssbClient.id
      const uid = userId || me

      const job = await this.getJobById(id)
      if (!job) throw new Error("Job not found")
      if (job.author === uid) throw new Error("Cannot unsubscribe from your own job")
      if (Array.isArray(job.subscribers) && !job.subscribers.includes(uid)) return { notSubscribed: true }

      const rootId = job.rootId || (await this.resolveRootId(id))

      const msg = {
        type: "job_sub",
        jobId: rootId,
        value: false,
        createdAt: new Date().toISOString()
      }

      const recps = [me, job.author]
      return new Promise((res, rej) => ssbClient.private.publish(msg, recps, (e, m) => e ? rej(e) : res(m)))
    },

    async listJobs(filter = "ALL", viewerId = null, query = {}) {
      const ssbClient = await openSsb()
      const me = ssbClient.id
      const viewer = viewerId || me

      const messages = await readAll(ssbClient)
      const idx = buildIndex(messages, ssbClient)

      const jobs = []
      for (const [rootId, tipId] of idx.tipByRoot.entries()) {
        if (idx.tomb.has(tipId)) continue
        const node = idx.jobNodes.get(tipId)
        if (!node) continue
        const subsSet = idx.subsByJob.get(rootId) || new Set()
        const subs = Array.from(subsSet)
        const job = buildJobObject(node, rootId, subs)
        if (job.visibility === "HIDDEN" && job.author !== viewer) continue
        jobs.push(job)
      }

      const F = String(filter || "ALL").toUpperCase()
      let list = jobs

      if (F === "MINE") list = list.filter((j) => j.author === viewer)
      else if (F === "REMOTE") list = list.filter((j) => String(j.location || "").toUpperCase() === "REMOTE")
      else if (F === "PRESENCIAL") list = list.filter((j) => String(j.location || "").toUpperCase() === "PRESENCIAL")
      else if (F === "FREELANCER") list = list.filter((j) => String(j.job_type || "").toUpperCase() === "FREELANCER")
      else if (F === "EMPLOYEE") list = list.filter((j) => String(j.job_type || "").toUpperCase() === "EMPLOYEE")
      else if (F === "EXCHANGE") list = list.filter((j) => String(j.job_type || "").toUpperCase() === "EXCHANGE")
      else if (F === "OPEN") list = list.filter((j) => String(j.status || "").toUpperCase() === "OPEN")
      else if (F === "CLOSED") list = list.filter((j) => String(j.status || "").toUpperCase() === "CLOSED")
      else if (F === "RECENT") list = list.filter((j) => moment(j.createdAt).isAfter(moment().subtract(24, "hours")))
      else if (F === "APPLIED") list = list.filter((j) => Array.isArray(j.subscribers) && j.subscribers.includes(viewer))

      const search = String(query.search || query.q || "").trim()
      const minSalary = query.minSalary ?? ""
      const maxSalary = query.maxSalary ?? ""
      const sort = String(query.sort || "").trim()

      if (search) list = list.filter((j) => matchSearch(j, search))

      const minS = toNum(minSalary)
      const maxS = toNum(maxSalary)

      if (Number.isFinite(minS)) list = list.filter((j) => toNum(j.salary) >= minS)
      if (Number.isFinite(maxS)) list = list.filter((j) => toNum(j.salary) <= maxS)

      const byRecent = () => list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
      const bySalary = () => list.sort((a, b) => toNum(b.salary) - toNum(a.salary))
      const bySubscribers = () => list.sort((a, b) => (b.subscribers || []).length - (a.subscribers || []).length)

      if (F === "TOP") bySalary()
      else if (sort === "salary") bySalary()
      else if (sort === "subscribers") bySubscribers()
      else byRecent()

      return list
    },

    async getJobById(id, viewerId = null) {
      const ssbClient = await openSsb()
      const viewer = viewerId || ssbClient.id

      const messages = await readAll(ssbClient)
      const idx = buildIndex(messages, ssbClient)

      let tipId = id
      while (idx.child.has(tipId)) tipId = idx.child.get(tipId)
      if (idx.tomb.has(tipId)) throw new Error("Job not found")

      let rootId = tipId
      while (idx.parent.has(rootId)) rootId = idx.parent.get(rootId)

      const gate = (job) => {
        if (job.visibility === "HIDDEN" && job.author !== viewer) throw new Error("Job not found")
        return job
      }

      const node = idx.jobNodes.get(tipId)
      if (!node) {
        const msg = await new Promise((r, j) => ssbClient.get(tipId, (e, m) => e ? j(e) : r(m)))
        if (!msg || !msg.content) throw new Error("Job not found")
        const tmpNode = { key: tipId, ts: msg.timestamp || 0, c: msg.content, author: msg.author }
        const subsSet = idx.subsByJob.get(rootId) || new Set()
        const subs = Array.from(subsSet)
        return gate(buildJobObject(tmpNode, rootId, subs))
      }

      const subsSet = idx.subsByJob.get(rootId) || new Set()
      const subs = Array.from(subsSet)
      return gate(buildJobObject(node, rootId, subs))
    },

    async getJobTipId(id) {
      return this.resolveCurrentId(id)
    }
  }
}

