"""YakPanel - Site service"""
import os
import re
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select

from app.models.site import Site, Domain
from app.models.redirect import SiteRedirect
from app.core.config import get_runtime_config
from app.core.utils import path_safe_check, write_file, read_file, exec_shell_sync


DOMAIN_REGEX = re.compile(r"^([\w\-\*]{1,100}\.){1,8}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$")


def _render_vhost(
    template: str,
    server_names: str,
    root_path: str,
    logs_path: str,
    site_name: str,
    php_version: str,
    force_https: int,
    redirects: list[tuple[str, str, int]] | None = None,
) -> str:
    """Render nginx vhost template. redirects: [(source, target, code), ...]"""
    force_block = "return 301 https://$host$request_uri;" if force_https else ""
    redirect_lines = []
    for src, tgt, code in (redirects or []):
        if src and tgt:
            redirect_lines.append(f"    location = {src} {{ return {code} {tgt}; }}")
    redirect_block = "\n".join(redirect_lines) if redirect_lines else ""
    content = template.replace("{SERVER_NAMES}", server_names)
    content = content.replace("{ROOT_PATH}", root_path)
    content = content.replace("{LOGS_PATH}", logs_path)
    content = content.replace("{SITE_NAME}", site_name)
    content = content.replace("{PHP_VERSION}", php_version or "74")
    content = content.replace("{FORCE_HTTPS_BLOCK}", force_block)
    content = content.replace("{REDIRECTS_BLOCK}", redirect_block)
    return content


async def domain_format(domains: list[str]) -> str | None:
    """Validate domain format. Returns first invalid domain or None."""
    for d in domains:
        if not DOMAIN_REGEX.match(d):
            return d
    return None


async def domain_exists(db: AsyncSession, domains: list[str], exclude_site_id: int | None = None) -> str | None:
    """Check if domain already exists. Returns first existing domain or None."""
    for d in domains:
        parts = d.split(":")
        name, port = parts[0], parts[1] if len(parts) > 1 else "80"
        q = select(Domain).where(Domain.name == name, Domain.port == port)
        if exclude_site_id is not None:
            q = q.where(Domain.pid != exclude_site_id)
        result = await db.execute(q)
        if result.scalar_one_or_none():
            return d
    return None


async def create_site(
    db: AsyncSession,
    name: str,
    path: str,
    domains: list[str],
    project_type: str = "PHP",
    ps: str = "",
    php_version: str = "74",
    force_https: int = 0,
) -> dict:
    """Create a new site with vhost config."""
    if not path_safe_check(name) or not path_safe_check(path):
        return {"status": False, "msg": "Invalid site name or path"}

    invalid = await domain_format(domains)
    if invalid:
        return {"status": False, "msg": f"Invalid domain format: {invalid}"}

    existing = await domain_exists(db, domains)
    if existing:
        return {"status": False, "msg": f"Domain already exists: {existing}"}

    cfg = get_runtime_config()
    setup_path = cfg["setup_path"]
    www_root = cfg["www_root"]
    www_logs = cfg["www_logs"]
    vhost_path = os.path.join(setup_path, "panel", "vhost", "nginx")

    site_path = os.path.join(www_root, name)
    if not os.path.exists(site_path):
        os.makedirs(site_path, 0o755)

    site = Site(name=name, path=site_path, ps=ps, project_type=project_type, php_version=php_version or "74", force_https=force_https or 0)
    db.add(site)
    await db.flush()

    for d in domains:
        parts = d.split(":")
        domain_name, port = parts[0], parts[1] if len(parts) > 1 else "80"
        db.add(Domain(pid=site.id, name=domain_name, port=port))

    await db.flush()

    # Generate Nginx vhost
    conf_path = os.path.join(vhost_path, f"{name}.conf")
    panel_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    template_path = os.path.join(panel_root, "webserver", "templates", "nginx_site.conf")

    if os.path.exists(template_path):
        template = read_file(template_path) or ""
        server_names = " ".join(d.split(":")[0] for d in domains)
        content = _render_vhost(template, server_names, site_path, www_logs, name, php_version or "74", force_https or 0, [])
        write_file(conf_path, content)

    # Reload Nginx if available
    nginx_bin = os.path.join(setup_path, "nginx", "sbin", "nginx")
    if os.path.exists(nginx_bin):
        exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")

    await db.commit()
    return {"status": True, "msg": "Site created", "id": site.id}


