package cl.homelogic.platform.designsystem.components.atoms

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Shapes
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.flavours.gfm.GFMElementTypes
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.parser.MarkdownParser

@Immutable
data class MarkdownTypography(
    val text: TextStyle,
    val code: TextStyle,
    val inlineCode: TextStyle,
    val h1: TextStyle,
    val h2: TextStyle,
    val h3: TextStyle,
    val h4: TextStyle,
    val h5: TextStyle,
    val h6: TextStyle,
    val quote: TextStyle,
    val paragraph: TextStyle,
    val ordered: TextStyle,
    val bullet: TextStyle,
    val list: TextStyle,
    val textLink: TextLinkStates,
    val table: TextStyle,
)

@Immutable
data class MarkdownColors(
    val codeBackground: Color,
    val codeText: Color,
    val dividerColor: Color,
    val inlineCodeBackground: Color,
    val inlineCodeText: Color,
    val linkText: Color,
    val tableBackground: Color,
    val tableText: Color,
    val text: Color,
)

@Immutable
data class TextLinkStates(
    val normal: TextStyle,
    val visited: TextStyle,
)

@Immutable
private data class MarkdownContentData(
    val node: ASTNode,
    val content: String,
    val colors: MarkdownColors,
    val typography: MarkdownTypography,
    val level: Int = 0,
)

@Composable
fun Markdown(
    modifier: Modifier = Modifier,
    text: String,
    colors: MarkdownColors = rememberDefaultMarkdownColors(),
    typography: MarkdownTypography = rememberDefaultMarkdownTypography(),
) {
    val flavour = remember { GFMFlavourDescriptor() }
    val parser = remember { MarkdownParser(flavour) }

    val markdownTree = remember(text) { parser.buildMarkdownTreeFromString(text) }

    val markdownContent = remember(markdownTree, text, colors, typography) {
        MarkdownContentData(markdownTree, text, colors, typography)
    }

    val annotatedStringCache = remember { mutableStateMapOf<String, AnnotatedString>() }

    val listState = rememberLazyListState()

    SelectionContainer {
        Column {
            OptimizedMarkdownContent(
                data = markdownContent,
                stringCache = annotatedStringCache,
            )
        }
    }
}

@Composable
private fun rememberDefaultMarkdownColors(): MarkdownColors {
    val colorScheme = MaterialTheme.colorScheme
    return remember {
        MarkdownColors(
            codeBackground = colorScheme.surfaceVariant,
            codeText = colorScheme.onSurfaceVariant,
            dividerColor = colorScheme.outline,
            inlineCodeBackground = colorScheme.surfaceVariant,
            inlineCodeText = colorScheme.onSurfaceVariant,
            linkText = colorScheme.tertiary,
            tableBackground = colorScheme.surface,
            tableText = colorScheme.onSurface,
            text = colorScheme.onPrimary,
        )
    }
}

@Composable
private fun rememberDefaultMarkdownTypography(): MarkdownTypography {
    val materialTypography = MaterialTheme.typography
    return remember {
        MarkdownTypography(
            bullet = materialTypography.bodySmall,
            code = materialTypography.bodyMedium,
            h1 = materialTypography.headlineLarge,
            h2 = materialTypography.headlineMedium,
            h3 = materialTypography.headlineSmall,
            h4 = materialTypography.titleLarge,
            h5 = materialTypography.titleMedium,
            h6 = materialTypography.bodyLarge,
            inlineCode = materialTypography.bodyLarge,
            list = materialTypography.bodyMedium,
            ordered = materialTypography.bodyMedium,
            paragraph = materialTypography.bodyMedium,
            quote = materialTypography.bodyMedium.copy(
                fontStyle = FontStyle.Italic,
            ),
            table = materialTypography.bodySmall,
            text = materialTypography.bodyMedium,
            textLink = TextLinkStates(
                normal = materialTypography.bodyMedium.copy(
                    textDecoration = TextDecoration.Underline,
                ),
                visited = materialTypography.bodyMedium.copy(
                    textDecoration = TextDecoration.Underline,
                ),
            ),
        )
    }
}

