# Servidor LOCAL do Sistema de Nomeações da DRH — trabalho em equipe em tempo real, sem internet.
# Roda numa máquina da PM; os outros acessam pelo navegador no endereço mostrado abaixo.
# A base (base-nomeacoes-drh.json) fica NESTA máquina — nada vai para a nuvem.
#
# Como usar:  python servidor.py      (fique com esta janela aberta)
# Depois abra no navegador o endereço que aparecer, e passe-o para a equipe (mesma rede).
import json, os, socket, threading, http.server, socketserver, base64, time, secrets
from urllib.parse import urlparse, parse_qs, quote
from http.cookies import SimpleCookie

RAIZ = os.path.dirname(os.path.abspath(__file__))
BASE = os.path.join(RAIZ, 'base-nomeacoes-drh.json')
HTML = 'Sistema de Nomeações - DRH.html'
PORTA = 8000

# ---- SENHA de acesso (login do navegador). TROQUE por uma senha forte antes de expor externamente.
#      Para desligar a senha (só rede interna), deixe SENHA = ''.
USUARIO = 'drh'
SENHA = 'pmrr@gape'
# PIN curto do APP DO CELULAR pra EDITAR (nomear/trocar). Ver é livre; editar pede este PIN. TROQUE por outro.
PIN_APP = 'pmrr@gape'

# Tela de login (centralizada, com a cara da PMRR) — substitui a caixinha feia do navegador (HTTP Basic).
PAGINA_LOGIN = '''<!doctype html><html lang="pt-BR"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>Entrar · Nomeações DRH/PMRR</title>
<style>
:root{--navy:#123a6b;--navy2:#1a4a8a;--navy3:#0f2a52;--gold:#cba449}
*{box-sizing:border-box}html,body{height:100%;margin:0}
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,system-ui,sans-serif;color:#16202e;
 background:linear-gradient(135deg,#0f2a52,#1a4a8a 60%,#123a6b);display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#fff;width:100%;max-width:380px;border-radius:20px;padding:34px 28px 28px;box-shadow:0 24px 60px rgba(8,20,45,.45);
 border-top:4px solid var(--gold);text-align:center;animation:up .35s cubic-bezier(.2,.7,.3,1)}
@keyframes up{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
.logos{display:flex;align-items:center;justify-content:center;margin-bottom:2px}
.logos .pmrr{height:98px;width:auto;object-fit:contain}
h1{font-size:20px;margin:10px 0 2px;color:var(--navy3);letter-spacing:.2px}
.credito{margin-top:18px;padding-top:14px;border-top:1px solid #eef1f5;display:flex;align-items:center;justify-content:center;gap:9px}
.credito img{height:30px;width:auto;border-radius:7px;box-shadow:0 2px 7px rgba(12,30,60,.2)}
.credito .txt{text-align:left;color:#9aa5b3;font-size:11px;line-height:1.35}
.credito .txt b{color:#66707e;font-weight:700;display:block;font-size:11.5px}
.sub{color:#66707e;font-size:13px;margin-bottom:22px}
label{display:block;text-align:left;font-size:12px;font-weight:700;color:#8792a1;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px}
.wrap{position:relative}
input{width:100%;border:1.5px solid #dbe2ec;border-radius:12px;padding:13px 62px 13px 14px;font-size:16px;background:#fbfcfe;color:#16202e;outline:none}
input:focus{border-color:var(--navy2)}
.ver{position:absolute;right:6px;top:50%;transform:translateY(-50%);border:0;background:none;color:var(--navy2);font-weight:700;font-size:13px;padding:8px 10px;border-radius:9px;cursor:pointer}
.ver:active{background:#eef2f7}
button.pri{width:100%;margin-top:16px;background:var(--navy2);color:#fff;border:0;border-radius:12px;padding:14px;font-weight:700;font-size:15px;cursor:pointer}
button.pri:active{opacity:.94}button.pri:disabled{opacity:.55}
.erro{color:#b3261e;font-size:13.5px;min-height:18px;margin-top:10px}
.rod{margin-top:18px;color:#9aa5b3;font-size:11.5px}
</style></head><body>
<form class="card" id="f">
  <div class="logos">
    <img class="pmrr" src="/Acervo/Modelos/brasao_pmrr_web.png" alt="PMRR" title="Polícia Militar de Roraima" onerror="this.style.display='none'">
  </div>
  <h1>Sistema de Nomeações</h1>
  <div class="sub">Diretoria de Recursos Humanos · Polícia Militar de Roraima</div>
  <label for="s">Senha de acesso</label>
  <div class="wrap"><input id="s" type="password" autocomplete="current-password" autofocus placeholder="Digite a senha">
    <button type="button" class="ver" id="v">ver</button></div>
  <button class="pri" id="ok" type="submit">Entrar</button>
  <div class="erro" id="e"></div>
  <div class="credito">
    <img class="cdrh" src="/Acervo/Modelos/logo_drh_t.png" alt="DRH" title="Diretoria de Recursos Humanos" onerror="this.style.display='none'">
    <img src="/icone-192.png" alt="GAPE" title="GAPE" onerror="this.style.display='none'">
    <div class="txt"><b>DRH · GAPE · DTI</b>Diretoria de Recursos Humanos · Decreto e desenvolvimento: GAPE e DTI</div>
  </div>
</form>
<script>
var s=document.getElementById('s'),e=document.getElementById('e'),ok=document.getElementById('ok');
document.getElementById('v').onclick=function(){var p=s.type==='password';s.type=p?'text':'password';this.textContent=p?'ocultar':'ver';s.focus();};
document.getElementById('f').onsubmit=function(ev){ev.preventDefault();e.textContent='';ok.disabled=true;ok.textContent='Entrando…';
 fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({senha:s.value})})
  .then(function(r){if(r.status===401){e.textContent='Senha incorreta.';ok.disabled=false;ok.textContent='Entrar';return;}if(!r.ok)throw 0;location.reload();})
  .catch(function(){e.textContent='Erro de conexão.';ok.disabled=false;ok.textContent='Entrar';});};
</script></body></html>'''

