# blog/models.py
import os
import uuid
import re
import requests
from urllib.parse import urlparse
from django.db import models
from django.utils.text import slugify
from django.urls import reverse

from django.core.files.base import ContentFile


class Post(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True, blank=True)
    date = models.DateField()
    is_public = models.BooleanField(default=True)
    body = models.TextField()  # single big text field

    def get_absolute_url(self):
        return reverse('post_detail', args=[self.slug])

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        # remove trailing and leading \n and \r
        self.body = self.body.strip()

        super().save(*args, **kwargs)
        # After saving the post, parse its body into sections
        self.sections.all().delete()
        self.postreference_set.all().delete()
        self.parse_references()
        self.parse_body_into_sections()

    def parse_references(self):
        # Regex to capture Markdown links with optional title:
        # Pattern groups:
        # 1: inline link text
        # 2: URL
        # 3: optional reference title in quotes
        pattern = re.compile(r'\[([^]]+)\]\((\S+)(?:\s"([^"]+)")?\)')

        for match in pattern.finditer(self.body):
            inline_text = match.group(1).strip()
            url = match.group(2).strip()
            ref_title = match.group(3).strip() if match.group(3) else None

            if not ref_title:
                # Fallback if no title given
                parsed = urlparse(url)
                ref_title = f"Reference from {parsed.netloc}"

            
            # Check if reference with this URL already exists for this post
            if not PostReference.objects.filter(post=self, url=url).exists():
                PostReference.objects.create(
                    post=self,
                    url=url,
                    inline_text=inline_text,
                    reference_title=ref_title
                )

    def parse_body_into_sections(self):
        content = self.body
        image_text_pattern = r':::image_text_(left|right)(.*?):::'
        image_text_blocks = re.finditer(image_text_pattern, content, re.DOTALL)
        tmp_content = []
        placeholders = []
        last_end = 0

        for match in image_text_blocks:
            start, end = match.start(), match.end()
            if start > last_end:
                tmp_content.append(('text', content[last_end:start]))
            block_side = match.group(1)
            block_text = match.group(2).strip()
            placeholders.append(('image_text', block_side, block_text))
            tmp_content.append(('image_text_placeholder', len(placeholders)-1))
            last_end = end

        if last_end < len(content):
            tmp_content.append(('text', content[last_end:]))

        code_fence_pattern = r'```(.*?)```'
        final_sections = []
        for item in tmp_content:
            if item[0] == 'text':
                segment = item[1]
                pos = 0
                for code_match in re.finditer(code_fence_pattern, segment, re.DOTALL):
                    start, end = code_match.start(), code_match.end()
                    if start > pos:
                        final_sections.append(('text', segment[pos:start]))
                    code_block_content = code_match.group(1).strip()

                    code_lines = code_block_content.split('\n')
                    if code_lines:
                        first_line = code_lines[0].strip()
                        if 'executed' in first_line:
                            # Executed code
                            code_lines = code_lines[1:]

                            if '---\r' in code_lines:
                                sep_index = code_lines.index('---\r')
                                code_part = '\n'.join(code_lines[:sep_index]).strip()
                                output_part = '\n'.join(code_lines[sep_index+1:]).strip()
                            else:
                                code_part = '\n'.join(code_lines).strip()
                                output_part = ''
                            final_sections.append(('executed_code', code_part, output_part))
                        else:
                            # Regular code
                            final_sections.append(('code', '\n'.join(code_lines).strip()))
                    else:
                        final_sections.append(('code', ''))
                    pos = end
                if pos < len(segment):
                    final_sections.append(('text', segment[pos:]))
            elif item[0] == 'image_text_placeholder':
                final_sections.append(item)

        image_pattern = r'!\[(.*?)\]\((.*?)\)'
        new_final = []
        for section in final_sections:
            stype = section[0]
            if stype == 'text':
                seg = section[1].strip()
                if not seg:
                    continue
                lines = seg.split('\n')

                text_lines_acc = []
                for line in lines:
                    line = line.strip()
                    if not line:
                        # Blank line (paragraph break)
                        text_lines_acc.append('')
                        continue
                    img_match = re.match(image_pattern, line)
 
                    if img_match:
                        # Flush accumulated text lines as one block
                        if text_lines_acc:
                            big_text_block = '\n'.join(text_lines_acc).strip()
                            if big_text_block:
                                sub_secs = self.split_text_into_subsections(big_text_block)
                                new_final.extend(sub_secs)
                            text_lines_acc = []
                        # Handle image
                        image_url = img_match.group(2)
                        width = None
                        image_desc = None
                        if 'width=' in image_url or 'desc=' in image_url:
                            parts = image_url.strip('"').split()
                            main_url = parts[0]
                            for p in parts[1:]:
                                if p.startswith('width='):
                                    width = int(p.split('=')[1])
                            
                            if 'desc=' in line:
                                image_desc = line.split("desc=")[-1][:-1]
                            
                            image_url = main_url

                        new_final.append(('image', '', image_url, width, image_desc))
                    else:
                        text_lines_acc.append(line)

                # Flush any remaining text lines
                if text_lines_acc:
                    big_text_block = '\n'.join(text_lines_acc).strip()
                    if big_text_block:
                        sub_secs = self.split_text_into_subsections(big_text_block)
                        new_final.extend(sub_secs)

            elif stype == 'code':
                code_content = section[1]
                new_final.append(('code', code_content))

            elif stype == 'executed_code':
                code_part = section[1]
                output_part = section[2]
                new_final.append(('executed_code', code_part, output_part))

            elif stype == 'image_text_placeholder':
                index = section[1]
                it_type, side, block_text = placeholders[index]
                lines = block_text.split('\n')
                lines = [l.strip() for l in lines if l.strip()]
                image_line = lines[0] if lines else ''
                img_match = re.match(image_pattern, image_line) if image_line else None
                image_url = None
                width = None
                image_desc = None
                if img_match:
                    image_url = img_match.group(2)
                    if 'width=' in image_url or 'desc=' in image_url:
                        parts = image_url.strip('"').split()
                        main_url = parts[0]
                        for p in parts[1:]:
                            if p.startswith('width='):
                                width = int(p.split('=')[1])

                        if 'desc=' in line:
                            image_desc =  line.split("desc=")[-1][:-1]

                        image_url = main_url
                    text_part = '\n'.join(lines[1:])
                else:
                    text_part = '\n'.join(lines)

                if text_part.strip():
                    sub_secs = self.split_text_into_subsections(text_part.strip())
                    # Combine them into one body if multiple
                    combined_body = '\n'.join([sec[1] for sec in sub_secs if sec[0] == 'text' and len(sec) < 3])
                    # If a heading appears in image_text block, handle appropriately.
                    # For simplicity, assume no headings in image_text. If headings appear, you'd need logic here.
                else:
                    combined_body = ''

                new_final.append(('image_text', side, combined_body, image_url, width, image_desc))

        # Now create PostSections from new_final
        order = 1
        for section in new_final:
            stype = section[0]
            if stype == 'text':
                # A normal text paragraph from split_text_into_subsections is ('text', 'body')
                # If a heading was detected, it's ('text', 'heading text', 'heading')
                if len(section) == 3 and section[2] == 'heading':
                    heading_text = section[1]
                    self.sections.create(
                        section_type='text',
                        order=order,
                        heading=heading_text,
                        body=None
                    )
                else:
                    body = section[1]
                    self.sections.create(
                        section_type='text',
                        order=order,
                        body=body
                    )
                order += 1

            elif stype == 'code':
                code_content = section[1]
                self.sections.create(
                    section_type='code',
                    order=order,
                    body=f'<pre><code>{code_content}</code></pre>'
                )
                order += 1

            elif stype == 'executed_code':
                code_part = section[1]
                output_part = section[2]
                s = self.sections.create(
                    section_type='executed_code',
                    order=order,
                    body=code_part,
                    code_output=output_part
                )
                order += 1

            elif stype == 'image':
                # ('image', '', image_url, width)
                image_url = section[2]
                width = section[3]
                image_desc = section[4]
                s = self.sections.create(
                    section_type='image',
                    order=order,
                    body=''
                )
                if width:
                    s.image_width = width
                s.body = image_url
                if image_desc:
                    s.image_description = image_desc
                s.save(update_fields=['body', 'image_width', 'image_description'])
                order += 1

            elif stype == 'image_text':
                # ('image_text', side, combined_body, image_url, width)
                side = section[1]
                combined_body = section[2]
                image_url = section[3]
                width = section[4]
                image_desc = section[5] if len(section) > 5 else None
                s = self.sections.create(
                    section_type='image_text',
                    order=order,
                    image_position='left' if side == 'left' else 'right',
                    body=f"IMAGE:{image_url}\n{combined_body}" if image_url else combined_body
                )
                if width:
                    s.image_width = width
                if image_desc:
                    s.image_description = image_desc
                s.save(update_fields=['body', 'image_width', 'image_description'])
                order += 1

    def handle_bullets(self, text):
        lines = text.split('\n')
        final_lines = []
        in_list = False
        for line in lines:
            if line.strip().startswith('- '):
                content = line.strip()[2:].strip()
                if not in_list:
                    final_lines.append('<ul class="list-disc text-gray-700 leading-relaxed list-inside text-xl p-2 pb-4 pt-4">')
                    in_list = True
                final_lines.append(f'<li>{content}</li>')
            else:
                if in_list:
                    final_lines.append('</ul>')
                    in_list = False
                final_lines.append(line)
        if in_list:
            final_lines.append('</ul>')
        return '\n'.join(final_lines)

    def handle_inline_formatting(self, text):
        # Bold: **text**
        text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text, flags=re.DOTALL)
        # Italic: *text*
        text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text, flags=re.DOTALL)
        return text

    def split_text_into_subsections(self, text_block):
        """
        Splits a large text block into subsections:
        - Handles bullets and inline formatting
        - Separates by blank lines into paragraphs
        - Detects headings at start of lines (#, ##, etc.)
        Returns a list of tuples like:
        ('text', 'paragraph text')
        ('text', 'heading text', 'heading')
        """
        # Apply bullets and inline formatting first
        text_block = self.handle_bullets(text_block)
        text_block = self.handle_inline_formatting(text_block)

        paragraphs = re.split(r'\n\s*\n', text_block.strip())
        subsections = []
        heading_pattern = re.compile(r'^(#{1,6})\s+(.*)')
        for para in paragraphs:
            p_lines = para.split('\n')
            temp_para_lines = []
            for line in p_lines:
                line = line.strip()
                if not line:
                    continue
                heading_match = heading_pattern.match(line)
                if heading_match:
                    # Flush paragraph lines first
                    if temp_para_lines:
                        subsection_text = '\n'.join(temp_para_lines).strip()
                        if subsection_text:
                            subsections.append(('text', subsection_text))
                        temp_para_lines = []
                    # This line is a heading
                    heading_text = heading_match.group(2)
                    subsections.append(('text', heading_text, 'heading'))
                else:
                    temp_para_lines.append(line)
            # Flush paragraph if any left
            if temp_para_lines:
                subsection_text = '\n'.join(temp_para_lines).strip()
                if subsection_text:
                    subsections.append(('text', subsection_text))
                temp_para_lines = []
        return subsections