@Composable
private fun OptimizedMarkdownContent(
    data: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
    maxDepth: Int = 10,
) {
    if (data.level > maxDepth) return

    for (child in data.node.children) {
        val nodeKey = "${child.hashCode()}-${data.level}"
        val childData = remember(child, data) {
            data.copy(node = child, level = data.level + 1)
        }

        when (child.type) {
            MarkdownElementTypes.PARAGRAPH -> {
                OptimizedParagraph(stringCache, nodeKey, child, data)
            }

            MarkdownElementTypes.ATX_1,
            MarkdownElementTypes.ATX_2,
            MarkdownElementTypes.ATX_3,
            MarkdownElementTypes.ATX_4,
            MarkdownElementTypes.ATX_5,
            MarkdownElementTypes.ATX_6,
            -> {
                OptimizedHeading(
                    childData = childData,
                    nodeKey = nodeKey,
                    stringCache = stringCache,
                )
            }

            MarkdownElementTypes.UNORDERED_LIST -> {
                OptimizedUnorderedList(
                    childData = childData,
                    stringCache = stringCache,
                )
            }

            MarkdownElementTypes.ORDERED_LIST -> {
                OptimizedOrderedList(
                    childData = childData,
                    stringCache = stringCache,
                )
            }

            MarkdownElementTypes.BLOCK_QUOTE -> {
                OptimizedBlockQuote(
                    childData = childData,
                    stringCache = stringCache,
                )
            }

            MarkdownElementTypes.CODE_FENCE,
            MarkdownElementTypes.CODE_BLOCK,
            -> {
                OptimizedCodeBlock(childData = childData)
            }

            MarkdownElementTypes.HTML_BLOCK -> {
                OptimizedHtmlBlock(childData = childData)
            }

            GFMElementTypes.TABLE -> {
                OptimizedTable(
                    childData = childData,
                    stringCache = stringCache,
                )
            }
        }
    }
}

@Composable
private fun OptimizedParagraph(
    stringCache: MutableMap<String, AnnotatedString>,
    nodeKey: String,
    child: ASTNode,
    data: MarkdownContentData,
) {
    val paragraphModifier = remember {
        Modifier.padding(
            vertical = Padding.small,
            horizontal = Padding.tiny,
        )
    }

    val annotatedString = stringCache.getOrPut(nodeKey) {
        buildAnnotatedString {
            renderInlineContent(child, data.content, data.colors, data.typography)
        }
    }

    Text(
        text = annotatedString,
        style = data.typography.paragraph,
        color = data.colors.text,
        modifier = paragraphModifier,
    )
}

@Composable
private fun OptimizedHeading(
    childData: MarkdownContentData,
    nodeKey: String,
    stringCache: MutableMap<String, AnnotatedString>,
) {
    val (style, topPadding, bottomPadding) = remember(childData.node.type) {
        when (childData.node.type) {
            MarkdownElementTypes.ATX_1 -> Triple(
                childData.typography.h1,
                Padding.regular,
                Padding.small,
            )

            MarkdownElementTypes.ATX_2 -> Triple(
                childData.typography.h2,
                Padding.small,
                Padding.small,
            )

            MarkdownElementTypes.ATX_3 -> Triple(
                childData.typography.h3,
                Padding.small,
                Padding.small,
            )

            MarkdownElementTypes.ATX_4 -> Triple(
                childData.typography.h4,
                Padding.small,
                Padding.tiny,
            )

            MarkdownElementTypes.ATX_5 -> Triple(
                childData.typography.h5,
                Padding.small,
                Padding.tiny,
            )

            MarkdownElementTypes.ATX_6 -> Triple(
                childData.typography.h6,
                Padding.small,
                Padding.tiny,
            )

            else -> Triple(
                childData.typography.text,
                Padding.small,
                Padding.small,
            )
        }
    }

    val headingModifier = remember(topPadding, bottomPadding) {
        Modifier.padding(top = topPadding, bottom = bottomPadding)
    }

    val headingText = stringCache.getOrPut(nodeKey) {
        buildAnnotatedString {
            for (headerChild in childData.node.children) {
                if (headerChild.type != MarkdownTokenTypes.ATX_HEADER &&
                    headerChild.type != MarkdownTokenTypes.EOL
                ) {
                    renderInlineContent(
                        headerChild,
                        childData.content,
                        childData.colors,
                        childData.typography,
                    )
                }
            }
        }
    }

    Text(
        text = headingText,
        style = style,
        color = childData.colors.text,
        modifier = headingModifier,
    )
}

