#!/usr/bin/env python3
import json
import os
import re
import sqlite3
import subprocess
import time
from collections import deque
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional

import psutil
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Response
from fastapi.responses import PlainTextResponse, RedirectResponse
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel

# ---------------------- Constants ----------------------
DB_PATH = "/4server/data/contracts.db"
BIN_PATH = "/4server/sbin"
API_KEY = os.getenv("API_KEY", "your-secret-api-key")
VERSION = "API: 0.0.9"

# ---------------------- FastAPI App ----------------------
app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")


def verify_api_key(key: str = Depends(api_key_header)):
    if key != API_KEY:
        raise HTTPException(status_code=403, detail="Unauthorized")


# ---------------------- Helpers ----------------------
def run_command(cmd: list[str]) -> str:
    """Run a shell command and return stdout or raise HTTPException on error."""
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        error_msg = (
            f"Command failed\n"
            f"Command: {' '.join(cmd)}\n"
            f"Return code: {result.returncode}\n"
            f"Stdout: {result.stdout.strip()}\n"
            f"Stderr: {result.stderr.strip() or 'None'}"
        )
        raise HTTPException(status_code=500, detail=error_msg)
    return result.stdout.strip()


def init_db():
    """Initialize the database with containers table."""
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS containers (
            ID INTEGER PRIMARY KEY AUTOINCREMENT,
            UUID CHAR(50) UNIQUE,
            email CHAR(100),
            expires DATE,
            tags TEXT,
            env TEXT,
            affiliate CHAR(30),
            image CHAR(50),
            history TEXT,
            comment TEXT,
            domains TEXT,
            status CHAR(20),
            created DATE,
            bump DATE,
            secret TEXT,
            contract TEXT,
            git integer,
            backup_slots integer,
            hdd integer,
            workers integer,
            utm_source text,
            utm_medium text,
            utm_campaign text,
            domains_slots integer,
            secret_list TEXT,
            env_list TEXT
        )
    """)
    conn.commit()
    conn.close()


def execute_db(query: str, params: tuple = (), fetch: bool = False):
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    cursor.execute(query, params)
    conn.commit()
    data = cursor.fetchall() if fetch else None
    conn.close()
    if data and fetch:
        return [dict(row) for row in data]
    return None


# ---------------------- Models ----------------------
class ContractItem(BaseModel):
    quantity: int
    name: str
    product_id: int
    features: Dict[str, Any]


class ContainerModel(BaseModel):
    UUID: str
    email: Optional[str] = None
    expires: Optional[str] = None
    tags: Optional[str] = None
    image: Optional[str] = None
    history: Optional[str] = None
    comment: Optional[str] = None
    domains: Optional[str] = None
    status: Optional[str] = None
    created: Optional[str] = None
    bump: Optional[str] = None
    git: Optional[int] = None
    backup_slots: Optional[int] = None
    hdd: Optional[int] = None
    workers: Optional[int] = None
    utm_source: Optional[str] = None
    utm_medium: Optional[str] = None
    utm_campaign: Optional[str] = None
    domains_slots: Optional[int] = None
<<<<<<< HEAD
    secret_list: Optional[str] = None
=======
>>>>>>> c583649 (hmm)
    env_list: Optional[str] = None


class UUIDRequest(BaseModel):
    UUID: str


class CommandRequest(BaseModel):
    uuid: str
    method: int


class ImportRequest(BaseModel):
    filename: str


class MoveRequest(BaseModel):
    source: str
    destination: str


class AddKeyRequest(BaseModel):
    key: str
    uuid: str


class ImageRequest(BaseModel):
    image: str


# ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False)
def redirect_to_odoo():
    return RedirectResponse(url="https://ODOO4PROJECTS.com")


import json
from typing import Any, Dict, Optional

from fastapi import Depends, FastAPI
from fastapi.responses import RedirectResponse
from pydantic import BaseModel

app = FastAPI()


# ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False)
def redirect_to_odoo():
    return RedirectResponse(url="https://ODOO4PROJECTS.com")


@app.post("/container/update", dependencies=[Depends(verify_api_key)])
def update_container(request: ContainerModel):
    # Fetch existing record
    existing = execute_db(
        "SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True
    )
    if not existing:
        # If record does not exist, insert a new one with all given fields
        execute_db(
            """
            INSERT INTO containers (UUID, email, expires, tags, image, history,
            comment, domains, status, created, bump, git, backup_slots, hdd, workers,
<<<<<<< HEAD
            utm_source, utm_medium, utm_campaign, domains_slots, secret_list, env_list)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