class PostSection(models.Model):
    SECTION_TYPES = [
        ('text', 'Text'),
        ('code', 'Code'),
        ('image', 'Image'),
        ('image_text', 'Image + Text'),
    ]

    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='sections')
    section_type = models.CharField(max_length=20, choices=SECTION_TYPES)
    order = models.PositiveIntegerField(default=1)
    heading = models.CharField(max_length=255, blank=True, null=True)
    body = models.TextField(blank=True, null=True)
    image = models.ImageField(upload_to='post_images/', blank=True, null=True)
    image_position = models.CharField(
        max_length=5,
        choices=[('left', 'Left'), ('right', 'Right')],
        blank=True, null=True
    )
    image_width = models.IntegerField(blank=True, null=True)  # Field for width
    code_output = models.TextField(blank=True, null=True)  # For executed code output
    image_description = models.TextField(blank=True, null=True)  

    class Meta:
        ordering = ['order']

    def __str__(self):
        return f"{self.post.title} - {self.section_type} section #{self.order}"

    def save(self, *args, **kwargs):
        is_downloading = kwargs.pop('downloading_image', False)
        super().save(*args, **kwargs)

        # Only attempt download if:
        # - section_type is 'image'
        # - we are not currently downloading (to avoid recursion)
        # - image field is empty (not already set)
        # - body has the URL
        if self.section_type in ['image', 'image_text'] and not is_downloading and not self.image and self.body:
            self.download_and_replace_images()

    def download_and_replace_images(self):
        # Here we assume self.body contains the image URL.
        if not self.body:
            return

        image_url = self.body.strip()

        # find the link in the body of the post self.post.body and
        # where style is ![alt text](image_url "width=500")
        # Group 1: alt text
        # Group 2: url
        # Group 3: optional title string
        # and see if there is optional title string with width in it,
        # if so extract the width parameter, else extract rest

        # Pattern to match markdown image syntax with optional title
        if 'IMAGE:' in image_url:
            image_url = image_url.split("IMAGE:")[1].split('\n')[0]
            section_type = 'image_text'
            body = '\n'.join(self.body.split("\n")[1:])
        else:
            section_type = 'image'
            body = ''

        pattern = re.compile(r'!\[(.*?)\]\((\S+)(?:\s+"([^"]+)")?\)')
        
        # Search for the image URL in the post body
        for match in pattern.finditer(self.post.body):
            alt_text = match.group(1)
            url = match.group(2)
            title = match.group(3)
            
            # If this is the URL we're looking for
            if url.strip() == image_url.strip():
                # Extract width if present in title
                if title:
                    self.image_width = self.extract_width_from_title(title)
                break

        if not image_url:
            return

        local_file_name = self.download_image(image_url)
        if local_file_name:
            # Set section_type to 'image', clear the body if you want
            self.section_type = section_type
            # Clear the body since we now have a local image
            self.body = body
            # Save again with the downloading_image flag to prevent recursion
            self.save(downloading_image=True)

    def extract_width_from_title(self, title_str):
        # Look for width=XXX within the title string
        # For example: "Some title width=500"
        width_pattern = re.compile(r'width=(\d+)')
        wmatch = width_pattern.search(title_str)
        if wmatch:
            return int(wmatch.group(1))
        return None

    def download_image(self, url):
        try:
            response = requests.get(url, timeout=10)
            response.raise_for_status()
        except requests.RequestException:
            return None

        parsed_url = urlparse(url)
        filename = os.path.basename(parsed_url.path)
        if not filename:
            filename = 'image.jpg'  # fallback

        # If you want a different filename, you can manipulate here.
        # Just ensure you don't cause infinite loops by repeatedly changing the filename.

        image_content = ContentFile(response.content)
        # save=False to avoid infinite recursion here, since we call self.save() later manually
        self.image.save(filename, image_content, save=False)
        return self.image.name


