from __future__ import annotations
_L='recycle'
_K='auto_redeploy'
_J='A deploy is already in progress for this project.'
_I='running'
_H='deploying'
_G='folder'
_F='success'
_E='Deployment not found.'
_D='system'
_C=True
_B='failed'
_A=None
import shutil,threading,traceback
from datetime import datetime,timezone
from pathlib import Path
from typing import Optional
from admin import config
from admin.models import Deployment,db
from admin.services.entrypoint import EntrypointError,resolve_entrypoint
from admin.services.fingerprint import audit_sources
from admin.services.folder_ingest import FolderIngestError,sync_folder,validate_project_folder
from admin.services.iis import IISError,probe_app,provision_iis_app,recycle_app_pool,remove_iis_app,write_web_config
from admin.services.releases import archive_build,prune_releases,restore_archive
from admin.services.util import append_log,is_valid_slug,slugify
from admin.services.venv_builder import VenvError,ensure_venv
_deploy_locks={}
_locks_guard=threading.Lock()
class DeployError(Exception):0
def app_dir_for(slug):return Path(config.APPS_ROOT)/slug
def log_path_for(slug):return Path(config.DATA_ROOT)/'logs'/f"{slug}.log"
def _slug_lock(slug):
	A=slug
	with _locks_guard:
		if A not in _deploy_locks:_deploy_locks[A]=threading.Lock()
		return _deploy_locks[A]
def create_and_deploy_folder(*,name,slug,source_path,entrypoint,requirements_path,auto_deploy=_C,actor=_A):
	A=slug;A=(A or slugify(name)).lower()
	if not is_valid_slug(A):raise DeployError('Invalid slug. Use lowercase letters, numbers, and hyphens.')
	try:C=validate_project_folder(source_path)
	except FolderIngestError as B:raise DeployError(str(B))from B
	if db.get_by_slug(A):raise DeployError(f"Deployment '{A}' already exists.")
	from admin.services.license import assert_can_add_project as D
	try:D()
	except PermissionError as B:raise DeployError(str(B))from B
	E=str(app_dir_for(A));F=db.create(name=name.strip()or A,slug=A,source_type=_G,source_path=str(C),app_path=E,entrypoint=entrypoint.strip()or'app:app',requirements_path=requirements_path.strip()or'requirements.txt',status=_H,auto_deploy=auto_deploy);_run_pipeline(F,event_type='deploy',actor=actor or _D);return db.get_by_slug(A)
def redeploy(slug,*,event_type='redeploy',actor=_A):
	C=event_type;A=slug;B=db.get_by_slug(A)
	if not B:raise DeployError(_E)
	if B.source_type!=_G or not B.source_path:raise DeployError('This deployment has no connected project folder.')
	D=_slug_lock(A)
	if not D.acquire(blocking=False):raise DeployError(_J)
	try:db.update_status(A,_H,last_error=_A);B=db.get_by_slug(A);_run_pipeline(B,event_type=C,actor=actor or('auto-deploy'if C==_K else _D));return db.get_by_slug(A)
	finally:D.release()
def rollback(slug,release_id,*,actor=_A):
	K=release_id;J='rollback';E=actor;A=slug;F=db.get_by_slug(A)
	if not F:raise DeployError(_E)
	B=db.get_release(K)
	if not B or B.slug!=A:raise DeployError('Release not found for this project.')
	if B.is_current:raise DeployError('That release is already current.')
	I=Path(B.archive_path)
	if not I.is_file():raise DeployError(f"Release archive missing: {I}")
	L=_slug_lock(A)
	if not L.acquire(blocking=False):raise DeployError(_J)
	E=E or _D;C=log_path_for(A);G=app_dir_for(A);D=_A
	if F.source_path:
		try:D=audit_sources(F.source_path)
		except Exception:D=_A
	try:
		db.update_status(A,_H,last_error=_A);append_log(C,f"\n==== Rollback to {B.label} ====");restore_archive(I,G,C);N=resolve_entrypoint(G,F.entrypoint,C);ensure_venv(G,C,requirements_rel=F.requirements_path);write_web_config(G,slug=A,entrypoint=N,log_path=C);provision_iis_app(A,G,C);recycle_app_pool(A,C,action=_L);O=probe_app(A,C);P=config.DRY_RUN or O
		if P:db.set_current_release(A,B.id);Q=B.fingerprint or'';db.update_fingerprint(A,Q,B.py_bytes,current_release_id=B.id);db.update_status(A,_I,last_error=_A);db.add_event(slug=A,event_type=J,status=_F,message=f"Rolled back to {B.label}",release_id=B.id,py_bytes=B.py_bytes,fingerprint=B.fingerprint,actor=E,source_mtime=D.source_mtime if D else _A,source_owner=D.source_owner if D else _A);append_log(C,'Rollback finished successfully')
		else:M='Rollback probe failed; see deploy log';db.update_status(A,_B,last_error=M);db.add_event(slug=A,event_type=J,status=_B,message=M,release_id=B.id,actor=E,source_mtime=D.source_mtime if D else _A,source_owner=D.source_owner if D else _A)
		return db.get_by_slug(A)
	except Exception as H:append_log(C,f"ROLLBACK ERROR: {H}\n{traceback.format_exc()}");db.update_status(A,_B,last_error=str(H));db.add_event(slug=A,event_type=J,status=_B,message=str(H),release_id=K,actor=E);raise DeployError(str(H))from H
	finally:L.release()
