import base64
import csv
import io
import logging

from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError

_logger = logging.getLogger(__name__)


class SeoContentBatch(models.Model):
    _name = 'otk.seo.content.batch'
    _description = 'SEO Content Batch Job'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'create_date desc'

    name = fields.Char('Batch Name', required=True, default=lambda self: _('New Batch'))
    state = fields.Selection([
        ('draft', 'Draft'),
        ('queued', 'Queued'),
        ('processing', 'Processing'),
        ('paused', 'Paused'),
        ('done', 'Completed'),
        ('failed', 'Failed'),
        ('cancelled', 'Cancelled'),
    ], string='Status', default='draft', tracking=True)

    # === Concurrency Settings ===
    concurrency = fields.Integer('Concurrency', default=5,
        help='Number of items to process per cron run. Higher values process faster but use more resources.')

    @api.constrains('concurrency')
    def _check_concurrency_range(self):
        for record in self:
            if not (1 <= record.concurrency <= 10):
                raise ValidationError(_('Concurrency must be between 1 and 10.'))

    # === Source Configuration ===
    source_type = fields.Selection([
        ('products', 'From Products'),
        ('csv', 'From CSV'),
        ('keywords', 'From Keywords List'),
    ], string='Source Type', default='products', required=True)

    product_ids = fields.Many2many('product.template', string='Products',
        help='Products to generate content for')
    csv_file = fields.Binary('CSV File', attachment=True)
    csv_filename = fields.Char('CSV Filename')
    keywords_list = fields.Text('Keywords List',
        help='One keyword/topic per line')

    # === Batch Items ===
    item_ids = fields.One2many('otk.seo.content.batch.item', 'batch_id', string='Batch Items')

    # === Shared Settings ===
    content_type = fields.Selection([
        ('blog_post', 'Blog Post'),
        ('product_desc', 'Product Description'),
        ('category_desc', 'Category Description'),
        ('landing_page', 'Landing Page Content'),
    ], string='Content Type', default='product_desc', required=True)

    template_id = fields.Many2one('otk.seo.template', 'Content Template',
        domain="[('content_type', '=', content_type)]")
    brand_voice_id = fields.Many2one('otk.seo.brand.voice', 'Brand Voice',
        default=lambda self: self.env['otk.seo.brand.voice'].search([('is_default', '=', True)], limit=1))
    tone = fields.Selection([
        ('professional', 'Professional'),
        ('casual', 'Casual'),
        ('technical', 'Technical'),
        ('persuasive', 'Persuasive'),
        ('conversational', 'Conversational'),
    ], string='Tone', default='professional')
    target_word_count = fields.Selection([
        ('short', 'Short (300-500 words)'),
        ('medium', 'Medium (500-1000 words)'),
        ('long', 'Long (1000-2000 words)'),
    ], string='Target Length', default='medium')
    language_id = fields.Many2one('res.lang', 'Language',
        default=lambda self: self.env['res.lang']._lang_get(self.env.lang or 'en_US'))

    # === Image Settings ===
    include_images = fields.Boolean('Generate Images', default=True)
    image_count = fields.Integer('Images per Content', default=1)
    image_style = fields.Selection([
        ('photorealistic', 'Photorealistic'),
        ('illustration', 'Illustration'),
        ('digital_art', 'Digital Art'),
        ('watercolor', 'Watercolor'),
        ('sketch', 'Sketch'),
        ('3d_render', '3D Render'),
    ], string='Image Style', default='photorealistic')

    # === Blog Settings ===
    blog_id = fields.Many2one('blog.blog', 'Target Blog',
        help='The blog where posts will be published. Only used for Blog Post content type.')

    # === Progress Tracking ===
    total_items = fields.Integer('Total Items', compute='_compute_progress', store=True)
    completed_items = fields.Integer('Completed', compute='_compute_progress', store=True)
    failed_items = fields.Integer('Failed', compute='_compute_progress', store=True)
    progress_percent = fields.Float('Progress %', compute='_compute_progress', store=True)

    # === Generated Content ===
    content_ids = fields.One2many('otk.seo.content', 'batch_id', string='Generated Content')
    content_count = fields.Integer('Content Count', compute='_compute_content_count')

    # === Timing ===
    started_at = fields.Datetime('Started At')
    completed_at = fields.Datetime('Completed At')
    duration = fields.Float('Duration (minutes)', compute='_compute_duration')

    @api.depends('item_ids', 'item_ids.state')
    def _compute_progress(self):
        for batch in self:
            items = batch.item_ids
            batch.total_items = len(items)
            batch.completed_items = len(items.filtered(lambda i: i.state == 'done'))
            batch.failed_items = len(items.filtered(lambda i: i.state == 'failed'))
            batch.progress_percent = (
                (batch.completed_items + batch.failed_items) / batch.total_items * 100
                if batch.total_items else 0
            )

    @api.depends('content_ids')
    def _compute_content_count(self):
        for batch in self:
            batch.content_count = len(batch.content_ids)

    # === ETA ===
    eta_minutes = fields.Float('ETA (minutes)', compute='_compute_eta',
        help='Estimated time remaining for batch completion.')
    avg_processing_time = fields.Float('Avg Time (s)', compute='_compute_eta',
        help='Average processing time per item in seconds.')

    @api.depends('started_at', 'completed_at')
    def _compute_duration(self):
        for batch in self:
            if batch.started_at and batch.completed_at:
                delta = batch.completed_at - batch.started_at
                batch.duration = delta.total_seconds() / 60
            else:
                batch.duration = 0

    @api.depends('started_at', 'completed_items', 'failed_items', 'total_items')
    def _compute_eta(self):
        for batch in self:
            done_count = batch.completed_items + batch.failed_items
            if batch.started_at and done_count > 0:
                now = fields.Datetime.now()
                elapsed = (now - batch.started_at).total_seconds()
                avg_time = elapsed / done_count
                remaining = batch.total_items - done_count
                batch.avg_processing_time = round(avg_time, 1)
                batch.eta_minutes = round((avg_time * remaining) / 60, 1)
            else:
                batch.avg_processing_time = 0
                batch.eta_minutes = 0

    @api.onchange('source_type')
    def _onchange_source_type(self):
        """Update content type based on source selection."""
        if self.source_type == 'products':
            self.content_type = 'product_desc'
        elif self.source_type == 'keywords':
            # Keywords can be for any content type, default to blog
            if self.content_type == 'product_desc':
                self.content_type = 'blog_post'

    @api.onchange('content_type')
    def _onchange_content_type(self):
        """Reset template when content type changes and update source type."""
        self.template_id = False
        # If switching to product_desc, suggest products source
        if self.content_type == 'product_desc' and self.source_type == 'keywords':
            self.source_type = 'products'
        # If switching away from product_desc with products source, switch to keywords
        elif self.content_type != 'product_desc' and self.source_type == 'products':
            self.source_type = 'keywords'

    @api.onchange('template_id')
    def _onchange_template_id(self):
        """Apply template settings to batch."""
        if self.template_id:
            self.tone = self.template_id.default_tone
            self.target_word_count = self.template_id.default_word_count
            self.include_images = self.template_id.include_images
            self.image_count = self.template_id.default_image_count
            self.image_style = self.template_id.default_image_style

    def action_prepare_items(self):
        """Parse source and ADD items to batch (preserves existing items)."""
        self.ensure_one()

        # Collect existing items to avoid duplicates
        existing_keys = set()
        for item in self.item_ids:
            if item.product_id:
                existing_keys.add(f"product_{item.product_id.id}")
            elif item.keywords:
                existing_keys.add(f"keywords_{item.keywords.lower().strip()}")

        items_data = []

        if self.source_type == 'products':
            if not self.product_ids:
                raise UserError(_("Please select at least one product."))
            for product in self.product_ids:
                key = f"product_{product.id}"
                if key not in existing_keys:
                    items_data.append({
                        'batch_id': self.id,
                        'name': product.name,
                        'product_id': product.id,
                        'keywords': product.name,
                        'topic': self._prepare_product_brief(product),
                    })
            # Clear product selection after adding
            self.product_ids = [(5, 0, 0)]

        elif self.source_type == 'csv':
            if not self.csv_file:
                raise UserError(_("Please upload a CSV file."))
            csv_items = self._parse_csv()
            for item in csv_items:
                key = f"keywords_{item.get('keywords', '').lower().strip()}"
                if key not in existing_keys:
                    item['batch_id'] = self.id
                    items_data.append(item)
            # Clear CSV after adding
            self.csv_file = False
            self.csv_filename = False

        elif self.source_type == 'keywords':
            if not self.keywords_list:
                raise UserError(_("Please enter keywords or topics."))
            for line in self.keywords_list.strip().split('\n'):
                line = line.strip()
                if line:
                    key = f"keywords_{line.lower()}"
                    if key not in existing_keys:
                        items_data.append({
                            'batch_id': self.id,
                            'name': line[:100],
                            'keywords': line,
                        })
            # Clear keywords list after adding
            self.keywords_list = False

        if not items_data:
            raise UserError(_("No new items to add. Items may already exist in the batch."))

        # Create new batch items
        self.env['otk.seo.content.batch.item'].create(items_data)

    def action_clear_items(self):
        """Remove all items from the batch."""
        self.ensure_one()

        if self.state not in ('draft',):
            raise UserError(_("Can only clear items when batch is in Draft state."))

        self.item_ids.unlink()

    def _prepare_product_brief(self, product):
        """Prepare product information for AI."""
        brief = f"Product: {product.name}\n"
        if product.description:
            brief += f"Description: {product.description}\n"
        if product.list_price:
            brief += f"Price: {product.list_price}\n"
        if product.categ_id:
            brief += f"Category: {product.categ_id.complete_name}\n"
        return brief

    def _parse_csv(self):
        """Parse CSV file and return items data (without batch_id)."""
        items_data = []
        try:
            csv_data = base64.b64decode(self.csv_file).decode('utf-8')
            reader = csv.DictReader(io.StringIO(csv_data))

            for row in reader:
                # Expected columns: keywords, topic (optional), name (optional)
                keywords = row.get('keywords', row.get('keyword', ''))
                topic = row.get('topic', row.get('brief', ''))
                name = row.get('name', keywords[:100] if keywords else 'Untitled')

                if keywords or topic:
                    items_data.append({
                        'name': name,
                        'keywords': keywords,
                        'topic': topic,
                    })
        except Exception as e:
            raise UserError(_("Error parsing CSV file: %s") % str(e))

        if not items_data:
            raise UserError(_("No valid items found in CSV. Expected columns: keywords, topic (optional)"))

        return items_data

    def action_start_batch(self):
        """Queue the batch for async processing via cron."""
        self.ensure_one()

        if not self.item_ids:
            self.action_prepare_items()

        if not self.item_ids:
            raise UserError(_("No items to process."))

        self.write({
            'state': 'queued',
            'started_at': fields.Datetime.now(),
        })

        # Mark all items as pending - they will be picked up by the cron job
        self.item_ids.write({'state': 'pending'})

        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _('Batch Queued'),
                'message': _('Batch has been queued for processing. Items will be processed asynchronously.'),
                'type': 'success',
                'sticky': False,
            }
        }

    def action_pause(self):
        """Pause the batch processing."""
        self.ensure_one()
        if self.state in ('queued', 'processing'):
            self.write({'state': 'paused'})

    def action_resume(self):
        """Resume paused batch processing."""
        self.ensure_one()
        if self.state == 'paused':
            self.write({'state': 'processing'})

    def action_cancel(self):
        """Cancel the batch."""
        self.ensure_one()
        if self.state in ('queued', 'processing', 'paused'):
            self.write({'state': 'cancelled'})
            self.item_ids.filtered(lambda i: i.state == 'pending').write({'state': 'cancelled'})
        return True

    @api.model
    def cron_process_batch_items(self):
        """Cron job to process batch items asynchronously.

        Processes up to `concurrency` items per active batch per cron run.
        Skips paused and cancelled batches.
        """
        # Find all batches that are queued or processing (not paused)
        active_batches = self.search([
            ('state', 'in', ['queued', 'processing']),
        ])

        for batch in active_batches:
            # Skip if cancelled or paused
            if batch.state in ('cancelled', 'paused'):
                continue

            # Get pending items up to concurrency limit
            items_limit = min(batch.concurrency or 5, 10)
            pending_items = batch.item_ids.filtered(lambda i: i.state == 'pending')[:items_limit]

            if not pending_items:
                # All items processed, complete the batch
                batch._complete_batch()
                continue

            # Update batch state to processing if it was queued
            if batch.state == 'queued':
                batch.state = 'processing'

            # Process items (with commit after each to preserve state)
            for item in pending_items:
                try:
                    item._process_item()
                    self.env.cr.commit()
                except Exception as e:
                    _logger.error("Error processing batch item %s: %s", item.id, e)
                    item.write({
                        'state': 'failed',
                        'error_message': str(e),
                    })
                    self.env.cr.commit()

        return True

    def _complete_batch(self):
        """Mark batch as completed."""
        self.ensure_one()
        failed = self.item_ids.filtered(lambda i: i.state == 'failed')
        total = len(self.item_ids)

        if total == 0:
            state = 'done'
        elif len(failed) == total:
            state = 'failed'
        else:
            state = 'done'

        self.write({
            'state': state,
            'completed_at': fields.Datetime.now(),
        })

        # Send bus notification to batch creator
        if self.create_uid and self.create_uid.partner_id:
            succeeded = len(self.item_ids) - len(failed)
            self.env['bus.bus']._sendone(
                self.create_uid.partner_id,
                'simple_notification',
                {
                    'title': _("Batch Complete!"),
                    'message': _("Batch '%s' complete: %d/%d items succeeded.") % (
                        self.name, succeeded, len(self.item_ids)),
                    'type': 'success' if not failed else 'warning',
                    'sticky': False,
                }
            )

    def action_view_content(self):
        """View generated content."""
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': _('Generated Content'),
            'res_model': 'otk.seo.content',
            'view_mode': 'list,form',
            'domain': [('batch_id', '=', self.id)],
            'context': {'default_batch_id': self.id},
        }

    def action_retry_failed(self):
        """Retry failed items - re-queue them for async processing."""
        self.ensure_one()
        failed_items = self.item_ids.filtered(lambda i: i.state == 'failed')

        if not failed_items:
            raise UserError(_("No failed items to retry."))

        failed_items.write({'state': 'pending', 'error_message': False})
        self.write({'state': 'queued'})

        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _('Items Re-queued'),
                'message': _('%d failed items have been re-queued for processing.') % len(failed_items),
                'type': 'success',
                'sticky': False,
            }
        }