@Composable
private fun OptimizedUnorderedList(
    childData: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
) {
    val listModifier = remember {
        Modifier
            .padding(vertical = Padding.small)
            .padding(start = Padding.regular)
    }

    Column(modifier = listModifier) {
        for (item in childData.node.children) {
            if (item.type == MarkdownElementTypes.LIST_ITEM) {
                val itemData = remember(item, childData) {
                    childData.copy(node = item)
                }
                OptimizedListItem(
                    itemData = itemData,
                    stringCache = stringCache,
                    isOrdered = false,
                    index = 0,
                )
            }
        }
    }
}

@Composable
private fun OptimizedOrderedList(
    childData: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
) {
    val listModifier = remember {
        Modifier
            .padding(vertical = Padding.small)
            .padding(start = Padding.regular)
    }

    Column(modifier = listModifier) {
        childData.node.children.forEachIndexed { index, item ->
            if (item.type == MarkdownElementTypes.LIST_ITEM) {
                val itemData = remember(item, childData) {
                    childData.copy(node = item)
                }
                OptimizedListItem(
                    itemData = itemData,
                    stringCache = stringCache,
                    isOrdered = true,
                    index = index + 1,
                )
            }
        }
    }
}

@Composable
private fun OptimizedListItem(
    itemData: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
    isOrdered: Boolean,
    index: Int,
) {
    val itemModifier = remember { Modifier.padding(vertical = Padding.tiny) }

    Row(
        modifier = itemModifier,
        verticalAlignment = Alignment.Top,
    ) {
        Text(
            text = if (isOrdered) "$index. " else "• ",
            style = if (isOrdered) itemData.typography.ordered else itemData.typography.bullet,
            color = itemData.colors.text,
        )

        Column {
            for (itemChild in itemData.node.children) {
                val nodeKey = "${itemChild.hashCode()}-${itemData.level}"

                when (itemChild.type) {
                    MarkdownElementTypes.PARAGRAPH -> {
                        val text = stringCache.getOrPut(nodeKey) {
                            buildAnnotatedString {
                                renderInlineContent(
                                    itemChild,
                                    itemData.content,
                                    itemData.colors,
                                    itemData.typography,
                                )
                            }
                        }

                        Text(
                            text = text,
                            style = itemData.typography.list,
                            color = itemData.colors.text,
                        )
                    }

                    MarkdownElementTypes.UNORDERED_LIST,
                    MarkdownElementTypes.ORDERED_LIST,
                    -> {
                        val childData = remember(itemChild, itemData) {
                            itemData.copy(node = itemChild)
                        }
                        OptimizedMarkdownContent(
                            data = childData,
                            stringCache = stringCache,
                        )
                    }
                }
            }
        }
    }
}

@Composable
private fun OptimizedBlockQuote(
    childData: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
) {
    val blockQuoteModifier = remember { Modifier.padding(vertical = Padding.small) }
    val dividerModifier = remember {
        Modifier
            .width(Padding.tiny)
            .wrapContentHeight()
            .background(childData.colors.dividerColor)
    }
    val contentModifier = remember { Modifier.padding(start = Padding.regular) }

    Row(
        modifier = blockQuoteModifier,
        verticalAlignment = Alignment.Top,
    ) {
        Box(modifier = dividerModifier)

        Column(modifier = contentModifier) {
            for (quoteChild in childData.node.children) {
                val nodeKey = "${quoteChild.hashCode()}-${childData.level}"

                if (quoteChild.type == MarkdownElementTypes.PARAGRAPH) {
                    val text = stringCache.getOrPut(nodeKey) {
                        buildAnnotatedString {
                            renderInlineContent(
                                quoteChild,
                                childData.content,
                                childData.colors,
                                childData.typography,
                            )
                        }
                    }

                    Text(
                        text = text,
                        style = childData.typography.quote,
                        color = childData.colors.text,
                    )
                } else {
                    val innerChildData = remember(quoteChild, childData) {
                        childData.copy(node = quoteChild)
                    }
                    OptimizedMarkdownContent(
                        data = innerChildData,
                        stringCache = stringCache,
                    )
                }
            }
        }
    }
}

