import os
import time
import threading
import random
import subprocess
import pickle
import sqlite3
import re
from datetime import datetime, timedelta, timezone
from flask import Flask, request, jsonify, render_template_string
from flask_cors import CORS
import cv2
from pydub import AudioSegment
from pydub.silence import detect_nonsilent
import numpy as np
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
import pytz
from werkzeug.utils import secure_filename
import math  # Importar math para funções trigonométricas usadas abaixo

# CONF DIRS, PATHS AND GLOBAL VARS

DB_PATH = r"C:\Users\jonathan\Desktop\FINANCE CHANEEL\monitorador_juntador.db"
UPLOAD_FOLDER = r"C:\Users\jonathan\Desktop\AUTLAND\CREDENTIALS_UPLOAD"
ALLOWED_EXTENSIONS = {'pickle', 'pkl'}

DEFAULT_DESCRIPTION = """
In this video, we reveal the shocking truth behind Warren Buffett’s boldest move ever — and what it could mean for the future of the market. Is a crash on the horizon? Don’t miss these crucial insights!
⏱️ Chapters
0:00 - Introduction
0:45 - What Just Happened
2:10 - Deep Dive into the Situation
3:00 - Final Thoughts and Predictions
8:45 - Outro

📢 Subscribe now for more eye-opening financial insights!
#finance #investing #stockmarket #breakingnews #wealth
"""
DEFAULT_TAGS = [
    "Warren Buffett sells biggest stock",
    "Buffett stock sale 2025",
    "Wall Street reaction Buffett",
    "Warren Buffett stock market news",
    "Buffett biggest investment sold",
    "Buffett stock dump explained",
    "Warren Buffett investing",
    "stock market news 2025",
    "Buffett sell off impact",
    "Warren Buffett market strategy",
    "investment advice Buffett",
    "Buffett latest moves",
    "Wall Street freak out Buffett",
    "trump",
    "trump tarrifs"
]

PASTA_CATEGORIAS = r"C:\Users\jonathan\Desktop\AUTLAND\CATEGORIAS DE IMAGENS VIDEO\CARL JUNG"
AUDIO_FUNDO = r"C:\Users\jonathan\Desktop\AUTLAND\TRILHA SONORA\soundfund.mp3"
PASTA_VIDEOS_POSTAGEM = r"C:\Users\jonathan\Desktop\AUTLAND\POSTAGEM\VIDEO CARL JUNG"
PASTA_THUMBNAILS = r"C:\Users\jonathan\Desktop\AUTLAND\THUMBNAILS\TUMB CARL"
PASTA_AUDIOS_CARL = r"C:\Users\jonathan\Desktop\AUTLAND\AUDIOS\AUDIOS CARL JUNG"

lock_editing_process = threading.Lock()
stop_requested = False

progress_editor = {
    "percent": 0,
    "eta": "--:--",
    "message": "Nenhuma ação iniciada."
}

progress_upload = {
    "percent": 0,
    "eta": "--:--",
    "message": "Nenhuma ação iniciada."
}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
CORS(app)

if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)


# Check allowed credential file
def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


# Initialize DB with tables if not exists
def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
    CREATE TABLE IF NOT EXISTS videos (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        filename TEXT UNIQUE,
        title TEXT,
        channel TEXT,
        schedule_time TEXT
    )
    ''')
    c.execute('''
    CREATE TABLE IF NOT EXISTS perfis (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nome TEXT UNIQUE,
        credentials BLOB,
        canal TEXT
    )
    ''')
    conn.commit()
    conn.close()


# Get profiles for dropdowns, etc.
def get_perfis():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT id, nome, canal, credentials FROM perfis")
    perfis = c.fetchall()
    conn.close()
    return perfis


def get_canal_por_perfil(nome):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT canal FROM perfis WHERE nome=?", (nome,))
    row = c.fetchone()
    conn.close()
    return row[0] if row else None


def get_title_from_db(filename):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT title FROM videos WHERE filename=?", (filename,))
    row = c.fetchone()
    conn.close()
    return row[0] if row else None


def set_video_entry(filename, title, channel, schedule_time):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
    INSERT OR REPLACE INTO videos (filename, title, channel, schedule_time)
    VALUES (?, ?, ?, ?)
    ''', (filename, title, channel, schedule_time))
    conn.commit()
    conn.close()


def load_credentials_from_blob(blob):
    filename_temp = "temp_creds.pickle"
    with open(filename_temp, "wb") as f:
        f.write(blob)
    with open(filename_temp, "rb") as f:
        creds = pickle.load(f)
    os.remove(filename_temp)
    return creds