class PostReference(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    url = models.URLField()
    inline_text = models.CharField(max_length=255)
    reference_title = models.CharField(max_length=500)

    def __str__(self):
        return f"{self.reference_title} ({self.url})"


class PostBodyParser:
    def __init__(self, text):
        self.text = text
    
    def parse(self):
        # Steps:
        # 1. Identify code blocks (regular and executed)
        # 2. Identify bullet lists
        # 3. Apply inline formatting (bold/italic)
        # 4. Return a list of section dicts

        sections = []
        # Break text into lines
        lines = self.text.split('\n')
        sections.extend(self.parse_blocks(lines))
        return sections

    def parse_blocks(self, lines):
        """
        Parse the entire body into logical sections.
        We'll have:
        - Executed code blocks: ```python executed ...``` until ```
          with `---` line to separate code and output
        - Regular code blocks: ```python``` until ```
        - Bullet lists: lines starting with '- '
        - Bold/Italics inline: transform after building text sections
        """
        sections = []
        i = 0
        current_text = []
        while i < len(lines):
            line = lines[i]
            # Detect code blocks
            if line.strip().startswith("```"):
                # Check for executed code
                lang_line = line.strip().lstrip('```').strip()
                # e.g. "python executed"
                parts = lang_line.split()
                lang = parts[0] if parts else ''
                executed = 'executed' in parts

                # Collect code lines until next ```
                code_lines = []
                i += 1
                while i < len(lines) and not lines[i].strip().startswith("```"):
                    code_lines.append(lines[i])
                    i += 1

                # consume closing ```
                i += 1

                if executed:
                    # Split by --- line
                    if '---' in code_lines:
                        idx = code_lines.index('---')
                        code_part = code_lines[:idx]
                        output_part = code_lines[idx+1:]
                    else:
                        code_part = code_lines
                        output_part = []

                    code_str = '\n'.join(code_part)
                    output_str = '\n'.join(output_part)

                    # Before adding code section, flush current_text as a text section if needed
                    if current_text:
                        text_section = self.create_text_section('\n'.join(current_text))
                        sections.append(text_section)
                        current_text = []

                    sections.append({
                        'type': 'executed_code',
                        'body': code_str,
                        'code_output': output_str,
                    })
                else:
                    # Regular code block
                    code_str = '\n'.join(code_lines)
                    if current_text:
                        text_section = self.create_text_section('\n'.join(current_text))
                        sections.append(text_section)
                        current_text = []

                    sections.append({
                        'type': 'code',
                        'body': code_str
                    })

            else:
                # Just a normal line, accumulate in current_text
                current_text.append(line)
                i += 1

        # After loop, flush any remaining text lines
        if current_text:
            sections.append(self.create_text_section('\n'.join(current_text)))

        return sections

    def create_text_section(self, text):
        # Handle bullet lists
        text = self.handle_bullets(text)
        # Handle inline formatting (bold, italic)
        text = self.handle_inline_formatting(text)
        return {'type': 'text', 'body': text}

    def handle_bullets(self, text):
        # Convert lines starting with '- ' into <li>...</li>
        # Group consecutive bullet lines into <ul>
        lines = text.split('\n')
        final_lines = []
        in_list = False
        for line in lines:
            if line.strip().startswith('- '):
                content = line.strip()[2:].strip()  # remove '- '
                if not in_list:
                    final_lines.append('<ul class="list-disc text-gray-700 leading-relaxed list-inside text-xl p-2 pb-4 pt-4">')
                    in_list = True
                final_lines.append(f'<li>{content}</li>')
            else:
                if in_list:
                    final_lines.append('</ul>')
                    in_list = False
                final_lines.append(line)
        if in_list:
            final_lines.append('</ul>')
        return '\n'.join(final_lines)

    def handle_inline_formatting(self, text):
        # Bold: **text**
        text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text, flags=re.DOTALL)
        # Italic: *text*
        text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text, flags=re.DOTALL)
        return text