def stop_app(slug,*,actor=_A):
	E='stop';B=actor;A=slug;D=db.get_by_slug(A)
	if not D:raise DeployError(_E)
	B=B or _D;F=log_path_for(A)
	try:recycle_app_pool(A,F,action=E);db.update_status(A,'stopped',last_error=_A);db.add_event(slug=A,event_type=E,status=_F,message='App pool stopped',actor=B)
	except IISError as C:db.update_status(A,D.status,last_error=str(C));raise DeployError(str(C))from C
	return db.get_by_slug(A)
def start_app(slug,*,actor=_A):
	G='Probe failed after start';F='start';C=actor;A=slug;H=db.get_by_slug(A)
	if not H:raise DeployError(_E)
	C=C or _D;E=log_path_for(A)
	try:recycle_app_pool(A,E,action=F);B=probe_app(A,E);db.update_status(A,_I if B else _B,last_error=_A if B else G);db.add_event(slug=A,event_type=F,status=_F if B else _B,message='App pool started'if B else G,actor=C)
	except IISError as D:db.update_status(A,_B,last_error=str(D));raise DeployError(str(D))from D
	return db.get_by_slug(A)
def delete_app(slug,*,actor=_A):
	B=actor;A=slug;C=db.get_by_slug(A)
	if not C:raise DeployError(_E)
	B=B or _D;D=log_path_for(A)
	try:remove_iis_app(A,D)
	except IISError as G:append_log(D,f"IIS remove warning: {G}")
	E=Path(C.app_path)
	if E.exists():shutil.rmtree(E,ignore_errors=_C)
	F=config.RELEASES_ROOT/A
	if F.exists():shutil.rmtree(F,ignore_errors=_C)
	db.add_event(slug=A,event_type='delete',status=_F,message='Deployment deleted',actor=B);db.delete(A)
def set_auto_deploy(slug,enabled,*,actor=_A):
	B=enabled;A=slug;C=db.get_by_slug(A)
	if not C:raise DeployError(_E)
	db.set_auto_deploy(A,B);db.add_event(slug=A,event_type='settings',status=_F,message=f"Auto-deploy {"enabled"if B else"disabled"}",actor=actor or _D);return db.get_by_slug(A)
def read_log(slug,max_chars=20000):
	B=max_chars;C=log_path_for(slug)
	if not C.is_file():return'(no log yet)'
	A=C.read_text(encoding='utf-8',errors='replace')
	if len(A)>B:return A[-B:]
	return A
def _run_pipeline(deployment,*,event_type,actor=_A):
	F=deployment;E=actor;D=event_type;A=F.slug;E=E or _D;B=log_path_for(A)
	if B.exists():append_log(B,f"\n==== {D} by {E} ====")
	else:append_log(B,f"Deploy start for {A} ({D}) by {E}")
	I=app_dir_for(A);config.APPS_ROOT.mkdir(parents=_C,exist_ok=_C);config.RELEASES_ROOT.mkdir(parents=_C,exist_ok=_C);G='';C=0;J=_A;K=_A
	try:
		if F.source_type!=_G or not F.source_path:raise DeployError('Connected project folder is required.')
		L=audit_sources(F.source_path);G=L.fingerprint;C=L.py_bytes;J=L.source_mtime;K=L.source_owner;append_log(B,f"Source fingerprint: {G[:12]}…  py_bytes={C}  files~={L.file_count}  mtime={J}  owner={K}");sync_folder(F.source_path,I,B);P=resolve_entrypoint(I,F.entrypoint,B);ensure_venv(I,B,requirements_rel=F.requirements_path);write_web_config(I,slug=A,entrypoint=P,log_path=B);provision_iis_app(A,I,B);recycle_app_pool(A,B,action=_L);Q=probe_app(A,B);R=config.DRY_RUN or Q
		if R:N=datetime.now(timezone.utc).strftime('r%Y%m%d-%H%M%S');S=archive_build(A,I,B,N);O=db.create_release(slug=A,label=N,archive_path=str(S),fingerprint=G,py_bytes=C,make_current=_C);db.update_fingerprint(A,G,C,current_release_id=O.id);db.update_status(A,_I,last_error=_A);prune_releases(A,B);M=f"{D} succeeded ({C} bytes of tracked sources)"if D!=_K else f"Auto-redeploy succeeded after Python source change ({C} bytes)";db.add_event(slug=A,event_type=D,status=_F,message=M,release_id=O.id,py_bytes=C,fingerprint=G,actor=E,source_mtime=J,source_owner=K);append_log(B,'Deploy finished successfully')
		else:M='Health probe failed; see deploy log';db.update_status(A,_B,last_error=M);db.add_event(slug=A,event_type=D,status=_B,message=M,py_bytes=C,fingerprint=G,actor=E,source_mtime=J,source_owner=K);append_log(B,'Deploy completed but probe failed')
	except(FolderIngestError,VenvError,EntrypointError,IISError,DeployError)as H:append_log(B,f"ERROR: {H}");db.update_status(A,_B,last_error=str(H));db.add_event(slug=A,event_type=D,status=_B,message=str(H),py_bytes=C or _A,fingerprint=G or _A,actor=E,source_mtime=J,source_owner=K)
	except Exception as H:append_log(B,f"UNEXPECTED ERROR: {H}\n{traceback.format_exc()}");db.update_status(A,_B,last_error=str(H));db.add_event(slug=A,event_type=D,status=_B,message=str(H),actor=E,source_mtime=J,source_owner=K)