_lock = threading.Lock()
_estado = {'versao': 1, 'config': {}, 'nomeacoes': [], 'afastamentos': [], 'disposicoes': [], 'recados': [], 'log': []}
_rev = 0
_sessoes = set()   # tokens de sessão válidos (login pela tela bonita, em vez da caixinha do navegador)


def carregar():
    global _estado
    if os.path.exists(BASE):
        try:
            _estado = json.load(open(BASE, encoding='utf-8'))
            _estado.setdefault('config', {}); _estado.setdefault('nomeacoes', []); _estado.setdefault('afastamentos', []); _estado.setdefault('disposicoes', []); _estado.setdefault('recados', []); _estado.setdefault('log', [])
        except Exception as e:
            print('!! não consegui ler a base:', e)


def salvar():
    tmp = BASE + '.tmp'
    json.dump(_estado, open(tmp, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
    os.replace(tmp, BASE)  # troca atômica (não corrompe se cair no meio)


def _merge(chave, upserts, deletes):
    """Merge por ID numa lista de _estado (upsert vence delete do mesmo id)."""
    _estado.setdefault(chave, [])
    up_ids = {n.get('id') for n in (upserts or [])}
    if deletes:
        rem = set(deletes) - up_ids
        _estado[chave] = [n for n in _estado[chave] if n.get('id') not in rem]
    por_id = {n['id']: i for i, n in enumerate(_estado[chave])}
    for n in (upserts or []):
        if n.get('id') in por_id:
            _estado[chave][por_id[n['id']]] = n
        else:
            por_id[n['id']] = len(_estado[chave]); _estado[chave].append(n)


def aplicar(upserts, deletes, config, afs_upserts=None, afs_deletes=None, disp_upserts=None, disp_deletes=None, rec_upserts=None, rec_deletes=None):
    """Aplica as mudanças de um cliente por ID (merge sem sobrescrever o que os outros fizeram)."""
    global _rev
    with _lock:
        _merge('nomeacoes', upserts, deletes)
        _merge('afastamentos', afs_upserts, afs_deletes)
        _merge('disposicoes', disp_upserts, disp_deletes)
        _merge('recados', rec_upserts, rec_deletes)
        if config:
            _estado['config'].update(config)
        _rev += 1
        salvar()
        return _rev


class Handler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *a, **k):
        super().__init__(*a, directory=RAIZ, **k)

    def log_message(self, *a):
        pass  # silencioso

    def end_headers(self):
        # NUNCA deixa o navegador guardar o HTML/JS em cache: assim, ao dar F5, a equipe sempre pega a versão nova.
        if not any(h.lower().startswith(b'cache-control') for h in self._headers_buffer):
            self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
        super().end_headers()

    def _cookie_ok(self):
        try:
            c = SimpleCookie(self.headers.get('Cookie', ''))
            tok = c['sessao'].value if 'sessao' in c else ''
            return bool(tok) and tok in _sessoes
        except Exception:
            return False

    def _auth_ok(self):
        if not SENHA:
            return True
        if self._cookie_ok():   # entrou pela tela de login bonita (cookie de sessão)
            return True
        h = self.headers.get('Authorization', '')
        if h.startswith('Basic '):
            try:
                u, p = base64.b64decode(h[6:]).decode('utf-8').split(':', 1)
                return (u == USUARIO and p == SENHA) or (PIN_APP and p == PIN_APP)   # senha do desktop OU PIN do app
            except Exception:
                return False
        return False

    def _pagina_login(self):
        corpo = PAGINA_LOGIN.encode('utf-8')
        self.send_response(401)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.send_header('Cache-Control', 'no-store')
        self.send_header('Content-Length', str(len(corpo)))
        self.end_headers()
        self.wfile.write(corpo)

    def _json(self, obj, code=200):
        corpo = json.dumps(obj, ensure_ascii=False).encode('utf-8')
        self.send_response(code)
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Cache-Control', 'no-store')
        self.send_header('Content-Length', str(len(corpo)))
        self.end_headers()
        self.wfile.write(corpo)

    def _publico(self, path):
        # PÚBLICO = só o necessário para DESENHAR a tela de login. Todo o resto (app, desktop, dados, fotos) exige ENTRAR.
        return path in ('/icone-192.png', '/apple-touch-icon.png', '/favicon-64.png',
                        '/Acervo/Modelos/brasao_pmrr_web.png', '/Acervo/Modelos/brasao_roraima.svg',
                        '/Acervo/Modelos/logo_drh_t.png')

    def do_GET(self):
        u = urlparse(self.path)
        if u.path == '/api/logout':   # sair: apaga a sessão e o cookie, volta pra tela de login
            try:
                c = SimpleCookie(self.headers.get('Cookie', '')); _sessoes.discard(c['sessao'].value if 'sessao' in c else '')
            except Exception:
                pass
            corpo = b'{"ok":true}'
            self.send_response(200)
            self.send_header('Content-Type', 'application/json; charset=utf-8')
            self.send_header('Set-Cookie', 'sessao=; Path=/; Max-Age=0')
            self.send_header('Content-Length', str(len(corpo)))
            self.end_headers(); self.wfile.write(corpo); return
        if not self._publico(u.path) and not self._auth_ok():
            return self._pagina_login()   # tela de login bonita (centralizada), no lugar da caixinha do navegador
        if u.path == '/':  # atalho: raiz abre o sistema direto (link curto para a equipe)
            ua = self.headers.get('User-Agent', '')
            if any(x in ua for x in ('Mobi', 'Android', 'iPhone', 'iPad', 'iPod')):   # celular -> app enxuto
                self.send_response(302); self.send_header('Location', '/app-celular.html'); self.end_headers(); return
            try: v = str(int(os.path.getmtime(os.path.join(RAIZ, HTML))))   # versão = data do arquivo; muda a URL a cada atualização (mata o cache do navegador)
            except Exception: v = '0'
            self.send_response(302); self.send_header('Location', '/' + quote(HTML) + '?v=' + v); self.end_headers(); return
        if u.path == '/api/base':
            since = parse_qs(u.query).get('since', [None])[0]
            with _lock:
                if since is not None and since.isdigit() and int(since) == _rev:
                    self.send_response(204); self.end_headers(); return
                self._json({'rev': _rev, 'versao': _estado.get('versao', 1),
                            'config': _estado.get('config', {}), 'nomeacoes': _estado['nomeacoes'],
                            'afastamentos': _estado.get('afastamentos', []), 'disposicoes': _estado.get('disposicoes', []),
                            'recados': _estado.get('recados', []), 'log': _estado.get('log', [])})
            return
        return super().do_GET()  # arquivos (HTML, fotos, etc.)

    def do_POST(self):
        global _rev
        u = urlparse(self.path)
        if u.path == '/api/recado':   # PÚBLICO (app do celular, sem senha): recado que chega no desktop
            n = int(self.headers.get('Content-Length', 0))
            try:
                d = json.loads(self.rfile.read(n) or b'{}')
            except Exception:
                return self._json({'erro': 'json inválido'}, 400)
            texto = (d.get('texto') or '').strip()
            if not texto:
                return self._json({'erro': 'vazio'}, 400)
            with _lock:
                _estado.setdefault('recados', []).append({
                    'id': 'r' + str(int(time.time() * 1000)), 'quando': time.strftime('%Y-%m-%dT%H:%M:%S'),
                    'nome': (d.get('nome') or '').strip()[:120], 'texto': texto[:2000], 'lido': False})
                _rev += 1; salvar()
            return self._json({'ok': True})
        if u.path == '/api/login':   # PÚBLICO: recebe a senha da tela bonita e devolve um cookie de sessão
            n = int(self.headers.get('Content-Length', 0))
            try:
                d = json.loads(self.rfile.read(n) or b'{}')
            except Exception:
                return self._json({'erro': 'json inválido'}, 400)
            if not SENHA or (d.get('senha') or '') == SENHA:
                tok = secrets.token_urlsafe(24); _sessoes.add(tok)
                corpo = json.dumps({'ok': True}).encode('utf-8')
                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                self.send_header('Set-Cookie', 'sessao=%s; Path=/; HttpOnly; SameSite=Lax; Max-Age=%d' % (tok, 60 * 60 * 24 * 30))
                self.send_header('Content-Length', str(len(corpo)))
                self.end_headers(); self.wfile.write(corpo); return
            return self._json({'erro': 'senha incorreta'}, 401)
        if not self._auth_ok():
            return self._json({'erro': 'nao autenticado'}, 401)   # 401 simples (sem WWW-Authenticate, pra não abrir a caixinha do navegador)
        u = urlparse(self.path)
        if u.path == '/api/sync':
            n = int(self.headers.get('Content-Length', 0))
            try:
                d = json.loads(self.rfile.read(n) or b'{}')
            except Exception:
                return self._json({'erro': 'json inválido'}, 400)
            rev = aplicar(d.get('upserts'), d.get('deletes'), d.get('config'), d.get('afsUpserts'), d.get('afsDeletes'), d.get('dispUpserts'), d.get('dispDeletes'), d.get('recUpserts'), d.get('recDeletes'))
            return self._json({'rev': rev})
        self._json({'erro': 'rota desconhecida'}, 404)


def ips_locais():
    ips = set()
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.connect(('8.8.8.8', 80))
        ips.add(s.getsockname()[0]); s.close()
    except Exception:
        pass
    try:
        for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
            ips.add(info[4][0])
    except Exception:
        pass
    return sorted(i for i in ips if not i.startswith('127.'))


class Servidor(socketserver.ThreadingMixIn, http.server.HTTPServer):
    daemon_threads = True
    allow_reuse_address = True


if __name__ == '__main__':
    if not os.path.exists(os.path.join(RAIZ, HTML)):
        print('!! Rode este arquivo DENTRO da pasta do sistema (onde está o "%s").' % HTML)
    carregar()
    srv = Servidor(('0.0.0.0', PORTA), Handler)
    print('=' * 64)
    print('  SISTEMA DE NOMEAÇÕES DA DRH — SERVIDOR LOCAL (tempo real)')
    print('=' * 64)
    print('  Base carregada:', len(_estado['nomeacoes']), 'lançamentos')
    print()
    print('  NESTE computador, abra:    http://localhost:%d' % PORTA)
    print()
    print('  Passe para a EQUIPE (mesma rede) este endereço:')
    for ip in ips_locais() or ['<ip-desta-maquina>']:
        print('     http://%s:%d' % (ip, PORTA))
    print()
    if SENHA:
        print('  LOGIN (o navegador vai pedir):   usuário: %s    senha: %s' % (USUARIO, SENHA))
        print()
    print('  A base fica SÓ nesta máquina. Deixe esta janela ABERTA enquanto trabalham.')
    print('  Para encerrar: feche a janela ou aperte Ctrl+C.')
    print('=' * 64)
    try:
        srv.serve_forever()
    except KeyboardInterrupt:
        print('\nEncerrado.')
