import { ensure } from "@workshop/shared/utils"
import { DateTime } from "luxon"

export type TodoJSON = {
  done: boolean
  indent: string
  text: string
  children: TodoJSON[]
}

export const checkboxRegex = /^\s*\[\s*(\w?)\s*\]\s*/

export class Todo {
  done: boolean
  indent = ""

  children: Todo[] = []
  nodes: TodoNode[]

  static parse(line: string) {
    return parseTodo(line)
  }

  constructor(nodes: TodoNode[], done: boolean, indent = "") {
    this.nodes = nodes
    this.done = done
    this.indent = indent
  }

  toString() {
    let string = `${this.indent}[${this.done ? "x" : " "}] ${this.text}`
    this.children.forEach((child) => {
      string += "\n" + child.toString()
    })
    return string
  }

  toJSON(): TodoJSON {
    return {
      done: this.done,
      indent: this.indent,
      text: this.text,
      children: this.children.map((child) => child.toJSON()),
    }
  }

  addChild(todo: Todo) {
    this.children.push(todo)
  }

  get text() {
    return this.nodes
      .filter((node) => node.type !== "indent" && node.type !== "checkbox")
      .map((node) => node.content)
      .join("")
  }

  get timeEstimate() {
    return this.nodes.find((node) => node.type === "time-estimate")?.seconds
  }

  get tags() {
    return this.nodes.filter((node) => node.type === "tag").map((node) => node.content.slice(1))
  }

  get dueDate() {
    const dateNode = this.nodes.find((node) => node.type === "date")
    return dateNode && dateNode.parsed
  }
}

type TodoNode = { content: string } & (
  | { type: "indent" }
  | { type: "checkbox"; checked: boolean }
  | { type: "whitespace" }
  | { type: "text" }
  | { type: "date"; parsed: DateTime }
  | { type: "tag" }
  | { type: "time-estimate"; seconds: number }
  | { type: "url"; url: string }
)

const parseError = (line: string, offset: number, message?: string): string => {
  const errorMessage = message
    ? `Failed to parse node at offset ${offset}`
    : `Failed to parse node at offset ${offset}`
  return `${errorMessage}
${line}
${" ".repeat(offset)}^ error occurred here`
}

const parseTodo = (line: string): Todo | undefined => {
  const todoNodes: TodoNode[] = []

  // Grab the indent at the start of the line
  const indentMatch = line.match(/^\s+/)
  const indent = indentMatch?.[0] ?? ""
  todoNodes.push({ type: "indent", content: indent })
  let offset = indent.length

  // Check for done status
  const match = line.slice(offset).match(checkboxRegex)
  if (!match) return // Invalid format, no status found
  const done = Boolean(match[1])
  todoNodes.push({ type: "checkbox", content: match[0], checked: done })
  offset += match[0].length

  // Parse everything else in the line
  let previousOffset = -1
  while (offset < line.length) {
    ensure(offset !== previousOffset, parseError(line, offset, `Infinite loop detected.`))
    previousOffset = offset

    // eat whitespace
    const whitespaceMatch = line.slice(offset).match(/^\s+/)
    if (whitespaceMatch) {
      const content = whitespaceMatch[0]
      todoNodes.push({ type: "whitespace", content })
      offset += content.length
      continue
    }

    // eat text
    const isTag = line.slice(offset).startsWith("#")
    const isDate = line.slice(offset).startsWith("@")
    const isTimeEstimate = line.slice(offset).match(/^[\d\.]+\w($|\s+)/)
    const isUrl = line.slice(offset).match(/^https?:\/\//)

    let node: TodoNode | undefined
    if (isTag) {
      node = parseTag(line, offset)
    } else if (isDate) {
      node = parseDate(line, offset)
    } else if (isTimeEstimate) {
      node = parseTimeEstimate(line, offset)
    } else if (isUrl) {
      node = parseUrl(line, offset)
    } else {
      node = parseText(line, offset)
    }

    ensure(node, parseError(line, offset))

    todoNodes.push(node)
    offset += node.content.length
  }

  return new Todo(todoNodes, done, indent)
}

const parseTimeEstimate = (line: string, start: number) => {
  const timeMatch = line.slice(start).match(/^([\d\.]+)([mhs])($|\s+)/)
  const [_, numberString, unitString] = timeMatch ?? []
  if (!numberString || !unitString) {
    return parseText(line, start)
  }

  const value = parseFloat(numberString)
  const unit = unitString.toLowerCase()

  let seconds: number
  if (unit === "m") {
    seconds = value * 60
  } else if (unit === "h") {
    seconds = value * 3600
  } else {
    return parseText(line, start)
  }

  const node: TodoNode = { type: "time-estimate", content: timeMatch![0], seconds }
  return node
}

const parseTag = (line: string, start: number) => {
  const tagMatch = line.slice(start).match(/^#(\w+)/)
  const content = tagMatch?.[0]
  if (!content) {
    return parseText(line, start)
  }

  const node: TodoNode = { type: "tag", content }

  return node
}

const parseDate = (line: string, start: number) => {
  const dateMatch = line.slice(start).match(/^@([\d\/\-]{6,})/)
  const content = dateMatch?.[0]

  if (!content) {
    return parseText(line, start)
  }

  let [month, day, year] = content.slice(1).split(/[\/-]/) || []
  if (!month || !day || !year) {
    return parseText(line, start)
  }

  const date = DateTime.fromMillis(Date.parse(content.slice(1)))

  if (date.isValid) {
    const node: TodoNode = { type: "date", content, parsed: date }
    return node
  } else {
    return parseText(line, start)
  }
}

const parseText = (line: string, start: number) => {
  const textMatch = line.slice(start).match(/^\S+/)
  const content = textMatch?.[0]
  if (!content) return

  const node: TodoNode = { type: "text", content }

  return node
}

const parseUrl = (line: string, start: number) => {
  const urlMatch = line.slice(start).match(/^(https?:\/\/[^\s]+)/)
  const content = urlMatch?.[0]
  if (!content) {
    return parseText(line, start)
  }

  const node: TodoNode = { type: "url", content, url: content }
  return node
}