def upload_video_to_youtube(credentials_blob, title, description, tags, video_path, thumbnail_path,
                            publish_now, publish_at=None, category_id="22"):
    creds = load_credentials_from_blob(credentials_blob)
    youtube = build("youtube", "v3", credentials=creds)

    if not os.path.exists(video_path):
        raise FileNotFoundError(f"Arquivo não encontrado: {video_path}")

    if publish_now:
        privacy_status = "public"
        publish_at_utc = None
    else:
        if not publish_at:
            raise ValueError("publishAt deve ser informado para agendamento")
        dt_local = datetime.fromisoformat(publish_at)
        now_local = datetime.now(dt_local.tzinfo)
        if dt_local <= now_local:
            raise ValueError(f"publishAt ({dt_local.isoformat()}) deve ser no futuro em relação a agora ({now_local.isoformat()})")
        publish_at_utc = dt_local.astimezone(timezone.utc).isoformat()
        privacy_status = "private"

    body = {
        "snippet": {
            "title": title,
            "description": description,
            "tags": tags,
            "categoryId": category_id
        },
        "status": {
            "privacyStatus": privacy_status,
            "publishAt": publish_at_utc
        }
    }

    media = MediaFileUpload(video_path, mimetype="video/mp4", resumable=True, chunksize=10 * 1024 * 1024)

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

    response = None
    while response is None:
        status, response = request.next_chunk()
        if status:
            print(f"Upload {int(status.progress() * 100)}%")

    if thumbnail_path and os.path.exists(thumbnail_path):
        youtube.thumbnails().set(
            videoId=response["id"],
            media_body=MediaFileUpload(thumbnail_path)
        ).execute()

    print(f"[INFO] Upload finalizado, videoId: {response['id']}")

    # Apagar o vídeo APÓS envio, por segurança, só se estiver na pasta correta
    try:
        if os.path.exists(video_path) and os.path.commonprefix([os.path.abspath(video_path), os.path.abspath(PASTA_VIDEOS_POSTAGEM)]) == os.path.abspath(PASTA_VIDEOS_POSTAGEM):
            os.remove(video_path)
            print(f"Vídeo removido: {video_path}")
    except Exception as e:
        print(f"Erro ao remover vídeo: {e}")
    try:
        if thumbnail_path and os.path.exists(thumbnail_path) and os.path.commonprefix([os.path.abspath(thumbnail_path), os.path.abspath(PASTA_THUMBNAILS)]) == os.path.abspath(PASTA_THUMBNAILS):
            os.remove(thumbnail_path)
            print(f"Thumbnail removida: {thumbnail_path}")
    except Exception as e:
        print(f"Erro ao remover thumbnail: {e}")

    return response["id"]


def get_scheduled_times_for_channel(channel):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT schedule_time FROM videos WHERE channel=?", (channel,))
    horarios = [row[0] for row in c.fetchall()]
    conn.close()
    return horarios


def get_next_available_time(profile_name, start_dt=None, num_slots=1):
    canal = get_canal_por_perfil(profile_name)
    horarios_ocupados = get_scheduled_times_for_channel(canal)

    br_tz = pytz.timezone("America/Sao_Paulo")
    agora = datetime.now(br_tz)
    if start_dt:
        if isinstance(start_dt, str):
            try:
                dt = datetime.fromisoformat(start_dt)
                if dt.tzinfo is None:
                    dt = br_tz.localize(dt)
                start_dt = dt
            except Exception:
                start_dt = None
        elif isinstance(start_dt, datetime):
            if start_dt.tzinfo is None:
                start_dt = br_tz.localize(start_dt)
    if start_dt is None or start_dt <= agora:
        start_dt = agora + timedelta(minutes=1)
        start_dt = start_dt.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)

    parsed_ocupados = []
    for h in horarios_ocupados:
        try:
            dt = datetime.fromisoformat(h)
            if dt.tzinfo is None:
                dt = br_tz.localize(dt)
            parsed_ocupados.append(dt)
        except Exception:
            continue

    scheduled_slots = []
    candidate_time = start_dt
    limit_date = agora + timedelta(days=30)

    while len(scheduled_slots) < num_slots and candidate_time <= limit_date:
        conflict = False
        for ocupado in parsed_ocupados:
            if abs((candidate_time - ocupado).total_seconds()) < 60:
                conflict = True
                break
        for scheduled in scheduled_slots:
            if abs((candidate_time - scheduled).total_seconds()) < 60:
                conflict = True
                break
        if not conflict:
            scheduled_slots.append(candidate_time)
            candidate_time += timedelta(hours=1)
        else:
            candidate_time += timedelta(hours=1)

    if len(scheduled_slots) < num_slots:
        raise Exception("Não foi possível agendar todos os vídeos nos próximos 30 dias devido a conflitos de horário.")

    return [slot.isoformat() for slot in scheduled_slots]


def remover_silencio(audio_segment, silence_thresh=-50, min_silence_len=500, padding=300):
    nao_silencioso = detect_nonsilent(audio_segment, min_silence_len=min_silence_len, silence_thresh=silence_thresh)
    if not nao_silencioso:
        return audio_segment
    partes = []
    for start, end in nao_silencioso:
        start_pad = max(0, start - padding)
        end_pad = min(len(audio_segment), end + padding)
        partes.append(audio_segment[start_pad:end_pad])
    audio_sem_silencio = partes[0]
    for part in partes[1:]:
        audio_sem_silencio += AudioSegment.silent(duration=150) + part
    return audio_sem_silencio


def load_images_from_folder(folder):
    images = []
    for filename in os.listdir(folder):
        if filename.lower().endswith((".png", ".jpg", ".jpeg")):
            path = os.path.join(folder, filename)
            img = cv2.imread(path)
            if img is not None and img.shape[0] > 0 and img.shape[1] > 0:
                images.append(img)
    random.shuffle(images)
    return images


def get_beat_times(audio_path, frame_ms=50, energy_threshold_factor=1.3):
    audio = AudioSegment.from_file(audio_path).set_channels(1)
    frame_length = frame_ms
    n_frames = len(audio) // frame_length
    energies = []
    for i in range(n_frames):
        frame = audio[i * frame_length: (i + 1) * frame_length]
        energies.append(frame.rms)
    energies = np.array(energies)
    avg_energy = np.mean(energies)
    beat_indices = []
    for i in range(1, len(energies) - 1):
        if energies[i] > energy_threshold_factor * avg_energy and energies[i] > energies[i - 1] and energies[i] > energies[i + 1]:
            beat_indices.append(i)
    beat_times = [i * frame_length / 1000.0 for i in beat_indices]
    filtered_beats = []
    min_separation = 1.0
    for t in beat_times:
        if len(filtered_beats) == 0 or (t - filtered_beats[-1]) > min_separation:
            filtered_beats.append(t)
    return filtered_beats


# Função para gerar translação suave e aleatória nas imagens — movimentação leve para evitar imagem estática
def get_smooth_random_translation(frame_index, total_frames, max_translation=20, cycle_frames=180):
    # cycle_frames define a full movement cycle to and fro (e.g. 6 seconds at 30fps)
    progress_in_cycle = frame_index % cycle_frames
    half_cycle = cycle_frames // 2

    # Movimento do tipo vai e volta para x e y, usando função sin para suavizar
    import math
    angle = (progress_in_cycle / cycle_frames) * 2 * math.pi
    offset_x = int(max_translation * math.sin(angle))
    offset_y = int(max_translation * math.sin(angle * 0.7))  # 0.7 para diferenciar movimento nos eixos

    return offset_x, offset_y