async def list_sites(db: AsyncSession) -> list[dict]:
    """List all sites with domain count."""
    result = await db.execute(select(Site).order_by(Site.id))
    sites = result.scalars().all()
    out = []
    for s in sites:
        domain_result = await db.execute(select(Domain).where(Domain.pid == s.id))
        domains = domain_result.scalars().all()
        out.append({
            "id": s.id,
            "name": s.name,
            "path": s.path,
            "status": s.status,
            "ps": s.ps,
            "project_type": s.project_type,
            "domain_count": len(domains),
            "addtime": s.addtime.isoformat() if s.addtime else None,
        })
    return out


async def delete_site(db: AsyncSession, site_id: int) -> dict:
    """Delete a site and its vhost config."""
    result = await db.execute(select(Site).where(Site.id == site_id))
    site = result.scalar_one_or_none()
    if not site:
        return {"status": False, "msg": "Site not found"}

    await db.execute(Domain.__table__.delete().where(Domain.pid == site_id))
    await db.execute(SiteRedirect.__table__.delete().where(SiteRedirect.site_id == site_id))
    await db.delete(site)

    cfg = get_runtime_config()
    conf_path = os.path.join(cfg["setup_path"], "panel", "vhost", "nginx", f"{site.name}.conf")
    if os.path.exists(conf_path):
        os.remove(conf_path)

    nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx")
    if os.path.exists(nginx_bin):
        exec_shell_sync(f"{nginx_bin} -s reload")

    await db.commit()
    return {"status": True, "msg": "Site deleted"}


async def get_site_count(db: AsyncSession) -> int:
    """Get total site count."""
    from sqlalchemy import func
    result = await db.execute(select(func.count()).select_from(Site))
    return result.scalar() or 0


async def get_site_with_domains(db: AsyncSession, site_id: int) -> dict | None:
    """Get site with domain list for editing."""
    result = await db.execute(select(Site).where(Site.id == site_id))
    site = result.scalar_one_or_none()
    if not site:
        return None
    domain_result = await db.execute(select(Domain).where(Domain.pid == site.id))
    domains = domain_result.scalars().all()
    domain_list = [f"{d.name}:{d.port}" if d.port != "80" else d.name for d in domains]
    return {
        "id": site.id,
        "name": site.name,
        "path": site.path,
        "status": site.status,
        "ps": site.ps,
        "project_type": site.project_type,
        "php_version": getattr(site, "php_version", None) or "74",
        "force_https": getattr(site, "force_https", 0) or 0,
        "domains": domain_list,
    }


async def update_site(
    db: AsyncSession,
    site_id: int,
    path: str | None = None,
    domains: list[str] | None = None,
    ps: str | None = None,
    php_version: str | None = None,
    force_https: int | None = None,
) -> dict:
    """Update site domains, path, or note."""
    result = await db.execute(select(Site).where(Site.id == site_id))
    site = result.scalar_one_or_none()
    if not site:
        return {"status": False, "msg": "Site not found"}

    if domains is not None:
        invalid = await domain_format(domains)
        if invalid:
            return {"status": False, "msg": f"Invalid domain format: {invalid}"}
        existing = await domain_exists(db, domains, exclude_site_id=site_id)
        if existing:
            return {"status": False, "msg": f"Domain already exists: {existing}"}
        await db.execute(Domain.__table__.delete().where(Domain.pid == site_id))
        for d in domains:
            parts = d.split(":")
            domain_name, port = parts[0], parts[1] if len(parts) > 1 else "80"
            db.add(Domain(pid=site.id, name=domain_name, port=port))

    if path is not None and path_safe_check(path):
        site.path = path

    if ps is not None:
        site.ps = ps
    if php_version is not None:
        site.php_version = php_version or "74"
    if force_https is not None:
        site.force_https = 1 if force_https else 0

    await db.flush()

    # Regenerate Nginx vhost if domains, php_version, or force_https changed
    if domains is not None or php_version is not None or force_https is not None:
        cfg = get_runtime_config()
        vhost_path = os.path.join(cfg["setup_path"], "panel", "vhost", "nginx")
        conf_path = os.path.join(vhost_path, f"{site.name}.conf")
        panel_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
        template_path = os.path.join(panel_root, "webserver", "templates", "nginx_site.conf")
        if os.path.exists(template_path):
            template = read_file(template_path) or ""
            domain_result = await db.execute(select(Domain).where(Domain.pid == site.id))
            domain_rows = domain_result.scalars().all()
            domain_list = [f"{d.name}:{d.port}" if d.port != "80" else d.name for d in domain_rows]
            server_names = " ".join(d.split(":")[0] for d in domain_list) if domain_list else site.name
            php_ver = getattr(site, "php_version", None) or "74"
            fhttps = getattr(site, "force_https", 0) or 0
            redir_result = await db.execute(select(SiteRedirect).where(SiteRedirect.site_id == site.id))
            redirects = [(r.source, r.target, r.code or 301) for r in redir_result.scalars().all()]
            content = _render_vhost(template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects)
            write_file(conf_path, content)
        nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx")
        if os.path.exists(nginx_bin):
            exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")

    await db.commit()
    return {"status": True, "msg": "Site updated"}