@Composable
private fun OptimizedCodeBlock(childData: MarkdownContentData) {
    val codeContent = remember(childData.node, childData.content) {
        when (childData.node.type) {
            MarkdownElementTypes.CODE_FENCE -> {
                childData.node.children
                    .firstOrNull { it.type == MarkdownTokenTypes.CODE_FENCE_CONTENT }
                    ?.getTextInNode(childData.content)
                    ?.toString() ?: ""
            }

            MarkdownElementTypes.CODE_BLOCK -> {
                childData.node.children
                    .firstOrNull { it.type == MarkdownTokenTypes.CODE_LINE }
                    ?.getTextInNode(childData.content)
                    ?.toString() ?: ""
            }

            else -> ""
        }
    }

    val codeBlockModifier = remember {
        Modifier
            .clip(Shapes.RoundCorner.regular)
            .background(childData.colors.codeBackground)
            .fillMaxWidth()
            .padding(vertical = Padding.small)
    }

    val textModifier = remember { Modifier.padding(Padding.regular) }

    Surface(
        modifier = codeBlockModifier,
        color = childData.colors.codeBackground,
    ) {
        Text(
            text = codeContent,
            style = childData.typography.code,
            color = childData.colors.codeText,
            modifier = textModifier,
        )
    }
}

@Composable
private fun OptimizedHtmlBlock(childData: MarkdownContentData) {
    val htmlContent = remember(childData.node, childData.content) {
        childData.node.getTextInNode(childData.content).toString()
    }

    val htmlBlockModifier = remember { Modifier.padding(vertical = Padding.small) }

    Text(
        text = htmlContent,
        style = childData.typography.text,
        color = childData.colors.text,
        modifier = htmlBlockModifier,
    )
}

@Composable
private fun OptimizedTable(
    childData: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
) {
    val tableModifier = remember {
        Modifier
            .fillMaxWidth()
            .padding(vertical = Padding.small)
    }

    val tableContentModifier = remember { Modifier.padding(Padding.small) }

    Surface(
        color = childData.colors.tableBackground,
        modifier = tableModifier,
    ) {
        Column(modifier = tableContentModifier) {
            for (row in childData.node.children) {
                if (row.type == GFMElementTypes.HEADER ||
                    row.type == GFMElementTypes.ROW
                ) {
                    val rowData = remember(row, childData) {
                        childData.copy(node = row)
                    }
                    OptimizedTableRow(
                        rowData = rowData,
                        stringCache = stringCache,
                    )

                    if (row.type == GFMElementTypes.HEADER) {
                        Divider(modifier = Modifier.padding(bottom = Padding.small))
                    }
                }
            }
        }
    }
}

@Composable
private fun OptimizedTableRow(
    rowData: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
) {
    val rowModifier = remember { Modifier.fillMaxWidth() }

    val cellNodes = remember(rowData.node) {
        rowData.node.children.filter {
            it.type != GFMElementTypes.HEADER &&
                it.type != GFMElementTypes.ROW &&
                it.type != GFMElementTypes.TABLE
        }
    }

    Row(
        modifier = rowModifier,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        cellNodes.forEach { cellNode ->
            val cellData = remember(cellNode, rowData) {
                rowData.copy(node = cellNode)
            }

            val isHeader = remember(rowData.node.type) {
                rowData.node.type == GFMElementTypes.HEADER
            }

            OptimizedTableCell(
                modifier = Modifier.weight(1f),
                cellData = cellData,
                stringCache = stringCache,
                isHeader = isHeader,
            )
        }
    }
}

@Composable
private fun OptimizedTableCell(
    modifier: Modifier = Modifier,
    cellData: MarkdownContentData,
    stringCache: MutableMap<String, AnnotatedString>,
    isHeader: Boolean,
) {
    val nodeKey = "${cellData.node.hashCode()}-${cellData.level}"
    val cellModifier = remember { modifier.padding(Padding.tiny) }

    val cellText = stringCache.getOrPut(nodeKey) {
        buildAnnotatedString {
            renderInlineContent(
                cellData.node,
                cellData.content,
                cellData.colors,
                cellData.typography,
            )
        }
    }

    val style = remember(isHeader) {
        if (isHeader) cellData.typography.h4 else cellData.typography.table
    }

    val fontWeight = remember(isHeader) {
        if (isHeader) FontWeight.Bold else FontWeight.Normal
    }

    Text(
        text = cellText,
        style = style,
        color = cellData.colors.tableText,
        fontWeight = fontWeight,
        modifier = cellModifier,
    )
}