=======
            utm_source, utm_medium, utm_campaign, domains_slots, env_list)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
>>>>>>> c583649 (hmm)
        """,
            (
                request.UUID,
                request.email,
                request.expires,
                request.tags,
                request.image,
                request.history,
                request.comment,
                request.domains,
                request.status,
                request.created,
                request.bump,
                request.git,
                request.backup_slots,
                request.hdd,
                request.workers,
                request.utm_source,
                request.utm_medium,
                request.utm_campaign,
                request.domains_slots,
<<<<<<< HEAD
                request.secret_list,
=======
>>>>>>> c583649 (hmm)
                request.env_list,
            ),
        )
        return {"UUID": request.UUID, "status": "created"}

    # Existing record found, do partial update
    existing = existing[0]  # Assuming fetch returns list of dicts
    updates = {}
    params = []

    # Only add fields that are not None in the request
    for field in ContainerModel.__fields__:
        if field == "UUID":
            continue
        value = getattr(request, field)
        if value is not None:
            updates[field] = value
            params.append(value)

    if updates:
        # Build SQL dynamically
        set_clause = ", ".join(f"{k}=?" for k in updates.keys())
        params.append(request.UUID)  # UUID for WHERE clause
        execute_db(f"UPDATE containers SET {set_clause} WHERE UUID=?", tuple(params))
        return {
            "UUID": request.UUID,
            "status": "updated",
            "fields_updated": list(updates.keys()),
        }

    return {"UUID": request.UUID, "status": "no_change"}


@app.post("/container/start", dependencies=[Depends(verify_api_key)])
def start_container(request: UUIDRequest):
    return {"message": run_command([f"{BIN_PATH}/startContainer", request.UUID])}


@app.post("/container/restartAllContainers", dependencies=[Depends(verify_api_key)])
def start_container(request: ImageRequest):
    return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}


@app.post("/container/stop", dependencies=[Depends(verify_api_key)])
def stop_container(request: UUIDRequest):
    try:
        return {"message": run_command([f"{BIN_PATH}/stopContainer", request.UUID])}
    except HTTPException:
        # Command failed, but continue without error to the API
        return {"message": "Container stop command executed (may have failed)"}


@app.post("/container/nuke", dependencies=[Depends(verify_api_key)])
def nuke_container(request: UUIDRequest):
    status = execute_db(
        "SELECT status FROM containers WHERE UUID=?", (request.UUID,), fetch=True
    )
    if not status or status[0]["status"] != "nuke":
        raise HTTPException(400, "Container status is not 'nuke'")
    return {"message": run_command([f"{BIN_PATH}/nukeContainer", request.UUID])}


@app.post("/container/info", dependencies=[Depends(verify_api_key)])
def info_container(request: Optional[UUIDRequest] = None):
    # Fields to select
    fields = [
        "ID",
        "UUID",
        "email",
        "expires",
        "tags",
        "image",
        "history",
        "comment",
        "domains",
        "status",
        "created",
        "git",
        "backup_slots",
        "hdd",
        "workers",
        "utm_source",
        "utm_medium",
        "utm_campaign",
        "domains_slots",
        "env_list",
    ]
    field_str = ", ".join(fields)

    # Execute query
    if request:
        rows = execute_db(
            f"SELECT {field_str} FROM containers WHERE UUID=?",
            (str(request.UUID),),
            fetch=True,
        )
    else:
        rows = execute_db(f"SELECT {field_str} FROM containers", fetch=True)

    # Map rows to dicts safely
    containers = []
    for row in rows:
        if isinstance(row, dict):
            # Already a dict (e.g., some DB wrappers)
            containers.append(row)
        else:
            # Tuple/list -> map with fields
            containers.append(dict(zip(fields, row)))

    # Wrap in n8n JSON format
    n8n_items = [{"json": container} for container in containers]

    return n8n_items


@app.post("/container/bump", dependencies=[Depends(verify_api_key)])
def bump_container(request: UUIDRequest):
    today = datetime.utcnow().strftime("%Y-%m-%d")
    execute_db("UPDATE containers SET bump=? WHERE UUID=?", (today, request.UUID))
    msg = run_command([f"{BIN_PATH}/bumpContainer", request.UUID])
    return {"message": msg, "bump_date": today}


@app.post("/container/quota", dependencies=[Depends(verify_api_key)])
def container_quota(request: UUIDRequest):
    output = run_command(
        [
            "docker",
            "stats",
            request.UUID,
            "--no-stream",
            "--format",
            "{{.MemUsage}},{{.BlockIO}}",
        ]
    )
    mem_usage, disk_usage = output.split(",")
    return {"memory_usage": mem_usage, "disk_io": disk_usage}


# ---------------------- SYSTEM ----------------------
@app.get("/system/containers", dependencies=[Depends(verify_api_key)])
def get_containers():
    return Response(
        content=run_command([f"{BIN_PATH}/getContainers"]),
        media_type="application/json",
    )


@app.get("/system/images", dependencies=[Depends(verify_api_key)])
def list_images():
    images = run_command(["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"])
    return {"images": images.split("\n")}


@app.get("/system/cpu", dependencies=[Depends(verify_api_key)])
def get_cpu_log():
    CPU_LOG_PATH = Path("/4server/data/log/cpu.log")
    if not CPU_LOG_PATH.exists():
        raise HTTPException(status_code=404, detail="CPU log file not found")

    try:
        with CPU_LOG_PATH.open("r") as f:
            content = f.read()
        return PlainTextResponse(content)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Error reading CPU log: {e}")


@app.get("/system/info", dependencies=[Depends(verify_api_key)])
def get_system_info():
    try:
        alpine_version = None
        last_update = None
        bump_dates = execute_db(
            "SELECT MAX(bump) AS latest_bump FROM containers", fetch=True
        )[0]["latest_bump"]
        if os.path.exists("/4server/data/update"):
            with open("/4server/data/update") as f:
                last_update = f.read().strip()
        if os.path.exists("/etc/alpine-release"):
            with open("/etc/alpine-release") as f:
                alpine_version = f.read().strip()
        mem = psutil.virtual_memory()
        disk = psutil.disk_usage("/")
        cpu_count = psutil.cpu_count(logical=True)
        return {
            "alpine_version": alpine_version,
            "last_update": last_update,
            "latest_bump": bump_dates,
            "version": VERSION,
            "resources": {
                "memory": {
                    "total": mem.total,
                    "available": mem.available,
                    "used": mem.used,
                },
                "disk": {"total": disk.total, "used": disk.used, "free": disk.free},
                "cpu_count": cpu_count,
            },
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/system/pull", dependencies=[Depends(verify_api_key)])
def pull_all_images():
    return {"message": run_command([f"{BIN_PATH}/pullAllContainers"])}


@app.post("/system/restartAllContainers", dependencies=[Depends(verify_api_key)])
def restart_all_containers(request: ImageRequest):
    if not request.image:
        raise HTTPException(status_code=400, detail="Image name is required")
    return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}


@app.post("/client/git", dependencies=[Depends(verify_api_key)])
def git_tool(request: CommandRequest):
    if request.method == 1:
        command = [f"{BIN_PATH}/gitPull", request.uuid]
    elif request.method == 2:
        command = [f"{BIN_PATH}/gitRevert", request.uuid]
    else:
        raise HTTPException(status_code=400, detail="Invalid method")

    output = run_command(command)
    return {"message": output}


@app.post("/client/addGitKey", dependencies=[Depends(verify_api_key)])
def add_git_key(request: AddKeyRequest):
    if not request.key or not request.uuid:
        raise HTTPException(status_code=400, detail="Key and uuid are required")

    file_path = f"/4server/data/{request.uuid}/git-server/keys/id_rsa.pub"

    try:
        # Ensure the directory exists
        os.makedirs(os.path.dirname(file_path), exist_ok=True)
        # Write the key to the file
        with open(file_path, "w", encoding="utf-8") as f:
            f.write(request.key)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to save key: {str(e)}")

    return {"message": f"Key saved for container {request.uuid}", "file": file_path}


@app.get("/client/audit/{uuid}", dependencies=[Depends(verify_api_key)])
def audit_odoo(uuid: str):
    return {"message": run_command([f"{BIN_PATH}/ODOO_19/listModules", uuid])}


@app.get("/client/logs/{uuid}", dependencies=[Depends(verify_api_key)])
async def get_odoo_log_summary(uuid: str):
    if not re.fullmatch(r"[0-9a-fA-F\-]+", uuid):
        raise HTTPException(
            status_code=400,
            detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.",
        )

    BASE_LOG_DIR = "/4server/data"

    # Build file paths as strings
    project_dir = os.path.join(BASE_LOG_DIR, uuid)
    odoo_log_file = os.path.join(project_dir, "logs", "odoo.log")
    git_log_file = os.path.join(project_dir, "logs", "git.log")

    if not os.path.isfile(odoo_log_file):
        raise HTTPException(status_code=404, detail="Odoo log file not found")

    # --- Helper variables and functions ---
    IMPORTANT_PATTERNS = [
        re.compile(r"odoo\.addons\."),  # Any Odoo addon log
        re.compile(r"Job '.*' starting"),  # Cron job start
        re.compile(r"Job '.*' fully done"),  # Cron job end
        re.compile(r"ERROR"),  # Errors
        re.compile(r"WARNING"),  # Warnings
        re.compile(r"Traceback"),  # Tracebacks
        re.compile(r"Error"),
    ]

    def is_important_line(line: str) -> bool:
        return any(p.search(line) for p in IMPORTANT_PATTERNS)

    def read_last_lines(file_path: str, max_lines: int) -> list[str]:
        """
        Read the last `max_lines` from the file efficiently.
        """
        if not os.path.isfile(file_path):
            return []
        last_lines = deque(maxlen=max_lines)
        with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                last_lines.append(line.strip())
        return list(last_lines)

    # --- Main logic ---
    try:
        # Last 500 lines from Odoo log
        last_500_lines = read_last_lines(odoo_log_file, 500)
        important_odoo_lines = [
            line for line in last_500_lines if is_important_line(line)
        ]

        # Last 50 lines from git.log
        last_50_git_lines = read_last_lines(git_log_file, 50)

        return {
            "uuid": str(uuid),
            "important_odoo_log_lines": important_odoo_lines,
            "last_git_log_lines": last_50_git_lines,
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Error reading log files: {e}")


# ------------------------ BACKUP HANDLING -------------------------------------
@app.post("/backup/import", dependencies=[Depends(verify_api_key)])
def backup_import(request: ImportRequest):
    if not request.filename:
        raise HTTPException(status_code=400, detail="Filename is required")

    command = [f"{BIN_PATH}/ODOO_19/import", request.filename]
    output = run_command(command)
    return {"message": output}


@app.post("/backup/move", dependencies=[Depends(verify_api_key)])
def backup_move(request: MoveRequest):
    if not request.source or not request.destination:
        raise HTTPException(
            status_code=400, detail="Source and destination are required"
        )

    if not os.path.exists(request.source):
        raise HTTPException(status_code=404, detail="Source file does not exist")

    # Use shell command to move the file
    command = ["mv", request.source, request.destination]  # Linux/macOS
    # For Windows, use: command = ["move", request.source, request.destination]

    output = run_command(command)
    return {
        "message": f"Moved {request.source} to {request.destination}",
        "output": output,
    }


@app.get("/backup/borgpush", dependencies=[Depends(verify_api_key)])
def backup_borgpush():
    return {"message": run_command(["doas", "-u", "4server", f"{BIN_PATH}/borgpush"])}


# ---------------------- Entry Point ----------------------
if __name__ == "__main__":
    print(VERSION)
    init_db()
    time.sleep(25)
    uvicorn.run(app, host="10.5.0.1", port=8888)