class SeoContentBatchItem(models.Model):
    _name = 'otk.seo.content.batch.item'
    _description = 'SEO Content Batch Item'
    _order = 'sequence, id'

    batch_id = fields.Many2one('otk.seo.content.batch', string='Batch',
        required=True, ondelete='cascade')
    sequence = fields.Integer('Sequence', default=10)
    name = fields.Char('Name', required=True)

    state = fields.Selection([
        ('draft', 'Draft'),
        ('pending', 'Pending'),
        ('processing', 'Processing'),
        ('done', 'Completed'),
        ('failed', 'Failed'),
        ('cancelled', 'Cancelled'),
    ], string='Status', default='draft')

    # === Source Data ===
    product_id = fields.Many2one('product.template', 'Product')
    keywords = fields.Char('Keywords')
    topic = fields.Text('Topic/Brief')

    # === Result ===
    content_id = fields.Many2one('otk.seo.content', 'Generated Content', ondelete='set null')
    error_message = fields.Text('Error Message')

    def _process_item(self):
        """Process this batch item - generate content."""
        self.ensure_one()
        self.state = 'processing'

        try:
            batch = self.batch_id

            # Create content record
            content_vals = {
                'name': self.name,
                'content_type': batch.content_type,
                'source_keywords': self.keywords,
                'source_topic': self.topic,
                'source_product_id': self.product_id.id if self.product_id else False,
                'template_id': batch.template_id.id if batch.template_id else False,
                'brand_voice_id': batch.brand_voice_id.id if batch.brand_voice_id else False,
                'tone': batch.tone,
                'target_word_count': batch.target_word_count,
                'language_id': batch.language_id.id if batch.language_id else False,
                'requested_image_count': batch.image_count if batch.include_images else 0,
                'image_style': batch.image_style if batch.include_images else False,
                'blog_id': batch.blog_id.id if batch.blog_id else False,
                'batch_id': batch.id,
                'state': 'draft',
            }

            content = self.env['otk.seo.content'].create(content_vals)
            self.content_id = content.id

            # Generate content
            content.action_generate()

            self.state = 'done'
            _logger.info(f"Batch item {self.id} processed successfully: content {content.id}")

        except Exception as e:
            self.state = 'failed'
            self.error_message = str(e)
            _logger.error(f"Batch item {self.id} failed: {e}")

        return True