private fun AnnotatedString.Builder.renderInlineContent(
    node: ASTNode,
    content: String,
    colors: MarkdownColors,
    typography: MarkdownTypography,
) {
    for (child in node.children) {
        when (child.type) {
            MarkdownTokenTypes.TEXT, MarkdownTokenTypes.WHITE_SPACE -> {
                append(child.getTextInNode(content))
            }

            MarkdownElementTypes.EMPH -> {
                renderEmphasis(child, content, colors, typography)
            }

            MarkdownElementTypes.STRONG -> {
                renderStrong(child, content, colors, typography)
            }

            MarkdownElementTypes.CODE_SPAN -> {
                renderCodeSpan(child, content, colors)
            }

            MarkdownElementTypes.INLINE_LINK -> {
                renderInlineLink(child, content, colors, typography)
            }

            MarkdownElementTypes.FULL_REFERENCE_LINK,
            MarkdownElementTypes.SHORT_REFERENCE_LINK,
            -> {
                renderReferenceLink(child, content, colors, typography)
            }

            MarkdownTokenTypes.HARD_LINE_BREAK -> {
                append("\n")
            }

            else -> {
                renderInlineContent(child, content, colors, typography)
            }
        }
    }
}

private fun AnnotatedString.Builder.renderEmphasis(
    node: ASTNode,
    content: String,
    colors: MarkdownColors,
    typography: MarkdownTypography,
) {
    pushStyle(SpanStyle(fontStyle = FontStyle.Italic))
    renderInlineContent(node, content, colors, typography)
    pop()
}

private fun AnnotatedString.Builder.renderStrong(
    node: ASTNode,
    content: String,
    colors: MarkdownColors,
    typography: MarkdownTypography,
) {
    pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
    renderInlineContent(node, content, colors, typography)
    pop()
}

private fun AnnotatedString.Builder.renderCodeSpan(
    node: ASTNode,
    content: String,
    colors: MarkdownColors,
) {
    pushStyle(
        SpanStyle(
            background = colors.inlineCodeBackground,
            color = colors.inlineCodeText,
            fontFamily = FontFamily.Monospace,
        ),
    )
    append("${node.getTextInNode(content).trim('`')}")
    pop()
}

private fun AnnotatedString.Builder.renderInlineLink(
    node: ASTNode,
    content: String,
    colors: MarkdownColors,
    typography: MarkdownTypography,
) {
    val linkText = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT }
    val linkDestination =
        node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_DESTINATION }
    val url = linkDestination?.getTextInNode(content)?.trim('(', ')')?.toString() ?: ""

    val startPosition = length

    pushStyle(SpanStyle(color = colors.linkText))

    if (linkText != null) {
        val linkTextContent = linkText.getTextInNode(content)
        if (linkTextContent.isNotEmpty()) {
            append(linkTextContent)
        } else {
            for (textChild in linkText.children) {
                if (textChild.type != MarkdownTokenTypes.LBRACKET &&
                    textChild.type != MarkdownTokenTypes.RBRACKET
                ) {
                    renderInlineContent(textChild, content, colors, typography)
                }
            }
        }
    } else {
        append(url)
    }

    if (length > startPosition) {
        addLink(
            url = LinkAnnotation.Url(url = url),
            start = startPosition,
            end = length,
        )
    }

    pop()
}

private fun AnnotatedString.Builder.renderReferenceLink(
    node: ASTNode,
    content: String,
    colors: MarkdownColors,
    typography: MarkdownTypography,
) {
    val linkText = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT }

    pushStyle(
        SpanStyle(
            color = colors.linkText,
            textDecoration = TextDecoration.Underline,
        ),
    )

    if (linkText != null) {
        for (textChild in linkText.children) {
            if (textChild.type != MarkdownTokenTypes.LBRACKET &&
                textChild.type != MarkdownTokenTypes.RBRACKET
            ) {
                renderInlineContent(textChild, content, colors, typography)
            }
        }
    }

    pop()
}
