import base64
import io
import json
import logging
import requests
from odoo import api, fields, models, _
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

try:
    from PIL import Image
    HAS_PILLOW = True
except ImportError:
    HAS_PILLOW = False
    _logger.warning("Pillow not installed. Image optimization features will be disabled.")


class OtkSeoContentImage(models.Model):
    _name = 'otk.seo.content.image'
    _description = 'SEO Content Generated Image'
    _order = 'sequence, id'

    content_id = fields.Many2one('otk.seo.content', 'Content',
        required=True, ondelete='cascade', index=True,
        help='The SEO content record this image belongs to.')

    sequence = fields.Integer('Sequence', default=10,
        help='Order in which images appear. Lower numbers appear first.')
    name = fields.Char('Image Name',
        help='A descriptive name for this image. Auto-generated from the prompt if not provided.')

    # === Generation Settings ===
    prompt = fields.Text('Generation Prompt', required=True,
        help='The text description sent to the AI to generate this image. Be specific about style, subject, composition, and mood.')
    negative_prompt = fields.Text('Negative Prompt',
        default='text, watermark, low quality, blurry, distorted',
        help='Elements to avoid in the generated image. The AI will try not to include these aspects.')

    style = fields.Selection([
        ('photorealistic', 'Photorealistic'),
        ('illustration', 'Illustration'),
        ('digital_art', 'Digital Art'),
        ('watercolor', 'Watercolor'),
        ('sketch', 'Sketch'),
        ('3d_render', '3D Render'),
    ], string='Style', default='photorealistic',
        help='Visual style for the generated image. Photorealistic creates photo-like images; other styles create artistic interpretations.')

    aspect_ratio = fields.Selection([
        ('1024x1024', 'Square (1:1)'),
        ('1792x1024', 'Landscape (16:9)'),
        ('1024x1792', 'Portrait (9:16)'),
    ], string='Size', default='1792x1024',
        help='Image dimensions. Landscape (16:9) is ideal for blog headers; Square (1:1) works well for social media; Portrait (9:16) for mobile-first content.')

    quality = fields.Selection([
        ('standard', 'Standard'),
        ('hd', 'HD'),
    ], string='Quality', default='standard',
        help='Image quality level. HD produces sharper images with more detail but uses more tokens.')

    # === Task Tracking ===
    task_id = fields.Char('API Task ID', index=True,
        help='Internal identifier for tracking this image generation task in the O\'Toolkit API.')
    state = fields.Selection([
        ('draft', 'Draft'),
        ('pending', 'Pending'),
        ('processing', 'Processing'),
        ('done', 'Done'),
        ('error', 'Error'),
    ], string='Status', default='draft', index=True,
        help='Current status of image generation. Draft: not submitted; Pending: waiting to be sent; Processing: AI is generating; Done: image ready; Error: generation failed.')

    # === Result ===
    image = fields.Binary('Image', attachment=True,
        help='The generated image file. Stored as an Odoo attachment.')
    image_filename = fields.Char('Filename',
        help='The filename for the image when downloaded.')
    revised_prompt = fields.Text('Revised Prompt',
        help='The prompt as revised by the AI for better generation. The AI may modify your prompt to produce better results.')

    # === Optimization ===
    image_original = fields.Binary('Original Image', attachment=True,
        help='The original unoptimized image. Kept as backup before optimization.')
    image_webp = fields.Binary('WebP Image', attachment=True,
        help='WebP version of the image for modern browsers. Smaller file size with same quality.')

    original_size = fields.Integer('Original Size (bytes)',
        help='File size of the original generated image before any optimization.')
    optimized_size = fields.Integer('Optimized Size (bytes)',
        help='File size after optimization. Lower is better for web performance.')
    webp_size = fields.Integer('WebP Size (bytes)',
        help='File size of the WebP version.')
    size_reduction = fields.Float('Size Reduction %', compute='_compute_size_reduction',
        help='Percentage reduction in file size from optimization.')

    width = fields.Integer('Width (px)',
        help='Image width in pixels.')
    height = fields.Integer('Height (px)',
        help='Image height in pixels.')

    is_optimized = fields.Boolean('Optimized', default=False,
        help='Indicates if this image has been optimized for web.')
    optimization_quality = fields.Integer('Optimization Quality', default=85,
        help='JPEG/WebP quality level (1-100). Lower means smaller files but less quality. 85 is a good balance.')

    # === Usage ===
    alt_text = fields.Char('Alt Text',
        help='SEO-friendly alt text describing the image. Important for accessibility and search engine optimization.')
    is_cover = fields.Boolean('Use as Cover Image',
        help='If enabled, this image will be used as the cover/header image for the blog post.')
    is_og_image = fields.Boolean('Use as OpenGraph Image',
        help='If enabled, this image will be used for social media sharing previews (Facebook, Twitter, LinkedIn).')
    inserted_in_content = fields.Boolean('Inserted in Content',
        help='Indicates whether this image has been embedded within the content body.')

    # === Tracking ===
    token_cost = fields.Integer('Tokens Used',
        help='Number of API tokens consumed to generate this image. HD quality images cost more tokens.')
    error_message = fields.Text('Error Message',
        help='If generation failed, this contains the error details. Check this to troubleshoot issues.')

    # === Retry Logic ===
    retry_count = fields.Integer('Retry Count', default=0,
        help='Number of retry attempts made for this image generation.')
    max_retries = fields.Integer('Max Retries', default=3,
        help='Maximum number of retry attempts before permanently failing.')
    next_retry_at = fields.Datetime('Next Retry At',
        help='Scheduled time for next retry attempt.')
    processing_since = fields.Datetime('Processing Since',
        help='Timestamp of when processing started, for stuck task detection.')

    @api.model_create_multi
    def create(self, vals_list):
        """Auto-generate name if not provided."""
        for vals in vals_list:
            if not vals.get('name') and vals.get('prompt'):
                vals['name'] = vals['prompt'][:50] + '...' if len(vals['prompt']) > 50 else vals['prompt']
        return super().create(vals_list)

    def _get_api_config(self):
        """Get API configuration."""
        ICP = self.env['ir.config_parameter'].sudo()
        base_url = ICP.get_param('otoolkit.api.endpoint', '')
        api_key = ICP.get_param('otoolkit_api_key', '')

        if not api_key:
            raise UserError(_("Please configure your O'Toolkit API key in Settings."))
        if not base_url:
            raise UserError(_("O'Toolkit API endpoint is not configured."))

        return base_url, api_key

    def action_generate(self):
        """Submit image for generation."""
        self.ensure_one()

        if not self.prompt:
            raise UserError(_("Please provide a generation prompt."))

        base_url, api_key = self._get_api_config()

        headers = {
            'Content-Type': 'application/json',
            'Odoo-Api-Key': api_key,
        }

        payload = {
            'prompt': self.prompt,
            'style': self.style,
            'size': self.aspect_ratio,
            'quality': self.quality,
            'odoo_user_id': self.env.uid,
        }

        if self.negative_prompt:
            payload['negative_prompt'] = self.negative_prompt

        try:
            response = requests.post(
                f"{base_url.rstrip('/')}/api/seo-content/image/",
                headers=headers,
                json=payload,
                timeout=30
            )

            if response.status_code == 400:
                try:
                    error_data = response.json()
                    error_msg = error_data.get('error') or error_data.get('detail') or 'Unknown error'
                except (ValueError, json.JSONDecodeError):
                    error_msg = response.text[:500] if response.text else 'Unknown error'
                raise UserError(_("API Error: %s") % error_msg)

            if response.status_code >= 400:
                raise UserError(_("API Error (%s): %s") % (response.status_code, response.text[:500]))

            response.raise_for_status()

            try:
                result = response.json()
            except (ValueError, json.JSONDecodeError):
                raise UserError(_("Invalid response from API: %s") % response.text[:200])

            if result.get('success') and result.get('task_id'):
                self.write({
                    'task_id': result['task_id'],
                    'state': 'processing',
                    'error_message': False,
                    'processing_since': fields.Datetime.now(),
                })
            else:
                raise UserError(_("Failed to submit image generation: %s") % result.get('error', 'Unknown error'))

        except requests.exceptions.RequestException as e:
            _logger.error(f"Image generation request error: {e}")
            self.write({
                'state': 'error',
                'error_message': str(e),
            })
            raise UserError(_("Failed to submit image generation: %s") % str(e))

    def action_retry(self):
        """Retry failed image generation."""
        self.ensure_one()
        self.write({
            'state': 'pending',
            'error_message': False,
            'task_id': False,
        })
        return self.action_generate()

    def action_regenerate(self):
        """Regenerate image with current settings (allows modifying prompt/style first)."""
        self.ensure_one()

        if self.state not in ('done', 'error'):
            raise UserError(_("Can only regenerate completed or failed images."))

        # Store current image as backup (original) if we have an image and no backup yet
        if self.image and not self.image_original:
            self.image_original = self.image

        # Reset state and clear generation results
        self.write({
            'state': 'pending',
            'task_id': False,
            'error_message': False,
            'image': False,
            'image_webp': False,
            'revised_prompt': False,
            'is_optimized': False,
            'optimized_size': 0,
            'webp_size': 0,
            'token_cost': 0,
        })

        # Submit for generation
        return self.action_generate()

    def action_poll_status(self):
        """Check the status of image generation."""
        self.ensure_one()

        if not self.task_id:
            raise UserError(_("No task ID found. Please generate the image first."))

        base_url, api_key = self._get_api_config()

        headers = {
            'Content-Type': 'application/json',
            'Odoo-Api-Key': api_key,
        }

        # Safely convert task_id to int
        try:
            task_id_int = int(self.task_id)
        except (ValueError, TypeError):
            raise UserError(_("Invalid task ID format: %s") % self.task_id)

        try:
            response = requests.post(
                f"{base_url.rstrip('/')}/api/tasks/",
                headers=headers,
                json={'task_ids': [task_id_int]},
                timeout=30
            )

            response.raise_for_status()

            try:
                data = response.json()
            except (ValueError, json.JSONDecodeError):
                _logger.error("Invalid JSON response from tasks API: %s", response.text[:200])
                return

            tasks = data.get('tasks', []) if isinstance(data, dict) else data

            if tasks and len(tasks) > 0:
                task = tasks[0]
                status = task.get('status')

                if status == 'completed':
                    result = task.get('result', {})
                    self._process_completed_image(result)
                elif status == 'failed':
                    error_msg = task.get('result', {}).get('error', 'Generation failed')
                    error_type = task.get('result', {}).get('type', '')
                    # Retry on transient errors
                    if error_type in ('rate_limit', 'timeout', 'connection_error'):
                        self._schedule_retry(error_msg)
                    else:
                        self.write({
                            'state': 'error',
                            'error_message': error_msg,
                        })
                        # Notify user
                        if self.content_id and self.content_id.create_uid and self.content_id.create_uid.partner_id:
                            self.env['bus.bus']._sendone(
                                self.content_id.create_uid.partner_id,
                                'simple_notification',
                                {
                                    'title': _("Image Generation Failed"),
                                    'message': _("An image for '%s' could not be generated.") % (
                                        self.content_id.generated_title or self.content_id.name),
                                    'type': 'warning',
                                    'sticky': False,
                                }
                            )
                        self._check_content_images_complete()
                # else: still processing, do nothing

        except requests.exceptions.RequestException as e:
            _logger.error("Image status poll error: %s", e)

    def _process_completed_image(self, result):
        """Process a completed image generation result."""
        self.ensure_one()

        image_base64 = result.get('image_base64')
        if not image_base64:
            self.write({
                'state': 'error',
                'error_message': 'No image data received',
            })
            return

        # Parse size from aspect_ratio
        size_parts = self.aspect_ratio.split('x')
        width = int(size_parts[0]) if len(size_parts) == 2 else 1024
        height = int(size_parts[1]) if len(size_parts) == 2 else 1024

        # Generate alt text if not set
        alt_text = self.alt_text
        if not alt_text and self.content_id:
            alt_text = f"{self.content_id.generated_title or self.content_id.name} - Image"

        self.write({
            'image': image_base64,
            'image_filename': f"seo_image_{self.id}.png",
            'revised_prompt': result.get('revised_prompt', ''),
            'width': width,
            'height': height,
            'token_cost': result.get('cost', 0),
            'state': 'done',
            'error_message': False,
            'alt_text': alt_text,
        })

        # Notify user of individual image completion
        if self.content_id and self.content_id.create_uid and self.content_id.create_uid.partner_id:
            total = len(self.content_id.image_ids)
            done = len(self.content_id.image_ids.filtered(lambda i: i.state == 'done'))
            self.env['bus.bus']._sendone(
                self.content_id.create_uid.partner_id,
                'simple_notification',
                {
                    'title': _("Image Generated"),
                    'message': _("Image %d/%d for '%s' is ready.") % (
                        done, total, self.content_id.generated_title or self.content_id.name),
                    'type': 'info',
                    'sticky': False,
                }
            )

        # Update content state if all images are done
        self._check_content_images_complete()

    def _schedule_retry(self, error_msg):
        """Schedule a retry with exponential backoff, or permanently fail."""
        self.ensure_one()
        backoff_minutes = [1, 5, 15]

        if self.retry_count >= self.max_retries:
            self.write({
                'state': 'error',
                'error_message': _("Permanently failed after %d retries. Last error: %s") % (
                    self.retry_count, error_msg),
                'processing_since': False,
            })
            _logger.warning("Image %s permanently failed after %d retries", self.id, self.retry_count)
            # Notify user of permanent failure
            if self.content_id and self.content_id.create_uid and self.content_id.create_uid.partner_id:
                self.env['bus.bus']._sendone(
                    self.content_id.create_uid.partner_id,
                    'simple_notification',
                    {
                        'title': _("Image Generation Failed"),
                        'message': _("An image for '%s' failed after %d retries.") % (
                            self.content_id.generated_title or self.content_id.name,
                            self.retry_count),
                        'type': 'warning',
                        'sticky': True,
                    }
                )
            self._check_content_images_complete()
            return

        from datetime import timedelta
        delay = backoff_minutes[min(self.retry_count, len(backoff_minutes) - 1)]
        next_retry = fields.Datetime.now() + timedelta(minutes=delay)

        self.write({
            'state': 'pending',
            'retry_count': self.retry_count + 1,
            'next_retry_at': next_retry,
            'error_message': _("Retry %d/%d scheduled. Error: %s") % (
                self.retry_count + 1, self.max_retries, error_msg),
            'processing_since': False,
            'task_id': False,
        })
        _logger.info("Image %s retry %d/%d scheduled at %s",
                      self.id, self.retry_count, self.max_retries, next_retry)

    def _check_content_images_complete(self):
        """Check if all images for the content are complete.

        Transitions content to 'review' when no images are pending/processing,
        regardless of whether they are 'done' or 'error'.
        """
        if not self.content_id:
            return

        content = self.content_id
        pending_images = content.image_ids.filtered(
            lambda i: i.state in ('pending', 'processing')
        )

        if not pending_images and content.state == 'images_pending':
            content.write({'state': 'review'})
            # Send bus notification
            if content.create_uid and content.create_uid.partner_id:
                self.env['bus.bus']._sendone(
                    content.create_uid.partner_id,
                    'simple_notification',
                    {
                        'title': _("Images Ready!"),
                        'message': _("All images for '%s' are ready.") % (
                            content.generated_title or content.name),
                        'type': 'success',
                        'sticky': False,
                    }
                )

    @api.model
    def cron_submit_pending_images(self):
        """Cron job to submit pending images for generation.

        Also picks up images with pending retries.
        """
        from datetime import timedelta
        now = fields.Datetime.now()

        # Include images with pending retries (next_retry_at <= now)
        pending = self.search([
            '|',
            '&', ('state', '=', 'pending'), ('next_retry_at', '=', False),
            '&', ('state', '=', 'pending'), ('next_retry_at', '<=', now),
        ], limit=10)

        for image in pending:
            try:
                image.write({'next_retry_at': False})
                image.action_generate()
            except Exception as e:
                _logger.error(f"Failed to submit image {image.id}: {e}")
                image.write({
                    'state': 'error',
                    'error_message': str(e),
                })

    @api.model
    def cron_poll_image_status(self):
        """Cron job to poll status of processing images.

        Also handles stuck images (processing > 30 min) and retries on failure.
        """
        from datetime import timedelta
        now = fields.Datetime.now()

        # --- Detect stuck images (processing for > 30 minutes) ---
        stuck_cutoff = now - timedelta(minutes=30)
        stuck_images = self.search([
            ('state', '=', 'processing'),
            ('processing_since', '!=', False),
            ('processing_since', '<', stuck_cutoff),
        ], limit=10)
        for img in stuck_images:
            _logger.warning("Image %s stuck in processing since %s, scheduling retry",
                            img.id, img.processing_since)
            img._schedule_retry(_("Task stuck for over 30 minutes"))

        # --- Poll processing tasks ---
        processing = self.search([
            ('state', '=', 'processing'),
            ('task_id', '!=', False),
        ], limit=20)

        if not processing:
            return

        # Batch poll all tasks - safely convert task_ids to int
        task_ids = []
        task_id_map = {}  # Maps int task_id back to image
        for img in processing:
            if img.task_id:
                try:
                    task_id_int = int(img.task_id)
                    task_ids.append(task_id_int)
                    task_id_map[str(task_id_int)] = img
                except (ValueError, TypeError):
                    _logger.warning("Invalid task_id format for image %s: %s", img.id, img.task_id)
                    img.write({
                        'state': 'error',
                        'error_message': f'Invalid task ID format: {img.task_id}',
                    })

        if not task_ids:
            return

        try:
            base_url, api_key = processing[0]._get_api_config()

            headers = {
                'Content-Type': 'application/json',
                'Odoo-Api-Key': api_key,
            }

            response = requests.post(
                f"{base_url.rstrip('/')}/api/tasks/",
                headers=headers,
                json={'task_ids': task_ids},
                timeout=30
            )

            response.raise_for_status()

            try:
                data = response.json()
            except (ValueError, json.JSONDecodeError):
                _logger.error("Invalid JSON response from tasks API in cron: %s", response.text[:200])
                return

            tasks = data.get('tasks', []) if isinstance(data, dict) else data

            # Create mapping of task_id to result
            task_result_map = {str(t.get('id')): t for t in tasks if t.get('id')}

            for task_id_str, image in task_id_map.items():
                task = task_result_map.get(task_id_str)
                if not task:
                    continue

                status = task.get('status')

                if status == 'completed':
                    result = task.get('result', {})
                    image._process_completed_image(result)
                elif status == 'failed':
                    error_msg = task.get('result', {}).get('error', 'Generation failed')
                    image._schedule_retry(error_msg)

        except Exception as e:
            _logger.error("Image status poll cron error: %s", e)

    @api.depends('original_size', 'optimized_size')
    def _compute_size_reduction(self):
        for record in self:
            if record.original_size and record.optimized_size:
                record.size_reduction = round(
                    (1 - record.optimized_size / record.original_size) * 100, 1
                )
            else:
                record.size_reduction = 0

    def action_optimize(self):
        """Optimize image for web - compress and create WebP version."""
        self.ensure_one()

        if not HAS_PILLOW:
            raise UserError(_("Image optimization requires Pillow library. Please install it with: pip install Pillow"))

        if not self.image:
            raise UserError(_("No image to optimize."))

        try:
            # Decode original image
            image_data = base64.b64decode(self.image)
            original_size = len(image_data)

            # Store original if not already stored
            if not self.image_original:
                self.image_original = self.image

            # Open with Pillow
            img = Image.open(io.BytesIO(image_data))

            # Get actual dimensions
            width, height = img.size

            # Convert to RGB if necessary (for JPEG/WebP compatibility)
            if img.mode in ('RGBA', 'P'):
                # Create white background for transparency
                background = Image.new('RGB', img.size, (255, 255, 255))
                if img.mode == 'P':
                    img = img.convert('RGBA')
                background.paste(img, mask=img.split()[3] if len(img.split()) > 3 else None)
                img = background
            elif img.mode != 'RGB':
                img = img.convert('RGB')

            quality = self.optimization_quality or 85

            # Optimize as JPEG
            jpeg_buffer = io.BytesIO()
            img.save(jpeg_buffer, format='JPEG', quality=quality, optimize=True)
            optimized_jpeg = jpeg_buffer.getvalue()
            optimized_size = len(optimized_jpeg)

            # Try to create WebP version (requires libwebp)
            webp_data = None
            webp_size = 0
            try:
                webp_buffer = io.BytesIO()
                img.save(webp_buffer, format='WEBP', quality=quality)
                webp_data = webp_buffer.getvalue()
                webp_size = len(webp_data)
            except Exception as webp_error:
                _logger.warning(f"WebP conversion not available: {webp_error}")

            # Update record
            update_vals = {
                'image': base64.b64encode(optimized_jpeg),
                'image_filename': f"seo_image_{self.id}.jpg",
                'original_size': original_size,
                'optimized_size': optimized_size,
                'webp_size': webp_size,
                'width': width,
                'height': height,
                'is_optimized': True,
            }
            if webp_data:
                update_vals['image_webp'] = base64.b64encode(webp_data)

            self.write(update_vals)

            _logger.info(
                f"Image {self.id} optimized: {original_size} -> {optimized_size} bytes "
                f"({round((1 - optimized_size/original_size) * 100, 1)}% reduction)"
                + (f", WebP: {webp_size} bytes" if webp_size else "")
            )

            message = _('Reduced from %s KB to %s KB (%.1f%% smaller)') % (
                round(original_size / 1024, 1),
                round(optimized_size / 1024, 1),
                (1 - optimized_size / original_size) * 100,
            )
            if webp_size:
                message += _('. WebP: %s KB') % round(webp_size / 1024, 1)

        except Exception as e:
            _logger.error(f"Image optimization failed: {e}")
            raise UserError(_("Image optimization failed: %s") % str(e))

    def action_restore_original(self):
        """Restore the original unoptimized image."""
        self.ensure_one()

        if not self.image_original:
            raise UserError(_("No original image stored."))

        self.write({
            'image': self.image_original,
            'image_filename': f"seo_image_{self.id}.png",
            'is_optimized': False,
            'optimized_size': 0,
            'image_webp': False,
            'webp_size': 0,
        })

    def action_auto_optimize_all(self):
        """Optimize all non-optimized images."""
        images = self.search([
            ('state', '=', 'done'),
            ('is_optimized', '=', False),
            ('image', '!=', False),
        ])

        optimized_count = 0
        for image in images:
            try:
                image.action_optimize()
                optimized_count += 1
            except Exception as e:
                _logger.error(f"Failed to optimize image {image.id}: {e}")

        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _('Batch Optimization Complete'),
                'message': _('%d images optimized.') % optimized_count,
                'type': 'success',
                'sticky': False,
                'next': {'type': 'ir.actions.client', 'tag': 'soft_reload'},
            }
        }