def _vhost_path(site_name: str) -> tuple[str, str]:
    """Return (conf_path, disabled_path) for site vhost."""
    cfg = get_runtime_config()
    vhost_dir = os.path.join(cfg["setup_path"], "panel", "vhost", "nginx")
    disabled_dir = os.path.join(vhost_dir, "disabled")
    return (
        os.path.join(vhost_dir, f"{site_name}.conf"),
        os.path.join(disabled_dir, f"{site_name}.conf"),
    )


async def set_site_status(db: AsyncSession, site_id: int, status: int) -> dict:
    """Enable (1) or disable (0) site by moving vhost config."""
    result = await db.execute(select(Site).where(Site.id == site_id))
    site = result.scalar_one_or_none()
    if not site:
        return {"status": False, "msg": "Site not found"}

    conf_path, disabled_path = _vhost_path(site.name)
    disabled_dir = os.path.dirname(disabled_path)

    if status == 1:  # enable
        if os.path.isfile(disabled_path):
            os.makedirs(os.path.dirname(conf_path), exist_ok=True)
            os.rename(disabled_path, conf_path)
    else:  # disable
        if os.path.isfile(conf_path):
            os.makedirs(disabled_dir, exist_ok=True)
            os.rename(conf_path, disabled_path)

    site.status = status
    await db.commit()

    nginx_bin = os.path.join(get_runtime_config()["setup_path"], "nginx", "sbin", "nginx")
    if os.path.exists(nginx_bin):
        exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")

    return {"status": True, "msg": "Site " + ("enabled" if status == 1 else "disabled")}


async def regenerate_site_vhost(db: AsyncSession, site_id: int) -> dict:
    """Regenerate nginx vhost for a site (e.g. after redirect changes)."""
    result = await db.execute(select(Site).where(Site.id == site_id))
    site = result.scalar_one_or_none()
    if not site:
        return {"status": False, "msg": "Site not found"}
    cfg = get_runtime_config()
    vhost_path = os.path.join(cfg["setup_path"], "panel", "vhost", "nginx")
    conf_path = os.path.join(vhost_path, f"{site.name}.conf")
    if site.status != 1:
        return {"status": True, "msg": "Site disabled, vhost not active"}
    panel_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    template_path = os.path.join(panel_root, "webserver", "templates", "nginx_site.conf")
    if not os.path.exists(template_path):
        return {"status": False, "msg": "Template not found"}
    template = read_file(template_path) or ""
    domain_result = await db.execute(select(Domain).where(Domain.pid == site.id))
    domain_rows = domain_result.scalars().all()
    domain_list = [f"{d.name}:{d.port}" if d.port != "80" else d.name for d in domain_rows]
    server_names = " ".join(d.split(":")[0] for d in domain_list) if domain_list else site.name
    php_ver = getattr(site, "php_version", None) or "74"
    fhttps = getattr(site, "force_https", 0) or 0
    redir_result = await db.execute(select(SiteRedirect).where(SiteRedirect.site_id == site.id))
    redirects = [(r.source, r.target, r.code or 301) for r in redir_result.scalars().all()]
    content = _render_vhost(template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects)
    write_file(conf_path, content)
    nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx")
    if os.path.exists(nginx_bin):
        exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")
    return {"status": True, "msg": "Vhost regenerated"}