# Função que aplica um zoom suave (feito junto da tradução para o efeito mais natural)
def apply_zoom_and_translation(image, zoom_factor, offset_x, offset_y):
    height, width = image.shape[:2]
    # Zoom centralizado
    new_width = int(width / zoom_factor)
    new_height = int(height / zoom_factor)

    x_center = width // 2 - offset_x
    y_center = height // 2 - offset_y

    # Calcula coordenadas de recorte garantindo limites válidos
    x1 = max(0, x_center - new_width // 2)
    y1 = max(0, y_center - new_height // 2)
    x2 = min(width, x_center + new_width // 2)
    y2 = min(height, y_center + new_height // 2)

    cropped = image[y1:y2, x1:x2]

    # Corrige o tamanho caso o recorte tenha sido bizarramente pequeno por causa dos offsets
    if cropped.shape[0] <= 0 or cropped.shape[1] <= 0:
        cropped = image

    resized = cv2.resize(cropped, (width, height), interpolation=cv2.INTER_LINEAR)
    return resized


def create_video_only(video_path, duration, images, fps=30, progress_callback=None, change_times=None):
    if not images:
        raise ValueError("Lista de imagens vazia.")
    height, width, _ = images[0].shape
    total_frames = int(duration * fps)
    out = cv2.VideoWriter(video_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height))
    img_index = 0

    efeitos = {
        "zoom": 0.15,
        "move": 0.15,
        "ken_burns": 0.15,
        "fast_zoom_blur": 0.10,
        "morph_transition": 0.10,
        "slide": 0.10,
        "gallery_slide": 0.10,
        "static": 0.15,
    }
    efeito_usado_count = {key: 0 for key in efeitos.keys()}

    morph_duration_frames = int(fps * 0.5)
    rapid_change_count = 0
    rapid_change_max = 7
    rapid_change_interval_frames = fps

    frames_per_image = int(3 * fps)

    def random_interval_frames():
        return int(random.uniform(2.5, 4.0) * fps)

    if change_times is None or len(change_times) == 0:
        frames_per_image = random_interval_frames()
        next_change_frame = frames_per_image
    else:
        change_frames = [int(t * fps) for t in change_times if t * fps < total_frames]
        if change_frames[-1] < total_frames - 1:
            change_frames.append(total_frames - 1)
        change_index = 0
        next_change_frame = change_frames[change_index]

    start_time = time.time()

    for i in range(total_frames):
        if rapid_change_count < rapid_change_max:
            if i % rapid_change_interval_frames == 0 and i != 0:
                img_index = (img_index + 1) % len(images)
                rapid_change_count += 1
        else:
            if change_times is None or len(change_times) == 0:
                if i >= next_change_frame:
                    img_index = (img_index + 1) % len(images)
                    frames_per_image = random_interval_frames()
                    next_change_frame = i + frames_per_image
            else:
                if i >= next_change_frame:
                    img_index = (img_index + 1) % len(images)
                    change_index += 1
                    if change_index < len(change_frames):
                        next_change_frame = change_frames[change_index]
                    else:
                        next_change_frame = total_frames + 1
        img = images[img_index]
        if change_times is not None and len(change_times) > 0 and i <= next_change_frame:
            prev_frame_change = change_frames[change_index - 1] if change_index > 0 else 0
            frame_in_image = i - prev_frame_change
            total_frames_for_image = next_change_frame - prev_frame_change
        else:
            total_frames_for_image = frames_per_image
            frame_in_image = i % frames_per_image
        progress = frame_in_image / total_frames_for_image if total_frames_for_image > 0 else 0
        progress = min(max(progress, 0), 1)

        # ------- Aplicar movimentação suave nesta imagem --------
        # Movimento de translação suave baseado no índice do frame atual
        offset_x, offset_y = get_smooth_random_translation(i, total_frames, max_translation=15)

        # Leve zoom para dar sensação de movimento natural suave
        zoom_base = 1.03  # zoom para aumentar levemente
        # Zoom varia lentamente entre 1.00 e zoom_base na metade do ciclo (ou seja zoom in e zoom out)
        import math
        cycle_len = 180  # frames
        zoom_phase = (i % cycle_len) / cycle_len
        if zoom_phase < 0.5:
            zoom_factor = 1 + (zoom_base - 1) * (zoom_phase * 2)  # zoom in
        else:
            zoom_factor = zoom_base - (zoom_base - 1) * ((zoom_phase - 0.5) * 2)  # zoom out

        # Aplicar zoom + translacao suavemente
        frame = apply_zoom_and_translation(img, zoom_factor, offset_x, offset_y)

        # Redimensionar para garantir tamanho correto
        frame = cv2.resize(frame, (width, height))

        # Escrever frame no video
        out.write(frame)

        if progress_callback:
            elapsed = time.time() - start_time
            frames_done = i + 1
            frames_left = total_frames - frames_done
            eta = (elapsed / frames_done) * frames_left if frames_done > 0 else 0
            progress_callback(frames_done, total_frames, eta)
    out.release()


def merge_audio_with_video(video_path, audio_path, output_path):
    audio1 = AudioSegment.from_file(audio_path).set_channels(2)
    audio2 = AudioSegment.from_file(AUDIO_FUNDO).set_channels(2)
    audio2 = audio2 - 1
    if len(audio2) < len(audio1):
        audio2 = audio2 * (len(audio1) // len(audio2) + 1)
    audio2 = audio2[:len(audio1)]
    combined = audio1.overlay(audio2)
    temp_audio_path = "combined_temp_audio.mp3"
    combined.export(temp_audio_path, format="mp3")
    command = [
        "ffmpeg",
        "-y",
        "-i", video_path,
        "-i", temp_audio_path,
        "-c:v", "copy",
        "-c:a", "aac",
        "-shortest",
        output_path
    ]
    subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    os.remove(temp_audio_path)


def create_video_with_audio(audio_path, progress_callback=None):
    try:
        audio = AudioSegment.from_file(audio_path)
    except Exception as e:
        print(f"[ERRO] Falha ao carregar áudio: {e}")
        return
    silence_threshold = audio.dBFS - 14
    audio_sem_silencio = remover_silencio(audio, silence_thresh=silence_threshold, min_silence_len=400, padding=300)
    temp_audio_no_silencio = "temp_audio_no_silencio.mp3"
    audio_sem_silencio.export(temp_audio_no_silencio, format="mp3")
    duration_seconds = audio_sem_silencio.duration_seconds
    audio_name = os.path.splitext(os.path.basename(audio_path))[0]
    temp_video_path = f"{audio_name}_temp.mp4"
    final_video_path = os.path.join(PASTA_VIDEOS_POSTAGEM, f"{audio_name}.mp4")

    images = load_images_from_folder(PASTA_CATEGORIAS)
    if not images:
        print("[ERRO] Nenhuma imagem válida encontrada na pasta fixa CARL JUNG.")
        return
    try:
        change_times = get_beat_times(temp_audio_no_silencio)

        def internal_progress(current_frame, total_frames, eta):
            if progress_callback:
                percent = int((current_frame / total_frames) * 100)
                eta_str = time.strftime("%M:%S", time.gmtime(max(0, eta)))
                progress_callback(percent, eta_str)

        create_video_only(temp_video_path, duration_seconds, images, fps=30,
                          progress_callback=internal_progress, change_times=change_times)
        merge_audio_with_video(temp_video_path, temp_audio_no_silencio, final_video_path)
        # APAGAR arquivo temporário de vídeo antes de export final_video_path não era removido porque era vídeo final, logo não apagar aqui.
        os.remove(temp_video_path)
        os.remove(temp_audio_no_silencio)
        if progress_callback:
            progress_callback(100, "00:00")
    except Exception as e:
        print(f"[ERRO FATAL] Falha ao criar vídeo: {e}")


def create_video_with_audio_multiple_custom(delete_audio_after=True, stop_when_done=True):
    global lock_editing_process, stop_requested
    with lock_editing_process:
        audios_all = [f for f in os.listdir(PASTA_AUDIOS_CARL) if f.lower().endswith((".mp3", ".wav", ".m4a", ".flac"))]
        total = len(audios_all)
        completed = 0
        if total == 0:
            print("[INFO] Não há áudios para processar.")
            progress_editor["percent"] = 100
            progress_editor["eta"] = "00:00"
            progress_editor["message"] = "Nenhum áudio para processar."
            return
        while audios_all:
            if stop_requested:
                print("[INFO] Solicitação de parada recebida. Finalizando após o áudio atual.")
                break
            audio_file = random.choice(audios_all)
            audio_path = os.path.join(PASTA_AUDIOS_CARL, audio_file)
            progress_editor["message"] = f"Processando áudio: {audio_file} ({completed+1}/{total})"
            progress_editor["percent"] = int((completed / total) * 100)
            try:
                create_video_with_audio(audio_path, progress_callback=None)
                if delete_audio_after:
                    try:
                        os.remove(audio_path)
                        print(f"[INFO] Áudio removido: {audio_file}")
                    except Exception as e:
                        print(f"[WARN] Não foi possível remover áudio {audio_file}: {e}")
                completed += 1
                audios_all = [f for f in os.listdir(PASTA_AUDIOS_CARL) if f.lower().endswith((".mp3", ".wav", ".m4a", ".flac"))]
            except Exception as e:
                print(f"[ERRO] Falha ao processar áudio {audio_file}: {e}")
                audios_all.remove(audio_file)
                completed += 1
        progress_editor["percent"] = 100
        progress_editor["eta"] = "00:00"
        progress_editor["message"] = "Processamento concluído para todos os áudios."
        if stop_when_done:
            stop_requested = False


def count_video_files():
    if not os.path.isdir(PASTA_VIDEOS_POSTAGEM):
        return 0
    return len([f for f in os.listdir(PASTA_VIDEOS_POSTAGEM) if f.lower().endswith(".mp4")])


def count_thumbnail_files():
    if not os.path.isdir(PASTA_THUMBNAILS):
        return 0
    return len([f for f in os.listdir(PASTA_THUMBNAILS) if f.lower().endswith((".jpg", ".jpeg", ".png"))])


def count_audio_files():
    if not os.path.isdir(PASTA_AUDIOS_CARL):
        return 0
    return len([f for f in os.listdir(PASTA_AUDIOS_CARL) if f.lower().endswith((".mp3", ".wav", ".m4a", ".flac"))])

@app.route('/')
def home():
    perfis_list = get_perfis()
    options_html = ""
    has_missing_creds = False
    for p in perfis_list:
        id_, nome, canal, credentials = p
        missing_text = "" if credentials else " (Credenciais Ausentes)"
        if not credentials:
            has_missing_creds = True
        options_html += f'<option value="{nome}">{nome}{missing_text}</option>'
    warning_creds_display = 'block' if has_missing_creds else 'none'

    html = f"""
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Carl Jung Automation</title>
<style>
    /* Reset and base */
    * {{
        box-sizing: border-box;
    }}
    body {{
        margin: 0;
        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
        background: #1a120b;
        color: #f3d06e;
        min-height: 100vh;
        display: flex;
        flex-direction: column;
        align-items: center;
        padding: 20px;
    }}
    .container {{
        width: 100%;
        max-width: 1200px;
    }}
    h1 {{
        font-weight: 700;
        font-size: 32px;
        margin-bottom: 12px;
        color: #f3d06e;
        text-align: center;
        letter-spacing: 2px;
    }}
    h2 {{
        margin-bottom: 12px;
        color: #f3d06e;
        border-bottom: 2px solid #f3d06e;
        padding-bottom: 6px;
    }}
    p {{
        line-height: 1.4;
        margin-bottom: 18px;
        color: #e6c55d;
    }}
    button {{
        background-color: #f3d06e;
        border: none;
        padding: 12px 20px;
        font-size: 16px;
        font-weight: 600;
        color: #1a120b;
        cursor: pointer;
        border-radius: 4px;
        transition: background-color 0.3s ease;
        width: 100%;
        max-width: 300px;
        margin-top: 12px;
    }}
    button:hover {{
        background-color: #e6c55d;
    }}
    input[type="text"], input[type="number"], input[type="date"], input[type="time"], textarea, select {{
        width: 100%;
        padding: 8px 12px;
        border-radius: 4px;
        border: 1px solid #f3d06e;
        background: #2c2115;
        color: #f3d06e;
        font-size: 14px;
        margin-bottom: 16px;
        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
        transition: border-color 0.3s ease, box-shadow 0.3s ease;
    }}
    input[type="text"]:focus, input[type="number"]:focus, input[type="date"]:focus, input[type="time"]:focus, textarea:focus, select:focus {{
        outline: none;
        border-color: #e6c55d;
        box-shadow: 0 0 6px #e6c55d;
    }}
    textarea {{
        resize: vertical;
        min-height: 100px;
        font-family: inherit;
    }}
    label {{
        font-weight: 600;
        display: block;
        margin-bottom: 4px;
        font-size: 14px;
        letter-spacing: 0.5px;
        color: #f3d06e;
    }}
    .card {{
        background: #2c2115;
        border-radius: 8px;
        padding: 20px;
        margin-bottom: 32px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.8);
    }}
    .progress-container {{
        background: #533e22;
        border-radius: 20px;
        height: 30px;
        margin: 10px 0 20px 0;
        width: 100%;
        max-width: 100%;
        overflow: hidden;
        box-shadow: inset 0 1px 3px rgba(255,255,255,0.2);
    }}
    .progress-bar {{
        height: 100%;
        background: linear-gradient(90deg, #f3d06e 0%, #e6c55d 100%);
        width: 0%;
        line-height: 30px;
        color: #1a120b;
        font-weight: 700;
        text-align: center;
        transition: width 0.4s ease;
        user-select: none;
        border-radius: 20px 0 0 20px;
    }}
    .progress-bar.complete {{
        border-radius: 20px;
    }}
    .flex-row {{
        display: flex;
        flex-wrap: wrap;
        gap: 15px;
        justify-content: space-between;
    }}
    .flex-col {{
        display: flex;
        flex-direction: column;
        flex: 1;
        min-width: 250px;
    }}
    small.warning {{
        color: #d9a602;
        display: {warning_creds_display};
        font-weight: 600;
        margin-bottom: 15px;
        letter-spacing: 0.8px;
    }}
    footer {{
        text-align: center;
        margin-top: 40px;
        padding: 10px;
        font-size: 14px;
        color: #aa985c;
    }}
    ::-webkit-scrollbar {{
        width: 8px;
        height: 8px;
    }}
    ::-webkit-scrollbar-track {{
        background: #2c2115;
        border-radius: 4px;
    }}
    ::-webkit-scrollbar-thumb {{
        background-color: #f3d06e;
        border-radius: 4px;
    }}
    .file-upload-section {{
        margin-top: 20px;
        background: #3e2e16;
        padding: 15px;
        border-radius: 8px;
        max-width: 400px;
    }}
    .file-upload-section input[type="file"] {{
        color: #333;
        padding: 6px 0;
    }}
    .file-upload-btn {{
        margin-top: 12px;
        background-color: #f3d06e;
        border-radius: 4px;
        color: #1a120b;
        padding: 10px 15px;
        font-weight: 600;
        border: none;
        cursor: pointer;
        width: 100%;
        transition: background-color 0.3s ease;
    }}
    .file-upload-btn:hover {{
        background-color: #e6c55d;
    }}
    @media (max-width: 768px) {{
        .flex-row {{
            flex-direction: column;
        }}
        button, input, select, textarea {{
            max-width: 100%;
        }}
    }}
</style>
<script>
function updateProgressEditing() {{
    fetch('/progress_editing')
    .then(response => response.json())
    .then(data => {{
        let bar = document.getElementById('progressEditingBar');
        bar.style.width = data.percent + '%';
        bar.textContent = data.percent + '%';
        if(data.percent === 100) bar.classList.add('complete');
        else bar.classList.remove('complete');
        document.getElementById('progressEditingMessage').textContent = data.message;
        document.getElementById('progressEditingEta').textContent = data.eta;
    }});
}}

function updateProgressUpload() {{
    fetch('/progress_upload')
    .then(response => response.json())
    .then(data => {{
        let bar = document.getElementById('progressUploadBar');
        bar.style.width = data.percent + '%';
        bar.textContent = data.percent + '%';
        if(data.percent === 100) bar.classList.add('complete');
        else bar.classList.remove('complete');
        document.getElementById('uploadStatus').textContent = data.message;
        document.getElementById('progressUploadEta').textContent = data.eta;
    }});
}}

function updateFileCounts() {{
    fetch('/file_counts_custom')
    .then(response => response.json())
    .then(data => {{
        document.getElementById('countAudios').textContent = data.audios;
        document.getElementById('countVideosUpload').textContent = data.videos_postagem;
        document.getElementById('countThumbnailsUpload').textContent = data.thumbs_carl;
        document.getElementById('countRealUpload').textContent = data.real_number;
    }});
}}

setInterval(() => {{
    updateProgressEditing();
    updateProgressUpload();
    updateFileCounts();
}}, 2500);

function startEditingContinuous() {{
    fetch('/start_editing_continuous', {{method: 'POST'}})
    .then(res => res.json())
    .then(data => alert(data.message));
}}

function stopEditing() {{
    fetch('/stop_editing', {{method: 'POST'}})
    .then(res => res.json())
    .then(data => alert(data.message));
}}

function startUpload() {{
    let profile_nome = document.getElementById('perfilSelect').value;
    let description = document.getElementById('descInput').value;
    let tags_str = document.getElementById('tagsInput').value;
    let tags = tags_str.split(',').map(s => s.trim()).filter(s => s !== "");
    let qtd_videos = parseInt(document.getElementById('numVideos').value);
    let data_str = document.getElementById('dateUpload').value;
    let hora_str = document.getElementById('timeUpload').value;
    if (!profile_nome) {{
        alert('Selecione um perfil.');
        return;
    }}
    if (!data_str) {{
        alert('Informe a data do upload/agendamento.');
        return;
    }}
    if (!hora_str) {{
        alert('Informe a hora do upload/agendamento.');
        return;
    }}
    fetch('/start_upload', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify({{
            profile_nome: profile_nome,
            description: description,
            tags: tags,
            qtd_videos: qtd_videos,
            data: data_str,
            hora: hora_str
        }})
    }}).then(res => res.json())
    .then(data => alert(data.message));
}}

function createProfile() {{
    let nome = prompt('Digite o nome do novo perfil:').trim();
    if (!nome) {{
        alert('Nome inválido.');
        return;
    }}
    let canal = prompt('Digite o nome do canal do YouTube para o perfil:').trim();
    if (!canal) {{
        alert('Canal inválido.');
        return;
    }}
    fetch('/create_profile', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify({{nome: nome, canal: canal}})
    }}).then(res => res.json())
    .then(data => {{
        alert(data.message);
        if (data.success) {{
            location.reload();
        }}
    }});
}}

function handleCredentialUpload() {{
    let profile = document.getElementById('profileCredentialSelect').value;
    let fileInput = document.getElementById('credentialFile');
    if (!profile) {{
        alert('Selecione um perfil para associar a credencial.');
        return;
    }}
    if (fileInput.files.length === 0) {{
        alert('Selecione um arquivo de credencial para enviar.');
        return;
    }}
    let file = fileInput.files[0];
    let allowedExtensions = ['pickle', 'pkl'];
    let ext = file.name.split('.').pop().toLowerCase();
    if (!allowedExtensions.includes(ext)) {{
        alert('Extensão de arquivo inválida. Use arquivos .pickle ou .pkl');
        return;
    }}

    let formData = new FormData();
    formData.append('profile_nome', profile);
    formData.append('credential_file', file);

    fetch('/upload_credentials', {{
        method: 'POST',
        body: formData
    }})
    .then(res => res.json())
    .then(data => {{
        alert(data.message);
        if (data.success) location.reload();
    }});
}}
</script>
</head>
<body>
<div class="container">
    <h1>CARL JUNG AUTOMATION</h1>

    <div class="card">
        <h2>Processamento Contínuo</h2>
        <button onclick="startEditingContinuous()">Iniciar Processamento Contínuo</button>
        <button style="margin-top: 8px;" onclick="stopEditing()">Parar Após Terminar Atual</button>

        <div class="progress-container" aria-label="Progresso de edição">
            <div class="progress-bar" id="progressEditingBar" role="progressbar" aria-valuemin="0" aria-valuemax="100">0%</div>
        </div>
        <p id="progressEditingMessage">Nenhuma ação iniciada.</p>
        <p>ETA: <span id="progressEditingEta">--:--</span></p>
    </div>

    <div class="card">
        <h2>Estatísticas em Tempo Real</h2>
        <p>Áudios Pendentes: <span id="countAudios">0</span></p>
        <p>Vídeos (Pasta Postagem): <span id="countVideosUpload">0</span></p>
        <p>Thumbnails Disponíveis: <span id="countThumbnailsUpload">0</span></p>
        <p>Real Number: <span id="countRealUpload">0</span></p>
    </div>

    <div class="card">
        <h2>Agendador e Upload YouTube</h2>

        <label for="perfilSelect">Selecionar Perfil</label>
        <select id="perfilSelect" class="form-select" aria-label="Selecionar Perfil para upload">
            {options_html}
        </select>
        <small class="warning" id="warningCreds">* Alguns perfis não possuem credenciais cadastradas e não funcionarão para upload.</small>

        <label for="descInput">Descrição</label>
        <textarea id="descInput" aria-label="Descrição do vídeo">{DEFAULT_DESCRIPTION.strip()}</textarea>

        <label for="tagsInput">Tags (vírgula separado)</label>
        <input type="text" id="tagsInput" value='{",".join(DEFAULT_TAGS)}' aria-label="Tags para o vídeo">

        <div class="flex-row">
            <div class="flex-col">
                <label for="numVideos">Número de vídeos</label>
                <input type="number" id="numVideos" min="1" value="1" aria-label="Número de vídeos para agendar/upload" />
            </div>
            <div class="flex-col">
                <label for="dateUpload">Data Upload/Agendamento</label>
                <input type="date" id="dateUpload" aria-label="Data para agendamento" />
            </div>
            <div class="flex-col">
                <label for="timeUpload">Hora (HH:MM)</label>
                <input type="time" id="timeUpload" value="06:00" aria-label="Hora para agendamento" />
            </div>
        </div>
        <button onclick="startUpload()">Iniciar Upload/Agendamento</button>

        <div class="progress-container" aria-label="Progresso do upload/agendamento" style="margin-top: 15px;">
            <div class="progress-bar" id="progressUploadBar" role="progressbar" aria-valuemin="0" aria-valuemax="100">0%</div>
        </div>
        <p id="uploadStatus">Nenhuma ação iniciada.</p>
        <p>ETA: <span id="progressUploadEta">--:--</span></p>
    </div>

    <div class="card">
        <h2>Gerenciar Perfis</h2>
        <button onclick="createProfile()">Criar Novo Perfil</button>
    </div>
    
    <div class="card file-upload-section" aria-label="Carregar credenciais para perfil">
        <h2>Upload de Credenciais para Perfil</h2>
        <label for="profileCredentialSelect">Selecionar Perfil</label>
        <select id="profileCredentialSelect" aria-label="Selecione um perfil para associar as credenciais">
            {options_html}
        </select>
        <input type="file" id="credentialFile" accept=".pickle,.pkl" aria-label="Selecione arquivo de credenciais (.pickle ou .pkl)">
        <button class="file-upload-btn" onclick="handleCredentialUpload()">Enviar Credenciais</button>
    </div>

    <footer>
        © 2024 Carl Jung Automation
    </footer>
</div>
</body>
</html>
"""
    return render_template_string(html)


@app.route("/file_counts_custom")
def file_counts_custom():
    audios = count_audio_files()
    videos_postagem = count_video_files()
    thumbs_carl = count_thumbnail_files()
    real_number = min(videos_postagem, thumbs_carl)
    return jsonify({"audios": audios, "videos_postagem": videos_postagem, "thumbs_carl": thumbs_carl, "real_number": real_number})


@app.route("/progress_editing")
def progress_editing_status():
    return jsonify(progress_editor)


@app.route("/progress_upload")
def progress_upload_status():
    return jsonify(progress_upload)


@app.route("/start_editing_continuous", methods=["POST"])
def start_editing_continuous():
    global stop_requested

    def editing_worker():
        create_video_with_audio_multiple_custom(delete_audio_after=True, stop_when_done=True)

    if not lock_editing_process.locked():
        thread = threading.Thread(target=editing_worker, daemon=True)
        thread.start()
        progress_editor["percent"] = 0
        progress_editor["eta"] = "--:--"
        progress_editor["message"] = "Processamento contínuo iniciado."
        stop_requested = False
        return jsonify({"message": "Processamento contínuo iniciado."})
    else:
        return jsonify({"message": "Processamento já está em execução."})


@app.route("/stop_editing", methods=["POST"])
def stop_editing():
    global stop_requested
    stop_requested = True
    progress_editor["message"] = "Parada solicitada. Terminando áudio atual antes de encerrar."
    return jsonify({"message": "Parada solicitada."})


@app.route("/perfis")
def perfis_route():
    perfis_list = get_perfis()
    perfis_json = []
    for p in perfis_list:
        id_, nome, canal, credentials = p
        has_creds = bool(credentials)
        perfis_json.append({"id": id_, "nome": nome, "canal": canal, "has_credentials": has_creds})
    return jsonify({"perfis": perfis_json})


@app.route("/start_upload", methods=["POST"])
def start_upload_api():
    data = request.json
    profile_nome = data.get("profile_nome", "")
    description = data.get("description", DEFAULT_DESCRIPTION)
    tags = data.get("tags", DEFAULT_TAGS)
    qtd_videos = data.get("qtd_videos", 1)
    data_str = data.get("data", "")
    hora_str = data.get("hora", "")

    try:
        dt_str = f"{data_str}T{hora_str}:00"
        dt_parsed = datetime.fromisoformat(dt_str)
        br_tz = pytz.timezone("America/Sao_Paulo")
        if dt_parsed.tzinfo is None:
            dt_parsed = br_tz.localize(dt_parsed)
        else:
            dt_parsed = dt_parsed.astimezone(br_tz)
        agora = datetime.now(br_tz)
        if dt_parsed <= agora:
            return jsonify({"message": "Data e hora devem ser no futuro"}), 400
    except Exception as e:
        return jsonify({"message": f"Data ou hora inválida: {e}"}), 400

    try:
        qtd_videos = int(qtd_videos)
    except:
        qtd_videos = 1

    def processar_envio_func():
        try:
            canal = get_canal_por_perfil(profile_nome)
            if not canal:
                progress_upload["message"] = "Perfil ou canal não encontrado."
                return

            videos_all = [f for f in os.listdir(PASTA_VIDEOS_POSTAGEM) if f.lower().endswith(".mp4")]
            thumbs_all = [f for f in os.listdir(PASTA_THUMBNAILS) if f.lower().endswith((".jpg", ".jpeg", ".png"))]

            if not videos_all or not thumbs_all:
                progress_upload["message"] = "Pasta de vídeos ou thumbnails está vazia."
                return

            real_number = min(len(videos_all), len(thumbs_all))
            if qtd_videos > real_number:
                progress_upload["message"] = f"Quantidade solicitada ({qtd_videos}) maior que possibilidade real ({real_number})."
                return

            selected_videos = random.sample(videos_all, qtd_videos)
            selected_thumbs = random.sample(thumbs_all, qtd_videos)

            try:
                schedule_times = get_next_available_time(profile_nome, start_dt=dt_parsed, num_slots=qtd_videos)
            except Exception as e:
                progress_upload["message"] = f"Falha ao calcular horários: {str(e)}"
                return

            enviados = 0
            total = qtd_videos

            br_tz = pytz.timezone("America/Sao_Paulo")

            conn = sqlite3.connect(DB_PATH)
            c = conn.cursor()
            c.execute("SELECT credentials FROM perfis WHERE nome=?", (profile_nome,))
            row = c.fetchone()
            conn.close()

            if not row or row[0] is None:
                progress_upload["message"] = "Credenciais não encontradas para o perfil selecionado."
                return

            credentials_blob = row[0]

            for idx in range(qtd_videos):
                video_file = os.path.join(PASTA_VIDEOS_POSTAGEM, selected_videos[idx])
                thumb_file = os.path.join(PASTA_THUMBNAILS, selected_thumbs[idx])
                publish_now = False
                publish_at = schedule_times[idx]

                filename_base = os.path.basename(selected_videos[idx])
                filename_sem_ext = os.path.splitext(filename_base)[0]

                titulo_bd = get_title_from_db(filename_sem_ext)
                titulo_final = titulo_bd or filename_sem_ext

                try:
                    upload_video_to_youtube(
                        credentials_blob,
                        titulo_final,
                        description,
                        tags,
                        video_file,
                        thumb_file,
                        publish_now,
                        publish_at,
                        category_id="22"
                    )
                    set_video_entry(filename_sem_ext, titulo_final, canal, publish_at if publish_at else datetime.now(br_tz).isoformat())
                    enviados += 1
                    progress_upload["percent"] = int((enviados / total) * 100)
                    progress_upload["message"] = f"Enviado(s) {enviados} de {total} vídeo(s)."
                    esteta_seconds = ((total - enviados) * 60)
                    mins = esteta_seconds // 60
                    secs = esteta_seconds % 60
                    progress_upload["eta"] = f"{int(mins):02}:{int(secs):02}"
                except Exception as e:
                    progress_upload["message"] = f"Falha no upload do vídeo {filename_base}: {str(e)}"
                    return

            progress_upload["percent"] = 100
            progress_upload["eta"] = "00:00"
            progress_upload["message"] = f"Upload/agendamento concluído para {total} vídeos."
        except Exception as e:
            progress_upload["message"] = f"Erro interno no upload: {str(e)}"

    progress_upload["percent"] = 0
    progress_upload["eta"] = "--:--"
    progress_upload["message"] = "Upload/agendamento iniciado."

    thread = threading.Thread(target=processar_envio_func, daemon=True)
    thread.start()

    return jsonify({"message": "Upload/agendamento iniciado"})


@app.route('/create_profile', methods=['POST'])
def create_profile():
    data = request.json
    nome = data.get("nome", "").strip()
    canal = data.get("canal", "").strip()

    if not nome:
        return jsonify({"success": False, "message": "Nome do perfil é obrigatório."})
    if not canal:
        return jsonify({"success": False, "message": "Canal do perfil é obrigatório."})

    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT id FROM perfis WHERE nome=?", (nome,))
    exists = c.fetchone()
    if exists:
        conn.close()
        return jsonify({"success": False, "message": "Perfil com esse nome já existe."})

    try:
        c.execute("INSERT INTO perfis (nome, canal, credentials) VALUES (?, ?, ?)", (nome, canal, None))
        conn.commit()
        conn.close()
        return jsonify({"success": True, "message": f"Perfil '{nome}' criado com sucesso. Por favor, carregue as credenciais separadamente."})
    except Exception as e:
        conn.close()
        return jsonify({"success": False, "message": f"Erro ao criar perfil: {str(e)}"})


@app.route('/upload_credentials', methods=['POST'])
def upload_credentials():
    if 'credential_file' not in request.files:
        return jsonify({"success": False, "message": "Arquivo de credenciais não enviado."})

    file = request.files['credential_file']
    profile_nome = request.form.get('profile_nome', '').strip()

    if profile_nome == '':
        return jsonify({"success": False, "message": "Nome do perfil não informado."})

    if file.filename == '':
        return jsonify({"success": False, "message": "Nenhum arquivo selecionado."})

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        upload_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(upload_path)

        try:
            with open(upload_path, 'rb') as f:
                blob_data = f.read()

            with open(upload_path, 'rb') as f:
                _ = pickle.load(f)
        except Exception as e:
            os.remove(upload_path)
            return jsonify({"success": False, "message": f"Erro ao validar arquivo de credenciais: {str(e)}"})

        try:
            conn = sqlite3.connect(DB_PATH)
            c = conn.cursor()
            c.execute("SELECT id FROM perfis WHERE nome=?", (profile_nome,))
            exists = c.fetchone()
            if not exists:
                conn.close()
                os.remove(upload_path)
                return jsonify({"success": False, "message": "Perfil inexistente."})
            c.execute("UPDATE perfis SET credentials=? WHERE nome=?", (blob_data, profile_nome))
            conn.commit()
            conn.close()
            os.remove(upload_path)
            return jsonify({"success": True, "message": "Credenciais carregadas com sucesso para o perfil."})
        except Exception as e:
            if os.path.exists(upload_path):
                os.remove(upload_path)
            return jsonify({"success": False, "message": f"Erro ao salvar credenciais no banco: {str(e)}"})

    else:
        return jsonify({"success": False, "message": "Extensão de arquivo inválida. Apenas arquivos .pickle e .pkl são aceitos."})


if __name__ == "__main__":
    init_db()
    threading.Thread(target=lambda: app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False), daemon=False).start() . chat o efeito de movimento que ele faz na hora da edição preciso que ele seja mas imersivo com mas zoom e mais aleatorio de forma suave e natural mas preciso que entre mas na imagem . ele ainda não está apagando o video que foi enviado na parte de upload quando eu postar no final ele precisa apagar tanto o video como a thumbnail ele tá apagando a thumb mas o video não. outra coisa quando eu começa a enviar o video e quando começa o processo de edição não tá aparecendo a estimativa de tempo e na parte da barra ela só atualiza quando termina e vai para 100% direto quero que ela vai aumentando gradualmente conforme for avançado de forma individual . e também se quiser adicione outras coisa bacanas que sejam ulteis e graficas a hub . "Por favor, me dê o código completo e integral da solução, linha por linha, sem resumir ou omitir nada, mesmo que seja um código muito longo. Divida em partes se necessário, mas garanta que todas as funções, classes e trechos estejam presentes na íntegra." não quero comentários do tipo “o restante do código é este ou aquele” — tudo deve ser escrito. chat me de tudo mesmo que o codigo fique gigante e passe do limite faça o codigo completo mesmo que fique longo pfv você tá abreviando partes do codigo me de completo sem abreviar pfv escreva todas as partes linha por linha não tente me enganar . não pessa para minha incluir nem nada escreva você tudo