"""
YouTube Bulk Upload Service.

Handles OAuth2 authentication (web-based flow) and bulk video uploads
to YouTube via the YouTube Data API v3.
"""

import json
import logging
import os
import random
import time
from datetime import datetime, timedelta
from pathlib import Path

from backend.core.config import settings
from backend.database.connection import SessionLocal

logger = logging.getLogger("viral_social.youtube_upload")

CLIENT_SECRET_PATH = Path(settings.data_path) / "youtube_client_secret.json"
TOKEN_PATH = Path(settings.data_path) / "youtube_upload_oauth.json"

VIDEO_EXTENSIONS = {
    ".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv", ".wmv",
    ".m4v", ".mpg", ".mpeg", ".3gp", ".ts",
}

SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]

# Retry config for upload errors
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
MAX_RETRIES = 5


class YouTubeUploadService:
    """Manages YouTube OAuth2 and bulk video uploads."""

    # ─── OAuth2 ─────────────────────────────────────────────

    def has_client_secrets(self) -> bool:
        return CLIENT_SECRET_PATH.exists() and CLIENT_SECRET_PATH.stat().st_size > 10

    def is_authenticated(self) -> bool:
        creds = self._get_credentials()
        return creds is not None and creds.valid

    def get_auth_status(self) -> dict:
        has_secret = self.has_client_secrets()
        creds = self._get_credentials() if has_secret else None
        authenticated = creds is not None and (creds.valid or creds.expired)
        return {
            "has_client_secret": has_secret,
            "authenticated": authenticated,
            "token_valid": creds.valid if creds else False,
            "token_expired": creds.expired if creds else False,
            "token_path": str(TOKEN_PATH),
        }

    def get_auth_url(self, redirect_uri: str) -> dict:
        if not self.has_client_secrets():
            return {"error": "client_secret.json nao encontrado. Faca upload primeiro."}

        from google_auth_oauthlib.flow import Flow

        flow = Flow.from_client_secrets_file(
            str(CLIENT_SECRET_PATH),
            scopes=SCOPES,
            redirect_uri=redirect_uri,
        )
        auth_url, state = flow.authorization_url(
            access_type="offline",
            include_granted_scopes="true",
            prompt="consent",
        )
        return {"auth_url": auth_url, "state": state}

    def handle_auth_callback(self, code: str, redirect_uri: str) -> dict:
        if not self.has_client_secrets():
            return {"error": "client_secret.json nao encontrado"}

        try:
            from google_auth_oauthlib.flow import Flow

            flow = Flow.from_client_secrets_file(
                str(CLIENT_SECRET_PATH),
                scopes=SCOPES,
                redirect_uri=redirect_uri,
            )
            flow.fetch_token(code=code)
            creds = flow.credentials

            # Save token
            token_data = {
                "token": creds.token,
                "refresh_token": creds.refresh_token,
                "token_uri": creds.token_uri,
                "client_id": creds.client_id,
                "client_secret": creds.client_secret,
                "scopes": list(creds.scopes),
                "expiry": creds.expiry.isoformat() if creds.expiry else None,
            }
            TOKEN_PATH.write_text(json.dumps(token_data, indent=2))
            logger.info("YouTube Upload OAuth token saved successfully")
            return {"success": True}
        except Exception as e:
            logger.error(f"OAuth callback error: {e}")
            return {"error": str(e)}

    def revoke_auth(self) -> dict:
        creds = self._get_credentials()
        if creds and creds.token:
            try:
                import requests
                requests.post(
                    "https://oauth2.googleapis.com/revoke",
                    params={"token": creds.token},
                    headers={"Content-Type": "application/x-www-form-urlencoded"},
                    timeout=10,
                )
            except Exception as e:
                logger.warning(f"Token revoke request failed: {e}")

        if TOKEN_PATH.exists():
            TOKEN_PATH.unlink()
        return {"success": True}

    def _get_credentials(self):
        """Load and refresh credentials from saved token file."""
        if not TOKEN_PATH.exists():
            return None

        try:
            from google.oauth2.credentials import Credentials
            from google.auth.transport.requests import Request

            token_data = json.loads(TOKEN_PATH.read_text())
            creds = Credentials(
                token=token_data.get("token"),
                refresh_token=token_data.get("refresh_token"),
                token_uri=token_data.get("token_uri", "https://oauth2.googleapis.com/token"),
                client_id=token_data.get("client_id"),
                client_secret=token_data.get("client_secret"),
                scopes=token_data.get("scopes"),
            )

            if creds.expired and creds.refresh_token:
                creds.refresh(Request())
                # Update saved token
                token_data["token"] = creds.token
                token_data["expiry"] = creds.expiry.isoformat() if creds.expiry else None
                TOKEN_PATH.write_text(json.dumps(token_data, indent=2))

            return creds
        except Exception as e:
            logger.error(f"Error loading credentials: {e}")
            return None

    def _build_youtube_service(self):
        """Build authenticated YouTube API service."""
        from googleapiclient.discovery import build

        creds = self._get_credentials()
        if not creds or not creds.valid:
            raise RuntimeError("YouTube nao autenticado. Conecte sua conta primeiro.")
        return build("youtube", "v3", credentials=creds)

    # ─── Scan & Batch ───────────────────────────────────────

    def scan_folder(self, folder_path: str) -> dict:
        """Scan a folder for video files."""
        path = Path(folder_path)
        if not path.exists():
            return {"error": f"Pasta nao encontrada: {folder_path}"}
        if not path.is_dir():
            return {"error": f"Caminho nao e uma pasta: {folder_path}"}

        files = []
        total_size = 0
        for f in sorted(path.iterdir()):
            if f.is_file() and f.suffix.lower() in VIDEO_EXTENSIONS:
                size = f.stat().st_size
                files.append({
                    "filename": f.name,
                    "path": str(f),
                    "size_bytes": size,
                    "size_formatted": self._format_size(size),
                })
                total_size += size

        return {
            "folder": folder_path,
            "count": len(files),
            "total_size_bytes": total_size,
            "total_size_formatted": self._format_size(total_size),
            "files": files,
        }

    def create_batch(
        self,
        folder_path: str,
        start_datetime: datetime,
        interval_minutes: int = 120,
        name: str = "",
        default_description: str = "",
        default_tags: str = "",
        default_category_id: str = "22",
        default_privacy: str = "private",
        use_publish_at: bool = True,
    ) -> dict:
        """Create a new upload batch from a folder of videos."""
        from backend.models.youtube_upload import YouTubeUploadBatch, YouTubeUploadItem

        scan = self.scan_folder(folder_path)
        if "error" in scan:
            return scan
        if scan["count"] == 0:
            return {"error": "Nenhum video encontrado na pasta"}

        db = SessionLocal()
        try:
            batch = YouTubeUploadBatch(
                name=name or Path(folder_path).name,
                folder_path=folder_path,
                start_datetime=start_datetime,
                interval_minutes=interval_minutes,
                default_description=default_description,
                default_tags=default_tags,
                default_category_id=default_category_id,
                default_privacy=default_privacy,
                use_publish_at=use_publish_at,
                total_files=scan["count"],
                status="active",
            )
            db.add(batch)
            db.flush()

            for i, file_info in enumerate(scan["files"]):
                scheduled = start_datetime + timedelta(minutes=interval_minutes * i)
                # Use filename (without extension) as default title
                title = Path(file_info["filename"]).stem
                item = YouTubeUploadItem(
                    batch_id=batch.id,
                    filename=file_info["filename"],
                    file_path=file_info["path"],
                    file_size_bytes=file_info["size_bytes"],
                    sort_order=i,
                    title=title,
                    scheduled_time=scheduled,
                )
                db.add(item)

            db.commit()
            db.refresh(batch)
            return batch.to_dict(include_items=True)
        except Exception as e:
            db.rollback()
            logger.error(f"Error creating batch: {e}")
            return {"error": str(e)}
        finally:
            db.close()

    def get_batch(self, batch_id: int) -> dict:
        from backend.models.youtube_upload import YouTubeUploadBatch
        db = SessionLocal()
        try:
            batch = db.query(YouTubeUploadBatch).filter_by(id=batch_id).first()
            if not batch:
                return {"error": "Batch nao encontrado"}
            return batch.to_dict(include_items=True)
        finally:
            db.close()

    def list_batches(self) -> list[dict]:
        from backend.models.youtube_upload import YouTubeUploadBatch
        db = SessionLocal()
        try:
            batches = db.query(YouTubeUploadBatch).order_by(
                YouTubeUploadBatch.created_at.desc()
            ).all()
            return [b.to_dict() for b in batches]
        finally:
            db.close()

    def pause_batch(self, batch_id: int) -> dict:
        return self._update_batch_status(batch_id, "paused", from_statuses=["active"])

    def resume_batch(self, batch_id: int) -> dict:
        return self._update_batch_status(batch_id, "active", from_statuses=["paused"])

    def cancel_batch(self, batch_id: int) -> dict:
        from backend.models.youtube_upload import YouTubeUploadBatch, YouTubeUploadItem
        db = SessionLocal()
        try:
            batch = db.query(YouTubeUploadBatch).filter_by(id=batch_id).first()
            if not batch:
                return {"error": "Batch nao encontrado"}
            batch.status = "cancelled"
            # Cancel pending items
            db.query(YouTubeUploadItem).filter(
                YouTubeUploadItem.batch_id == batch_id,
                YouTubeUploadItem.status.in_(["pending", "uploading"]),
            ).update({"status": "cancelled"}, synchronize_session="fetch")
            db.commit()
            return {"success": True, "status": "cancelled"}
        finally:
            db.close()

    def delete_batch(self, batch_id: int) -> dict:
        from backend.models.youtube_upload import YouTubeUploadBatch
        db = SessionLocal()
        try:
            batch = db.query(YouTubeUploadBatch).filter_by(id=batch_id).first()
            if not batch:
                return {"error": "Batch nao encontrado"}
            db.delete(batch)
            db.commit()
            return {"success": True}
        finally:
            db.close()

    def retry_failed(self, batch_id: int) -> dict:
        from backend.models.youtube_upload import YouTubeUploadBatch, YouTubeUploadItem
        db = SessionLocal()
        try:
            batch = db.query(YouTubeUploadBatch).filter_by(id=batch_id).first()
            if not batch:
                return {"error": "Batch nao encontrado"}

            failed_items = db.query(YouTubeUploadItem).filter(
                YouTubeUploadItem.batch_id == batch_id,
                YouTubeUploadItem.status == "failed",
            ).all()

            count = 0
            for item in failed_items:
                if item.retry_count < item.max_retries:
                    item.status = "pending"
                    item.error_message = ""
                    count += 1

            if batch.status in ("completed", "cancelled"):
                batch.status = "active"
            batch.failed_count = max(0, batch.failed_count - count)
            db.commit()
            return {"success": True, "retried": count}
        finally:
            db.close()

    def _update_batch_status(self, batch_id: int, new_status: str, from_statuses: list[str]) -> dict:
        from backend.models.youtube_upload import YouTubeUploadBatch
        db = SessionLocal()
        try:
            batch = db.query(YouTubeUploadBatch).filter_by(id=batch_id).first()
            if not batch:
                return {"error": "Batch nao encontrado"}
            if batch.status not in from_statuses:
                return {"error": f"Batch esta '{batch.status}', nao pode mudar para '{new_status}'"}
            batch.status = new_status
            db.commit()
            return {"success": True, "status": new_status}
        finally:
            db.close()

    # ─── Upload Processing ──────────────────────────────────

    def process_next_upload(self):
        """Called periodically by APScheduler. Finds and uploads the next pending item."""
        from backend.models.youtube_upload import YouTubeUploadBatch, YouTubeUploadItem

        db = SessionLocal()
        try:
            # Find next item: pending, scheduled_time <= now, batch is active
            item = (
                db.query(YouTubeUploadItem)
                .join(YouTubeUploadBatch)
                .filter(
                    YouTubeUploadBatch.status == "active",
                    YouTubeUploadItem.status == "pending",
                    YouTubeUploadItem.scheduled_time <= datetime.utcnow(),
                )
                .order_by(YouTubeUploadItem.scheduled_time.asc())
                .first()
            )

            if not item:
                return  # Nothing to process

            batch = item.batch
            logger.info(f"Processing upload: {item.filename} (batch {batch.id})")

            # Mark as uploading
            item.status = "uploading"
            db.commit()

            # Perform upload
            result = self._upload_video(item, batch)

            if result.get("success"):
                item.status = "uploaded"
                item.youtube_video_id = result.get("video_id", "")
                item.youtube_url = f"https://www.youtube.com/watch?v={item.youtube_video_id}"
                item.uploaded_at = datetime.utcnow()
                item.error_message = ""
                batch.uploaded_count += 1
                logger.info(f"Upload success: {item.filename} -> {item.youtube_url}")
            else:
                item.retry_count += 1
                error_msg = result.get("error", "Unknown error")
                item.error_message = error_msg
                if item.retry_count >= item.max_retries:
                    item.status = "failed"
                    batch.failed_count += 1
                    logger.error(f"Upload failed (max retries): {item.filename} - {error_msg}")
                else:
                    item.status = "pending"  # Will retry on next cycle
                    logger.warning(f"Upload failed (retry {item.retry_count}): {item.filename} - {error_msg}")

            # Check if batch is complete
            pending = db.query(YouTubeUploadItem).filter(
                YouTubeUploadItem.batch_id == batch.id,
                YouTubeUploadItem.status.in_(["pending", "uploading"]),
            ).count()
            if pending == 0:
                batch.status = "completed"
                logger.info(f"Batch {batch.id} completed: {batch.uploaded_count} uploaded, {batch.failed_count} failed")

            db.commit()

        except Exception as e:
            logger.error(f"Error in process_next_upload: {e}")
            db.rollback()
        finally:
            db.close()

    def _upload_video(self, item, batch) -> dict:
        """Upload a single video to YouTube using the Data API v3."""
        try:
            youtube = self._build_youtube_service()
        except Exception as e:
            return {"success": False, "error": f"Autenticacao falhou: {e}"}

        # Verify file exists
        file_path = Path(item.file_path)
        if not file_path.exists():
            return {"success": False, "error": f"Arquivo nao encontrado: {item.file_path}"}

        body = self._build_video_body(item, batch)

        try:
            from googleapiclient.http import MediaFileUpload

            media = MediaFileUpload(
                str(file_path),
                mimetype="video/*",
                resumable=True,
                chunksize=10 * 1024 * 1024,  # 10MB chunks
            )

            request = youtube.videos().insert(
                part="snippet,status",
                body=body,
                media_body=media,
            )

            # Resumable upload with retry
            response = None
            retry = 0
            while response is None:
                try:
                    status, response = request.next_chunk()
                    if status:
                        progress = int(status.progress() * 100)
                        logger.info(f"Upload {item.filename}: {progress}%")
                except Exception as e:
                    error_str = str(e)
                    if hasattr(e, "resp") and e.resp.status in RETRIABLE_STATUS_CODES:
                        retry += 1
                        if retry > MAX_RETRIES:
                            return {"success": False, "error": f"Max retries exceeded: {error_str}"}
                        sleep_time = random.uniform(1, 2 ** retry)
                        logger.warning(f"Retriable error, sleeping {sleep_time:.1f}s: {error_str}")
                        time.sleep(sleep_time)
                    else:
                        return {"success": False, "error": error_str[:500]}

            video_id = response.get("id", "")
            return {"success": True, "video_id": video_id}

        except Exception as e:
            return {"success": False, "error": str(e)[:500]}

    def _build_video_body(self, item, batch) -> dict:
        """Build the video resource body for YouTube API."""
        title = item.title or Path(item.filename).stem
        description = item.description or batch.default_description
        tags_str = item.tags or batch.default_tags
        tags = [t.strip() for t in tags_str.split(",") if t.strip()] if tags_str else []
        category_id = item.category_id or batch.default_category_id or "22"

        # Truncate title to YouTube limit (100 chars)
        if len(title) > 100:
            title = title[:97] + "..."

        body = {
            "snippet": {
                "title": title,
                "description": description,
                "tags": tags,
                "categoryId": category_id,
            },
            "status": {},
        }

        if batch.use_publish_at and item.scheduled_time:
            # Upload as private with publishAt -> YouTube auto-publishes at that time
            body["status"]["privacyStatus"] = "private"
            body["status"]["publishAt"] = item.scheduled_time.strftime("%Y-%m-%dT%H:%M:%S.000Z")
        else:
            # Upload with the configured privacy
            body["status"]["privacyStatus"] = batch.default_privacy or "private"

        return body

    # ─── YouTube Categories ─────────────────────────────────

    def get_categories(self, region: str = "BR") -> list[dict]:
        """Fetch YouTube video categories for a region."""
        try:
            youtube = self._build_youtube_service()
            resp = youtube.videoCategories().list(
                part="snippet",
                regionCode=region,
            ).execute()
            return [
                {"id": c["id"], "title": c["snippet"]["title"]}
                for c in resp.get("items", [])
                if c["snippet"].get("assignable")
            ]
        except Exception as e:
            logger.error(f"Error fetching categories: {e}")
            # Return common defaults
            return [
                {"id": "1", "title": "Film & Animation"},
                {"id": "2", "title": "Autos & Vehicles"},
                {"id": "10", "title": "Music"},
                {"id": "15", "title": "Pets & Animals"},
                {"id": "17", "title": "Sports"},
                {"id": "20", "title": "Gaming"},
                {"id": "22", "title": "People & Blogs"},
                {"id": "23", "title": "Comedy"},
                {"id": "24", "title": "Entertainment"},
                {"id": "25", "title": "News & Politics"},
                {"id": "26", "title": "Howto & Style"},
                {"id": "27", "title": "Education"},
                {"id": "28", "title": "Science & Technology"},
            ]

    # ─── Helpers ────────────────────────────────────────────

    @staticmethod
    def _format_size(size_bytes: int) -> str:
        if size_bytes == 0:
            return "0 B"
        units = ["B", "KB", "MB", "GB", "TB"]
        i = 0
        size = float(size_bytes)
        while size >= 1024 and i < len(units) - 1:
            size /= 1024
            i += 1
        return f"{size:.1f} {units[i]}"


# Singleton
youtube_upload_service = YouTubeUploadService()
